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