1import os 2import hashlib 3import pathlib 4import shlex 5import subprocess 6 7import SCons.Action 8from platformio import fs 9 10Import("env") 11 12# We don't use `env.Execute` because it does not handle spaces in path 13# See https://github.com/nanopb/nanopb/pull/834 14# So, we resolve the path to the executable and then use `subprocess.run` 15python_exe = env.subst("$PYTHONEXE") 16 17try: 18 import protobuf 19except ImportError: 20 print("[nanopb] Installing Protocol Buffers dependencies"); 21 22 # We need to specify protobuf version. In other case got next (on Ubuntu 20.04): 23 # Requirement already satisfied: protobuf in /usr/lib/python3/dist-packages (3.6.1) 24 subprocess.run([python_exe, '-m', 'pip', 'install', "protobuf>=3.19.1"]) 25 26try: 27 import grpc_tools.protoc 28except ImportError: 29 print("[nanopb] Installing gRPC dependencies"); 30 subprocess.run([python_exe, '-m', 'pip', 'install', "grpcio-tools>=1.43.0"]) 31 32 33nanopb_root = os.path.join(os.getcwd(), '..') 34 35project_dir = env.subst("$PROJECT_DIR") 36build_dir = env.subst("$BUILD_DIR") 37 38generated_src_dir = os.path.join(build_dir, 'nanopb', 'generated-src') 39generated_build_dir = os.path.join(build_dir, 'nanopb', 'generated-build') 40md5_dir = os.path.join(build_dir, 'nanopb', 'md5') 41 42nanopb_protos = env.GetProjectOption("custom_nanopb_protos", "") 43nanopb_plugin_options = env.GetProjectOption("custom_nanopb_options", "") 44 45if not nanopb_protos: 46 print("[nanopb] No generation needed.") 47else: 48 if isinstance(nanopb_plugin_options, (list, tuple)): 49 nanopb_plugin_options = " ".join(nanopb_plugin_options) 50 51 nanopb_plugin_options = shlex.split(nanopb_plugin_options) 52 53 protos_files = fs.match_src_files(project_dir, nanopb_protos) 54 if not len(protos_files): 55 print("[nanopb] ERROR: No files matched pattern:") 56 print(f"custom_nanopb_protos: {nanopb_protos}") 57 exit(1) 58 59 nanopb_generator = os.path.join(nanopb_root, 'generator', 'nanopb_generator.py') 60 61 nanopb_options = [] 62 nanopb_options.extend(["--output-dir", generated_src_dir]) 63 for opt in nanopb_plugin_options: 64 nanopb_options.append(opt) 65 66 try: 67 os.makedirs(generated_src_dir) 68 except FileExistsError: 69 pass 70 71 try: 72 os.makedirs(md5_dir) 73 except FileExistsError: 74 pass 75 76 # Collect include dirs based on 77 proto_include_dirs = set() 78 for proto_file in protos_files: 79 proto_file_abs = os.path.join(project_dir, proto_file) 80 proto_dir = os.path.dirname(proto_file_abs) 81 proto_include_dirs.add(proto_dir) 82 83 for proto_include_dir in proto_include_dirs: 84 nanopb_options.extend(["--proto-path", proto_include_dir]) 85 86 for proto_file in protos_files: 87 proto_file_abs = os.path.join(project_dir, proto_file) 88 89 proto_file_path_abs = os.path.dirname(proto_file_abs) 90 proto_file_basename = os.path.basename(proto_file_abs) 91 proto_file_without_ext = os.path.splitext(proto_file_basename)[0] 92 93 proto_file_md5_abs = os.path.join(md5_dir, proto_file_basename + '.md5') 94 proto_file_current_md5 = hashlib.md5(pathlib.Path(proto_file_abs).read_bytes()).hexdigest() 95 96 options_file = proto_file_without_ext + ".options" 97 options_file_abs = os.path.join(proto_file_path_abs, options_file) 98 options_file_md5_abs = None 99 options_file_current_md5 = None 100 if pathlib.Path(options_file_abs).exists(): 101 options_file_md5_abs = os.path.join(md5_dir, options_file + '.md5') 102 options_file_current_md5 = hashlib.md5(pathlib.Path(options_file_abs).read_bytes()).hexdigest() 103 else: 104 options_file = None 105 106 header_file = proto_file_without_ext + ".pb.h" 107 source_file = proto_file_without_ext + ".pb.c" 108 109 header_file_abs = os.path.join(generated_src_dir, source_file) 110 source_file_abs = os.path.join(generated_src_dir, header_file) 111 112 need_generate = False 113 114 # Check proto file md5 115 try: 116 last_md5 = pathlib.Path(proto_file_md5_abs).read_text() 117 if last_md5 != proto_file_current_md5: 118 need_generate = True 119 except FileNotFoundError: 120 need_generate = True 121 122 if options_file: 123 # Check options file md5 124 try: 125 last_md5 = pathlib.Path(options_file_md5_abs).read_text() 126 if last_md5 != options_file_current_md5: 127 need_generate = True 128 except FileNotFoundError: 129 need_generate = True 130 131 options_info = f"{options_file}" if options_file else "no options" 132 133 if not need_generate: 134 print(f"[nanopb] Skipping '{proto_file}' ({options_info})") 135 else: 136 print(f"[nanopb] Processing '{proto_file}' ({options_info})") 137 cmd = [python_exe, nanopb_generator] + nanopb_options + [proto_file_basename] 138 action = SCons.Action.CommandAction(cmd) 139 result = env.Execute(action) 140 if result != 0: 141 print(f"[nanopb] ERROR: ({result}) processing cmd: '{cmd}'") 142 exit(1) 143 pathlib.Path(proto_file_md5_abs).write_text(proto_file_current_md5) 144 if options_file: 145 pathlib.Path(options_file_md5_abs).write_text(options_file_current_md5) 146 147 # 148 # Add generated includes and sources to build environment 149 # 150 env.Append(CPPPATH=[generated_src_dir]) 151 152 # Fix for ESP32 ESP-IDF https://github.com/nanopb/nanopb/issues/734#issuecomment-1001544447 153 global_env = DefaultEnvironment() 154 already_called_env_name = "_PROTOBUF_GENERATOR_ALREADY_CALLED_" + env['PIOENV'].replace("-", "_") 155 if not global_env.get(already_called_env_name, False): 156 env.BuildSources(generated_build_dir, generated_src_dir) 157 global_env[already_called_env_name] = True 158