Add generator support for PlatformIO (#718)
Add rules for running nanopb generator from PlatformIO build.
Added example and test for a PlatformIO based build.
diff --git a/.github/workflows/platformio.yaml b/.github/workflows/platformio.yaml
new file mode 100644
index 0000000..8843bfa
--- /dev/null
+++ b/.github/workflows/platformio.yaml
@@ -0,0 +1,47 @@
+name: platformio
+
+on:
+ push:
+ pull_request:
+
+jobs:
+ platformio-example:
+ name: Build and run PlatformIO example
+ runs-on: ubuntu-latest
+ steps:
+ - name: ⤵️ Check out code from GitHub
+ uses: actions/checkout@v2
+
+ - name: Installing dependencies for local act
+ if: ${{ env.ACT }}
+ run: |
+ sudo apt update
+
+ - name: Installing common dependencies
+ run: |
+ sudo apt install -y python3-pip python3-protobuf protobuf-compiler
+
+ - name: Install and setup PlatformIO
+ run: |
+ pip3 install -U platformio
+ export PATH=~/.local/bin:$PATH
+
+ - name: Build PlatformIO package
+ run: pio package pack
+
+ - name: Extract PlatformIO package to example dir
+ run: |
+ mkdir -p examples/platformio/lib/nanopb
+ tar -xzf Nanopb-*.tar.gz -C examples/platformio/lib/nanopb
+ ls -l examples/platformio/lib/nanopb
+
+ - name: 🚀 Build
+ run: |
+ cd examples/platformio
+ pio run
+
+ - name: Run test without options
+ run: examples/platformio/.pio/build/pio_without_options/program
+
+ - name: Run test with options
+ run: examples/platformio/.pio/build/pio_with_options/program
diff --git a/examples/platformio/.gitignore b/examples/platformio/.gitignore
new file mode 100644
index 0000000..bf8c477
--- /dev/null
+++ b/examples/platformio/.gitignore
@@ -0,0 +1,5 @@
+.pio/
+.idea/
+cmake-build-*/
+CMakeLists.txt
+CMakeListsPrivate.txt
diff --git a/examples/platformio/platformio.ini b/examples/platformio/platformio.ini
new file mode 100644
index 0000000..bf0d544
--- /dev/null
+++ b/examples/platformio/platformio.ini
@@ -0,0 +1,35 @@
+;
+; You can setup `nanopb_protos` `nanopb_options` vars to generate code from proto files
+;
+; Generator will use next folders:
+;
+; `$BUILD_DIR/nanopb/generated-src` - `*.pb.h` and `*.pb.c` files
+; `$BUILD_DIR/nanopb/md5` - MD5 files to track changes in source .proto/.options
+;
+; Compiled `.pb.o` files will be located under `$BUILD_DIR/nanopb/generated-build`
+;
+; Example:
+
+[env:pio_with_options]
+platform = native
+lib_deps = Nanopb
+
+src_filter =
+ +<pio_with_options.c>
+
+; All path are relative to the `$PROJECT_DIR`
+nanopb_protos =
+ +<proto/pio_with_options.proto>
+nanopb_options =
+ --error-on-unmatched
+
+[env:pio_without_options]
+platform = native
+lib_deps = Nanopb
+
+src_filter =
+ +<pio_without_options.c>
+
+; All path are relative to the `$PROJECT_DIR`
+nanopb_protos =
+ +<proto/pio_without_options.proto>
diff --git a/examples/platformio/proto/pio_with_options.options b/examples/platformio/proto/pio_with_options.options
new file mode 100644
index 0000000..fe2dbee
--- /dev/null
+++ b/examples/platformio/proto/pio_with_options.options
@@ -0,0 +1 @@
+TestMessageWithOptions.str max_size:16
diff --git a/examples/platformio/proto/pio_with_options.proto b/examples/platformio/proto/pio_with_options.proto
new file mode 100644
index 0000000..58e00ed
--- /dev/null
+++ b/examples/platformio/proto/pio_with_options.proto
@@ -0,0 +1,5 @@
+syntax = "proto3";
+
+message TestMessageWithOptions {
+ string str = 1;
+}
diff --git a/examples/platformio/proto/pio_without_options.proto b/examples/platformio/proto/pio_without_options.proto
new file mode 100644
index 0000000..2284488
--- /dev/null
+++ b/examples/platformio/proto/pio_without_options.proto
@@ -0,0 +1,5 @@
+syntax = "proto3";
+
+message TestMessageWithoutOptions {
+ int32 number = 1;
+}
diff --git a/examples/platformio/src/pio_with_options.c b/examples/platformio/src/pio_with_options.c
new file mode 100644
index 0000000..f558c61
--- /dev/null
+++ b/examples/platformio/src/pio_with_options.c
@@ -0,0 +1,35 @@
+#include "pb_encode.h"
+#include "pb_decode.h"
+
+#include "test.h"
+
+#include "pio_with_options.pb.h"
+
+int main(int argc, char *argv[]) {
+
+ int status = 0;
+
+ uint8_t buffer[256];
+ pb_ostream_t ostream;
+ pb_istream_t istream;
+ size_t written;
+
+ TestMessageWithOptions original = TestMessageWithOptions_init_zero;
+ strcpy(original.str,"Hello");
+
+ ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
+
+ TEST(pb_encode(&ostream, &TestMessageWithOptions_msg, &original));
+
+ written = ostream.bytes_written;
+
+ istream = pb_istream_from_buffer(buffer, written);
+
+ TestMessageWithOptions decoded = TestMessageWithOptions_init_zero;
+
+ TEST(pb_decode(&istream, &TestMessageWithOptions_msg, &decoded));
+
+ TEST(strcmp(decoded.str,"Hello") == 0);
+
+ return status;
+}
diff --git a/examples/platformio/src/pio_without_options.c b/examples/platformio/src/pio_without_options.c
new file mode 100644
index 0000000..1ab59f9
--- /dev/null
+++ b/examples/platformio/src/pio_without_options.c
@@ -0,0 +1,35 @@
+#include "pb_encode.h"
+#include "pb_decode.h"
+
+#include "test.h"
+
+#include "pio_without_options.pb.h"
+
+int main(int argc, char *argv[]) {
+
+ int status = 0;
+
+ uint8_t buffer[256];
+ pb_ostream_t ostream;
+ pb_istream_t istream;
+ size_t written;
+
+ TestMessageWithoutOptions original = TestMessageWithoutOptions_init_zero;
+ original.number = 45;
+
+ ostream = pb_ostream_from_buffer(buffer, sizeof(buffer));
+
+ TEST(pb_encode(&ostream, &TestMessageWithoutOptions_msg, &original));
+
+ written = ostream.bytes_written;
+
+ istream = pb_istream_from_buffer(buffer, written);
+
+ TestMessageWithoutOptions decoded = TestMessageWithoutOptions_init_zero;
+
+ TEST(pb_decode(&istream, &TestMessageWithoutOptions_msg, &decoded));
+
+ TEST(decoded.number == 45);
+
+ return status;
+}
diff --git a/examples/platformio/src/test.h b/examples/platformio/src/test.h
new file mode 100644
index 0000000..63895da
--- /dev/null
+++ b/examples/platformio/src/test.h
@@ -0,0 +1,9 @@
+#include <stdio.h>
+
+#define TEST(x) \
+ if (!(x)) { \
+ fprintf(stderr, "\033[31;1mFAILED:\033[22;39m %s:%d %s\n", __FILE__, __LINE__, #x); \
+ status = 1; \
+ } else { \
+ printf("\033[32;1mOK:\033[22;39m %s\n", #x); \
+ }
diff --git a/generator/platformio_generator.py b/generator/platformio_generator.py
new file mode 100644
index 0000000..452424c
--- /dev/null
+++ b/generator/platformio_generator.py
@@ -0,0 +1,127 @@
+import os
+import hashlib
+import pathlib
+from platformio import fs
+
+Import("env")
+
+nanopb_root = os.path.join(os.getcwd(), '..')
+
+project_dir = env.subst("$PROJECT_DIR")
+build_dir = env.subst("$BUILD_DIR")
+
+generated_src_dir = os.path.join(build_dir, 'nanopb', 'generated-src')
+generated_build_dir = os.path.join(build_dir, 'nanopb', 'generated-build')
+md5_dir = os.path.join(build_dir, 'nanopb', 'md5')
+
+nanopb_protos = env.GetProjectOption("nanopb_protos", "")
+nanopb_plugin_options = env.GetProjectOption("nanopb_options", "")
+
+if not nanopb_protos:
+ print("[nanopb] No `nanopb_protos` specified, exiting.")
+ exit(0)
+
+if isinstance(nanopb_plugin_options, (list, tuple)):
+ nanopb_plugin_options = " ".join(nanopb_plugin_options)
+
+nanopb_plugin_options = nanopb_plugin_options.split()
+
+protos_files = fs.match_src_files(project_dir, nanopb_protos)
+if not len(protos_files):
+ print("[nanopb] ERROR: No files matched pattern:")
+ print(f"nanopb_protos: {nanopb_protos}")
+ exit(1)
+
+protoc_generator = os.path.join(nanopb_root, 'generator', 'protoc')
+
+nanopb_options = ""
+nanopb_options += f" --nanopb_out={generated_src_dir}"
+for opt in nanopb_plugin_options:
+ nanopb_options += (" --nanopb_opt=" + opt)
+
+try:
+ os.makedirs(generated_src_dir)
+except FileExistsError:
+ pass
+
+try:
+ os.makedirs(md5_dir)
+except FileExistsError:
+ pass
+
+# Collect include dirs based on
+proto_include_dirs = set()
+for proto_file in protos_files:
+ proto_file_abs = os.path.join(project_dir, proto_file)
+ proto_dir = os.path.dirname(proto_file_abs)
+ proto_include_dirs.add(proto_dir)
+
+for proto_include_dir in proto_include_dirs:
+ nanopb_options += (" --proto_path=" + proto_include_dir)
+ nanopb_options += (" --nanopb_opt=-I" + proto_include_dir)
+
+for proto_file in protos_files:
+ proto_file_abs = os.path.join(project_dir, proto_file)
+
+ proto_file_path_abs = os.path.dirname(proto_file_abs)
+ proto_file_basename = os.path.basename(proto_file_abs)
+ proto_file_without_ext = os.path.splitext(proto_file_basename)[0]
+
+ proto_file_md5_abs = os.path.join(md5_dir, proto_file_basename + '.md5')
+ proto_file_current_md5 = hashlib.md5(pathlib.Path(proto_file_abs).read_bytes()).hexdigest()
+
+ options_file = proto_file_without_ext + ".options"
+ options_file_abs = os.path.join(proto_file_path_abs, options_file)
+ options_file_md5_abs = None
+ options_file_current_md5 = None
+ if pathlib.Path(options_file_abs).exists():
+ options_file_md5_abs = os.path.join(md5_dir, options_file + '.md5')
+ options_file_current_md5 = hashlib.md5(pathlib.Path(options_file_abs).read_bytes()).hexdigest()
+ else:
+ options_file = None
+
+ header_file = proto_file_without_ext + ".pb.h"
+ source_file = proto_file_without_ext + ".pb.c"
+
+ header_file_abs = os.path.join(generated_src_dir, source_file)
+ source_file_abs = os.path.join(generated_src_dir, header_file)
+
+ need_generate = False
+
+ # Check proto file md5
+ try:
+ last_md5 = pathlib.Path(proto_file_md5_abs).read_text()
+ if last_md5 != proto_file_current_md5:
+ need_generate = True
+ except FileNotFoundError:
+ need_generate = True
+
+ if options_file:
+ # Check options file md5
+ try:
+ last_md5 = pathlib.Path(options_file_md5_abs).read_text()
+ if last_md5 != options_file_current_md5:
+ need_generate = True
+ except FileNotFoundError:
+ need_generate = True
+
+ options_info = f"{options_file}" if options_file else "no options"
+
+ if not need_generate:
+ print(f"[nanopb] Skipping '{proto_file}' ({options_info})")
+ else:
+ print(f"[nanopb] Processing '{proto_file}' ({options_info})")
+ cmd = protoc_generator + " " + nanopb_options + " " + proto_file_basename
+ result = env.Execute(cmd)
+ if result != 0:
+ print(f"[nanopb] ERROR: ({result}) processing cmd: '{cmd}'")
+ exit(1)
+ pathlib.Path(proto_file_md5_abs).write_text(proto_file_current_md5)
+ if options_file:
+ pathlib.Path(options_file_md5_abs).write_text(options_file_current_md5)
+
+#
+# Add generated includes and sources to build environment
+#
+env.Append(CPPPATH=[generated_src_dir])
+env.BuildSources(generated_build_dir, generated_src_dir)
diff --git a/library.json b/library.json
index 9336458..2fed5ea 100644
--- a/library.json
+++ b/library.json
@@ -1,6 +1,6 @@
{
"name": "Nanopb",
- "version": "0.4.5",
+ "version": "0.4.6",
"keywords": "protocol buffers, protobuf, google",
"description": "Nanopb is a plain-C implementation of Google's Protocol Buffers data format. It is targeted at 32 bit microcontrollers, but is also fit for other embedded systems with tight (<10 kB ROM, <1 kB RAM) memory constraints.",
"repository": {
@@ -17,10 +17,26 @@
"*.c",
"*.cpp",
"*.h",
- "examples"
+ "examples",
+ "generator"
+ ],
+ "exclude": [
+ "generator/**/__pycache__",
+ "examples/platformio/.gitignore"
]
},
- "examples": "examples/*/*.c",
+ "build": {
+ "extraScript": "generator/platformio_generator.py",
+ "srcDir": "",
+ "srcFilter": [
+ "+<*.c>"
+ ]
+ },
+ "examples": [
+ "examples/platformio/platformio.ini",
+ "examples/platformio/src/*.c",
+ "examples/*/*.c"
+ ],
"frameworks": "*",
"platforms": "*"
}