signature.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  1. #
  2. # signature.py
  3. #
  4. import schema
  5. import subprocess,re,json,hashlib
  6. from datetime import datetime
  7. from pathlib import Path
  8. #
  9. # Return all macro names in a header as an array, so we can take
  10. # the intersection with the preprocessor output, giving a decent
  11. # reflection of all enabled options that (probably) came from the
  12. # configuration files. We end up with the actual configured state,
  13. # better than what the config files say. You can then use the
  14. # resulting config.ini to produce more exact configuration files.
  15. #
  16. def extract_defines(filepath):
  17. f = open(filepath, encoding="utf8").read().split("\n")
  18. a = []
  19. for line in f:
  20. sline = line.strip()
  21. if sline[:7] == "#define":
  22. # Extract the key here (we don't care about the value)
  23. kv = sline[8:].strip().split()
  24. a.append(kv[0])
  25. return a
  26. # Compute the SHA256 hash of a file
  27. def get_file_sha256sum(filepath):
  28. sha256_hash = hashlib.sha256()
  29. with open(filepath,"rb") as f:
  30. # Read and update hash string value in blocks of 4K
  31. for byte_block in iter(lambda: f.read(4096),b""):
  32. sha256_hash.update(byte_block)
  33. return sha256_hash.hexdigest()
  34. #
  35. # Compress a JSON file into a zip file
  36. #
  37. import zipfile
  38. def compress_file(filepath, storedname, outpath):
  39. with zipfile.ZipFile(outpath, 'w', compression=zipfile.ZIP_BZIP2, compresslevel=9) as zipf:
  40. zipf.write(filepath, arcname=storedname, compress_type=zipfile.ZIP_BZIP2, compresslevel=9)
  41. #
  42. # Compute the build signature. The idea is to extract all defines in the configuration headers
  43. # to build a unique reversible signature from this build so it can be included in the binary
  44. # We can reverse the signature to get a 1:1 equivalent configuration file
  45. #
  46. def compute_build_signature(env):
  47. if 'BUILD_SIGNATURE' in env:
  48. return
  49. # Definitions from these files will be kept
  50. files_to_keep = [ 'Marlin/Configuration.h', 'Marlin/Configuration_adv.h' ]
  51. build_path = Path(env['PROJECT_BUILD_DIR'], env['PIOENV'])
  52. # Check if we can skip processing
  53. hashes = ''
  54. for header in files_to_keep:
  55. hashes += get_file_sha256sum(header)[0:10]
  56. marlin_json = build_path / 'marlin_config.json'
  57. marlin_zip = build_path / 'mc.zip'
  58. # Read existing config file
  59. try:
  60. with marlin_json.open() as infile:
  61. conf = json.load(infile)
  62. if conf['__INITIAL_HASH'] == hashes:
  63. # Same configuration, skip recomputing the building signature
  64. compress_file(marlin_json, 'marlin_config.json', marlin_zip)
  65. return
  66. except:
  67. pass
  68. # Get enabled config options based on preprocessor
  69. from preprocessor import run_preprocessor
  70. complete_cfg = run_preprocessor(env)
  71. # Dumb #define extraction from the configuration files
  72. conf_defines = {}
  73. all_defines = []
  74. for header in files_to_keep:
  75. defines = extract_defines(header)
  76. # To filter only the define we want
  77. all_defines += defines
  78. # To remember from which file it cames from
  79. conf_defines[header.split('/')[-1]] = defines
  80. r = re.compile(r"\(+(\s*-*\s*_.*)\)+")
  81. # First step is to collect all valid macros
  82. defines = {}
  83. for line in complete_cfg:
  84. # Split the define from the value
  85. key_val = line[8:].strip().decode().split(' ')
  86. key, value = key_val[0], ' '.join(key_val[1:])
  87. # Ignore values starting with two underscore, since it's low level
  88. if len(key) > 2 and key[0:2] == "__" :
  89. continue
  90. # Ignore values containing a parenthesis (likely a function macro)
  91. if '(' in key and ')' in key:
  92. continue
  93. # Then filter dumb values
  94. if r.match(value):
  95. continue
  96. defines[key] = value if len(value) else ""
  97. #
  98. # Continue to gather data for CONFIGURATION_EMBEDDING or CONFIG_EXPORT
  99. #
  100. if not ('CONFIGURATION_EMBEDDING' in defines or 'CONFIG_EXPORT' in defines):
  101. return
  102. # Second step is to filter useless macro
  103. resolved_defines = {}
  104. for key in defines:
  105. # Remove all boards now
  106. if key.startswith("BOARD_") and key != "BOARD_INFO_NAME":
  107. continue
  108. # Remove all keys ending by "_NAME" as it does not make a difference to the configuration
  109. if key.endswith("_NAME") and key != "CUSTOM_MACHINE_NAME":
  110. continue
  111. # Remove all keys ending by "_T_DECLARED" as it's a copy of extraneous system stuff
  112. if key.endswith("_T_DECLARED"):
  113. continue
  114. # Remove keys that are not in the #define list in the Configuration list
  115. if key not in all_defines + [ 'DETAILED_BUILD_VERSION', 'STRING_DISTRIBUTION_DATE' ]:
  116. continue
  117. # Don't be that smart guy here
  118. resolved_defines[key] = defines[key]
  119. # Generate a build signature now
  120. # We are making an object that's a bit more complex than a basic dictionary here
  121. data = {}
  122. data['__INITIAL_HASH'] = hashes
  123. # First create a key for each header here
  124. for header in conf_defines:
  125. data[header] = {}
  126. # Then populate the object where each key is going to (that's a O(N^2) algorithm here...)
  127. for key in resolved_defines:
  128. for header in conf_defines:
  129. if key in conf_defines[header]:
  130. data[header][key] = resolved_defines[key]
  131. # Every python needs this toy
  132. def tryint(key):
  133. try:
  134. return int(defines[key])
  135. except:
  136. return 0
  137. config_dump = tryint('CONFIG_EXPORT')
  138. #
  139. # Produce an INI file if CONFIG_EXPORT == 2
  140. #
  141. if config_dump == 2:
  142. print("Generating config.ini ...")
  143. config_ini = build_path / 'config.ini'
  144. with config_ini.open('w') as outfile:
  145. ignore = ('CONFIGURATION_H_VERSION', 'CONFIGURATION_ADV_H_VERSION', 'CONFIG_EXPORT')
  146. filegrp = { 'Configuration.h':'config:basic', 'Configuration_adv.h':'config:advanced' }
  147. vers = defines["CONFIGURATION_H_VERSION"]
  148. dt_string = datetime.now().strftime("%Y-%m-%d at %H:%M:%S")
  149. ini_fmt = '{0:40}{1}\n'
  150. outfile.write(
  151. '#\n'
  152. + '# Marlin Firmware\n'
  153. + '# config.ini - Options to apply before the build\n'
  154. + '#\n'
  155. + f'# Generated by Marlin build on {dt_string}\n'
  156. + '#\n'
  157. + '\n'
  158. + '[config:base]\n'
  159. + ini_fmt.format('ini_use_config', ' = all')
  160. + ini_fmt.format('ini_config_vers', f' = {vers}')
  161. )
  162. # Loop through the data array of arrays
  163. for header in data:
  164. if header.startswith('__'):
  165. continue
  166. outfile.write('\n[' + filegrp[header] + ']\n')
  167. for key in sorted(data[header]):
  168. if key not in ignore:
  169. val = 'on' if data[header][key] == '' else data[header][key]
  170. outfile.write(ini_fmt.format(key.lower(), ' = ' + val))
  171. #
  172. # Produce a schema.json file if CONFIG_EXPORT == 3
  173. #
  174. if config_dump >= 3:
  175. try:
  176. conf_schema = schema.extract()
  177. except Exception as exc:
  178. print("Error: " + str(exc))
  179. conf_schema = None
  180. if conf_schema:
  181. #
  182. # Produce a schema.json file if CONFIG_EXPORT == 3
  183. #
  184. if config_dump in (3, 13):
  185. print("Generating schema.json ...")
  186. schema.dump_json(conf_schema, build_path / 'schema.json')
  187. if config_dump == 13:
  188. schema.group_options(conf_schema)
  189. schema.dump_json(conf_schema, build_path / 'schema_grouped.json')
  190. #
  191. # Produce a schema.yml file if CONFIG_EXPORT == 4
  192. #
  193. elif config_dump == 4:
  194. print("Generating schema.yml ...")
  195. try:
  196. import yaml
  197. except ImportError:
  198. env.Execute(env.VerboseAction(
  199. '$PYTHONEXE -m pip install "pyyaml"',
  200. "Installing YAML for schema.yml export",
  201. ))
  202. import yaml
  203. schema.dump_yaml(conf_schema, build_path / 'schema.yml')
  204. # Append the source code version and date
  205. data['VERSION'] = {}
  206. data['VERSION']['DETAILED_BUILD_VERSION'] = resolved_defines['DETAILED_BUILD_VERSION']
  207. data['VERSION']['STRING_DISTRIBUTION_DATE'] = resolved_defines['STRING_DISTRIBUTION_DATE']
  208. try:
  209. curver = subprocess.check_output(["git", "describe", "--match=NeVeRmAtCh", "--always"]).strip()
  210. data['VERSION']['GIT_REF'] = curver.decode()
  211. except:
  212. pass
  213. #
  214. # Produce a JSON file for CONFIGURATION_EMBEDDING or CONFIG_EXPORT == 1
  215. #
  216. if config_dump == 1 or 'CONFIGURATION_EMBEDDING' in defines:
  217. with marlin_json.open('w') as outfile:
  218. json.dump(data, outfile, separators=(',', ':'))
  219. #
  220. # The rest only applies to CONFIGURATION_EMBEDDING
  221. #
  222. if not 'CONFIGURATION_EMBEDDING' in defines:
  223. return
  224. # Compress the JSON file as much as we can
  225. compress_file(marlin_json, 'marlin_config.json', marlin_zip)
  226. # Generate a C source file for storing this array
  227. with open('Marlin/src/mczip.h','wb') as result_file:
  228. result_file.write(
  229. b'#ifndef NO_CONFIGURATION_EMBEDDING_WARNING\n'
  230. + b' #warning "Generated file \'mc.zip\' is embedded (Define NO_CONFIGURATION_EMBEDDING_WARNING to suppress this warning.)"\n'
  231. + b'#endif\n'
  232. + b'const unsigned char mc_zip[] PROGMEM = {\n '
  233. )
  234. count = 0
  235. for b in (build_path / 'mc.zip').open('rb').read():
  236. result_file.write(b' 0x%02X,' % b)
  237. count += 1
  238. if count % 16 == 0:
  239. result_file.write(b'\n ')
  240. if count % 16:
  241. result_file.write(b'\n')
  242. result_file.write(b'};\n')