signature.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. #!/usr/bin/env python3
  2. #
  3. # signature.py
  4. #
  5. import schema, subprocess, re, json, hashlib
  6. from datetime import datetime
  7. from pathlib import Path
  8. from functools import reduce
  9. def enabled_defines(filepath):
  10. '''
  11. Return all enabled #define items from a given C header file in a dictionary.
  12. A "#define" in a multi-line comment could produce a false positive if it's not
  13. preceded by a non-space character (like * in a multi-line comment).
  14. Output:
  15. Each entry is a dictionary with a 'name' and a 'section' key. We end up with:
  16. { MOTHERBOARD: { name: "MOTHERBOARD", section: "hardware" }, ... }
  17. TODO: Drop the 'name' key as redundant. For now it's useful for debugging.
  18. This list is only used to filter config-defined options from those defined elsewhere.
  19. Because the option names are the keys, only the last occurrence is retained.
  20. This means the actual used value might not be reflected by this function.
  21. The Schema class does more complete parsing for a more accurate list of options.
  22. While the Schema class parses the configurations on its own, this script will
  23. get the preprocessor output and get the intersection of the enabled options from
  24. our crude scraping method and the actual compiler output.
  25. We end up with the actual configured state,
  26. better than what the config files say. You can then use the
  27. resulting config.ini to produce more exact configuration files.
  28. '''
  29. outdict = {}
  30. section = "user"
  31. spatt = re.compile(r".*@section +([-a-zA-Z0-9_\s]+)$") # @section ...
  32. f = open(filepath, encoding="utf8").read().split("\n")
  33. incomment = False
  34. for line in f:
  35. sline = line.strip()
  36. m = re.match(spatt, sline) # @section ...
  37. if m: section = m.group(1).strip() ; continue
  38. if incomment:
  39. if '*/' in sline:
  40. incomment = False
  41. continue
  42. else:
  43. mpos, spos = sline.find('/*'), sline.find('//')
  44. if mpos >= 0 and (spos < 0 or spos > mpos):
  45. incomment = True
  46. continue
  47. if sline[:7] == "#define":
  48. # Extract the key here (we don't care about the value)
  49. kv = sline[8:].strip().split()
  50. outdict[kv[0]] = { 'name':kv[0], 'section': section }
  51. return outdict
  52. # Compute the SHA256 hash of a file
  53. def get_file_sha256sum(filepath):
  54. sha256_hash = hashlib.sha256()
  55. with open(filepath,"rb") as f:
  56. # Read and update hash string value in blocks of 4K
  57. for byte_block in iter(lambda: f.read(4096),b""):
  58. sha256_hash.update(byte_block)
  59. return sha256_hash.hexdigest()
  60. #
  61. # Compress a JSON file into a zip file
  62. #
  63. import zipfile
  64. def compress_file(filepath, storedname, outpath):
  65. with zipfile.ZipFile(outpath, 'w', compression=zipfile.ZIP_DEFLATED, allowZip64=False, compresslevel=9) as zipf:
  66. zipf.write(filepath, arcname=storedname)
  67. ignore = ('CONFIGURATION_H_VERSION', 'CONFIGURATION_ADV_H_VERSION', 'CONFIG_EXAMPLES_DIR', 'CONFIG_EXPORT')
  68. #
  69. # Compute a build signature and/or export the configuration
  70. #
  71. def compute_build_signature(env):
  72. '''
  73. Compute the build signature by extracting all configuration settings and
  74. building a unique reversible signature that can be included in the binary.
  75. The signature can be reversed to get a 1:1 equivalent configuration file.
  76. Used by common-dependencies.py after filtering build files by feature.
  77. '''
  78. if 'BUILD_SIGNATURE' in env: return
  79. env.Append(BUILD_SIGNATURE=1)
  80. build_path = Path(env['PROJECT_BUILD_DIR'], env['PIOENV'])
  81. json_name = 'marlin_config.json'
  82. marlin_json = build_path / json_name
  83. marlin_zip = build_path / 'mc.zip'
  84. # ANSI colors
  85. green = "\u001b[32m"
  86. yellow = "\u001b[33m"
  87. red = "\u001b[31m"
  88. # Definitions from these files will be kept
  89. header_paths = ('Marlin/Configuration.h', 'Marlin/Configuration_adv.h')
  90. # Check if we can skip processing
  91. hashes = ''
  92. for header in header_paths:
  93. hashes += get_file_sha256sum(header)[0:10]
  94. # Read a previously exported JSON file
  95. # Same configuration, skip recomputing the build signature
  96. same_hash = False
  97. try:
  98. with marlin_json.open() as infile:
  99. conf = json.load(infile)
  100. same_hash = conf['__INITIAL_HASH'] == hashes
  101. if same_hash:
  102. compress_file(marlin_json, json_name, marlin_zip)
  103. except:
  104. pass
  105. # Extract "enabled" #define lines by scraping the configuration files.
  106. # This data also contains the @section for each option.
  107. conf_defines = {}
  108. conf_names = []
  109. for hpath in header_paths:
  110. # Get defines in the form of { name: { name:..., section:... }, ... }
  111. defines = enabled_defines(hpath)
  112. # Get all unique define names into a flat array
  113. conf_names += defines.keys()
  114. # Remember which file these defines came from
  115. conf_defines[hpath.split('/')[-1]] = defines
  116. # Get enabled config options based on running GCC to preprocess the config files.
  117. # The result is a list of line strings, each starting with '#define'.
  118. from preprocessor import run_preprocessor
  119. build_output = run_preprocessor(env)
  120. # Dumb regex to filter out some dumb macros
  121. r = re.compile(r"\(+(\s*-*\s*_.*)\)+")
  122. # Extract all the #define lines in the build output as key/value pairs
  123. build_defines = {}
  124. for line in build_output:
  125. # Split the define from the value.
  126. key_val = line[8:].strip().decode().split(' ')
  127. key, value = key_val[0], ' '.join(key_val[1:])
  128. # Ignore values starting with two underscore, since it's low level
  129. if len(key) > 2 and key[0:2] == "__": continue
  130. # Ignore values containing parentheses (likely a function macro)
  131. if '(' in key and ')' in key: continue
  132. # Then filter dumb values
  133. if r.match(value): continue
  134. build_defines[key] = value if len(value) else ""
  135. #
  136. # Continue to gather data for CONFIGURATION_EMBEDDING or CONFIG_EXPORT
  137. #
  138. is_embed = 'CONFIGURATION_EMBEDDING' in build_defines
  139. if not (is_embed or 'CONFIG_EXPORT' in build_defines):
  140. return
  141. # Filter out useless macros from the output
  142. cleaned_build_defines = {}
  143. for key in build_defines:
  144. # Remove all boards now
  145. if key.startswith("BOARD_") and key != "BOARD_INFO_NAME": continue
  146. # Remove all keys ending by "_T_DECLARED" as it's a copy of extraneous system stuff
  147. if key.endswith("_T_DECLARED"): continue
  148. # Remove keys that are not in the #define list in the Configuration list
  149. if key not in conf_names + [ 'DETAILED_BUILD_VERSION', 'STRING_DISTRIBUTION_DATE' ]: continue
  150. # Add to a new dictionary for simplicity
  151. cleaned_build_defines[key] = build_defines[key]
  152. # And we only care about defines that (most likely) came from the config files
  153. # Build a dictionary of dictionaries with keys: 'name', 'section', 'value'
  154. # { 'file1': { 'option': { 'name':'option', 'section':..., 'value':... }, ... }, 'file2': { ... } }
  155. real_config = {}
  156. for header in conf_defines:
  157. real_config[header] = {}
  158. for key in cleaned_build_defines:
  159. if key in conf_defines[header]:
  160. if key[0:2] == '__': continue
  161. val = cleaned_build_defines[key]
  162. real_config[header][key] = { 'file':header, 'name': key, 'value': val, 'section': conf_defines[header][key]['section']}
  163. def tryint(key):
  164. try: return int(build_defines[key])
  165. except: return 0
  166. # Get the CONFIG_EXPORT value and do an extended dump if > 100
  167. # For example, CONFIG_EXPORT 102 will make a 'config.ini' with a [config:] group for each schema @section
  168. config_dump = tryint('CONFIG_EXPORT')
  169. extended_dump = config_dump > 100
  170. if extended_dump: config_dump -= 100
  171. # Get the schema class for exports that require it
  172. if config_dump in (3, 4) or (extended_dump and config_dump in (2, 5)):
  173. try:
  174. conf_schema = schema.extract()
  175. except Exception as exc:
  176. print(red + "Error: " + str(exc))
  177. conf_schema = None
  178. optorder = ('MOTHERBOARD','SERIAL_PORT','BAUDRATE','USE_WATCHDOG','THERMAL_PROTECTION_HOTENDS','THERMAL_PROTECTION_HYSTERESIS','THERMAL_PROTECTION_PERIOD','BUFSIZE','BLOCK_BUFFER_SIZE','MAX_CMD_SIZE','EXTRUDERS','TEMP_SENSOR_0','TEMP_HYSTERESIS','HEATER_0_MINTEMP','HEATER_0_MAXTEMP','PREHEAT_1_TEMP_HOTEND','BANG_MAX','PIDTEMP','PID_K1','PID_MAX','PID_FUNCTIONAL_RANGE','DEFAULT_KP','DEFAULT_KI','DEFAULT_KD','X_DRIVER_TYPE','Y_DRIVER_TYPE','Z_DRIVER_TYPE','E0_DRIVER_TYPE','X_BED_SIZE','X_MIN_POS','X_MAX_POS','Y_BED_SIZE','Y_MIN_POS','Y_MAX_POS','Z_MIN_POS','Z_MAX_POS','X_HOME_DIR','Y_HOME_DIR','Z_HOME_DIR','X_MIN_ENDSTOP_HIT_STATE','Y_MIN_ENDSTOP_HIT_STATE','Z_MIN_ENDSTOP_HIT_STATE','DEFAULT_AXIS_STEPS_PER_UNIT','AXIS_RELATIVE_MODES','DEFAULT_MAX_FEEDRATE','DEFAULT_MAX_ACCELERATION','HOMING_FEEDRATE_MM_M','HOMING_BUMP_DIVISOR','X_ENABLE_ON','Y_ENABLE_ON','Z_ENABLE_ON','E_ENABLE_ON','INVERT_X_DIR','INVERT_Y_DIR','INVERT_Z_DIR','INVERT_E0_DIR','STEP_STATE_E','STEP_STATE_X','STEP_STATE_Y','STEP_STATE_Z','DISABLE_X','DISABLE_Y','DISABLE_Z','DISABLE_E','PROPORTIONAL_FONT_RATIO','DEFAULT_NOMINAL_FILAMENT_DIA','JUNCTION_DEVIATION_MM','DEFAULT_ACCELERATION','DEFAULT_TRAVEL_ACCELERATION','DEFAULT_RETRACT_ACCELERATION','DEFAULT_MINIMUMFEEDRATE','DEFAULT_MINTRAVELFEEDRATE','MINIMUM_PLANNER_SPEED','MIN_STEPS_PER_SEGMENT','DEFAULT_MINSEGMENTTIME','BED_OVERSHOOT','BUSY_WHILE_HEATING','DEFAULT_EJERK','DEFAULT_KEEPALIVE_INTERVAL','DEFAULT_LEVELING_FADE_HEIGHT','DISABLE_OTHER_EXTRUDERS','DISPLAY_CHARSET_HD44780','EEPROM_BOOT_SILENT','EEPROM_CHITCHAT','ENDSTOPPULLUPS','EXTRUDE_MAXLENGTH','EXTRUDE_MINTEMP','HOST_KEEPALIVE_FEATURE','HOTEND_OVERSHOOT','JD_HANDLE_SMALL_SEGMENTS','LCD_INFO_SCREEN_STYLE','LCD_LANGUAGE','MAX_BED_POWER','MESH_INSET','MIN_SOFTWARE_ENDSTOPS','MAX_SOFTWARE_ENDSTOPS','MIN_SOFTWARE_ENDSTOP_X','MIN_SOFTWARE_ENDSTOP_Y','MIN_SOFTWARE_ENDSTOP_Z','MAX_SOFTWARE_ENDSTOP_X','MAX_SOFTWARE_ENDSTOP_Y','MAX_SOFTWARE_ENDSTOP_Z','PREHEAT_1_FAN_SPEED','PREHEAT_1_LABEL','PREHEAT_1_TEMP_BED','PREVENT_COLD_EXTRUSION','PREVENT_LENGTHY_EXTRUDE','PRINTJOB_TIMER_AUTOSTART','PROBING_MARGIN','SHOW_BOOTSCREEN','SOFT_PWM_SCALE','STRING_CONFIG_H_AUTHOR','TEMP_BED_HYSTERESIS','TEMP_BED_RESIDENCY_TIME','TEMP_BED_WINDOW','TEMP_RESIDENCY_TIME','TEMP_WINDOW','VALIDATE_HOMING_ENDSTOPS','XY_PROBE_FEEDRATE','Z_CLEARANCE_BETWEEN_PROBES','Z_CLEARANCE_DEPLOY_PROBE','Z_CLEARANCE_MULTI_PROBE','ARC_SUPPORT','AUTO_REPORT_TEMPERATURES','AUTOTEMP','AUTOTEMP_OLDWEIGHT','BED_CHECK_INTERVAL','DEFAULT_STEPPER_TIMEOUT_SEC','DEFAULT_VOLUMETRIC_EXTRUDER_LIMIT','DISABLE_IDLE_X','DISABLE_IDLE_Y','DISABLE_IDLE_Z','DISABLE_IDLE_E','E0_AUTO_FAN_PIN','ENCODER_100X_STEPS_PER_SEC','ENCODER_10X_STEPS_PER_SEC','ENCODER_RATE_MULTIPLIER','EXTENDED_CAPABILITIES_REPORT','EXTRUDER_AUTO_FAN_SPEED','EXTRUDER_AUTO_FAN_TEMPERATURE','FANMUX0_PIN','FANMUX1_PIN','FANMUX2_PIN','FASTER_GCODE_PARSER','HOMING_BUMP_MM','MAX_ARC_SEGMENT_MM','MIN_ARC_SEGMENT_MM','MIN_CIRCLE_SEGMENTS','N_ARC_CORRECTION','SERIAL_OVERRUN_PROTECTION','SLOWDOWN','SLOWDOWN_DIVISOR','TEMP_SENSOR_BED','THERMAL_PROTECTION_BED_HYSTERESIS','THERMOCOUPLE_MAX_ERRORS','TX_BUFFER_SIZE','WATCH_BED_TEMP_INCREASE','WATCH_BED_TEMP_PERIOD','WATCH_TEMP_INCREASE','WATCH_TEMP_PERIOD')
  179. def optsort(x, optorder):
  180. return optorder.index(x) if x in optorder else float('inf')
  181. #
  182. # CONFIG_EXPORT 102 = config.ini, 105 = Config.h
  183. # Get sections using the schema class
  184. #
  185. if extended_dump and config_dump in (2, 5):
  186. if not conf_schema: exit(1)
  187. # Start with a preferred @section ordering
  188. preorder = ('test','custom','info','machine','eeprom','stepper drivers','multi stepper','idex','extruder','geometry','homing','kinematics','motion','motion control','endstops','filament runout sensors','probe type','probes','bltouch','leveling','temperature','hotend temp','mpctemp','pid temp','mpc temp','bed temp','chamber temp','fans','tool change','advanced pause','calibrate','calibration','media','lcd','lights','caselight','interface','custom main menu','custom config menu','custom buttons','develop','debug matrix','delta','scara','tpara','polar','polargraph','cnc','nozzle park','nozzle clean','gcode','serial','host','filament width','i2c encoders','i2cbus','joystick','multi-material','nanodlp','network','photo','power','psu control','reporting','safety','security','servos','stats','tmc/config','tmc/hybrid','tmc/serial','tmc/smart','tmc/spi','tmc/stallguard','tmc/status','tmc/stealthchop','tmc/tmc26x','units','volumetrics','extras')
  189. sections = { key:{} for key in preorder }
  190. # Group options by schema @section
  191. for header in real_config:
  192. for name in real_config[header]:
  193. #print(f" name: {name}")
  194. if name in ignore: continue
  195. ddict = real_config[header][name]
  196. #print(f" real_config[{header}][{name}]:", ddict)
  197. sect = ddict['section']
  198. if sect not in sections: sections[sect] = {}
  199. sections[sect][name] = ddict
  200. #
  201. # CONFIG_EXPORT 2 or 102 = config.ini
  202. #
  203. if config_dump == 2:
  204. print(yellow + "Generating config.ini ...")
  205. ini_fmt = '{0:40} = {1}'
  206. ext_fmt = '{0:40} {1}'
  207. if extended_dump:
  208. # Extended export will dump config options by section
  209. # We'll use Schema class to get the sections
  210. if not conf_schema: exit(1)
  211. # Then group options by schema @section
  212. sections = {}
  213. for header in real_config:
  214. for name in real_config[header]:
  215. #print(f" name: {name}")
  216. if name not in ignore:
  217. ddict = real_config[header][name]
  218. #print(f" real_config[{header}][{name}]:", ddict)
  219. sect = ddict['section']
  220. if sect not in sections: sections[sect] = {}
  221. sections[sect][name] = ddict
  222. # Get all sections as a list of strings, with spaces and dashes replaced by underscores
  223. long_list = [ re.sub(r'[- ]+', '_', x).lower() for x in sections.keys() ]
  224. # Make comma-separated lists of sections with 64 characters or less
  225. sec_lines = []
  226. while len(long_list):
  227. line = long_list.pop(0) + ', '
  228. while len(long_list) and len(line) + len(long_list[0]) < 64 - 1:
  229. line += long_list.pop(0) + ', '
  230. sec_lines.append(line.strip())
  231. sec_lines[-1] = sec_lines[-1][:-1] # Remove the last comma
  232. else:
  233. sec_lines = ['all']
  234. # Build the ini_use_config item
  235. sec_list = ini_fmt.format('ini_use_config', sec_lines[0])
  236. for line in sec_lines[1:]: sec_list += '\n' + ext_fmt.format('', line)
  237. config_ini = build_path / 'config.ini'
  238. with config_ini.open('w') as outfile:
  239. filegrp = { 'Configuration.h':'config:basic', 'Configuration_adv.h':'config:advanced' }
  240. vers = build_defines["CONFIGURATION_H_VERSION"]
  241. dt_string = datetime.now().strftime("%Y-%m-%d at %H:%M:%S")
  242. outfile.write(
  243. f'''#
  244. # Marlin Firmware
  245. # config.ini - Options to apply before the build
  246. #
  247. # Generated by Marlin build on {dt_string}
  248. #
  249. [config:base]
  250. #
  251. # ini_use_config - A comma-separated list of actions to apply to the Configuration files.
  252. # The actions will be applied in the listed order.
  253. # - none
  254. # Ignore this file and don't apply any configuration options
  255. #
  256. # - base
  257. # Just apply the options in config:base to the configuration
  258. #
  259. # - minimal
  260. # Just apply the options in config:minimal to the configuration
  261. #
  262. # - all
  263. # Apply all 'config:*' sections in this file to the configuration
  264. #
  265. # - another.ini
  266. # Load another INI file with a path relative to this config.ini file (i.e., within Marlin/)
  267. #
  268. # - https://me.myserver.com/path/to/configs
  269. # Fetch configurations from any URL.
  270. #
  271. # - example/Creality/Ender-5 Plus @ bugfix-2.1.x
  272. # Fetch example configuration files from the MarlinFirmware/Configurations repository
  273. # https://raw.githubusercontent.com/MarlinFirmware/Configurations/bugfix-2.1.x/config/examples/Creality/Ender-5%20Plus/
  274. #
  275. # - example/default @ release-2.0.9.7
  276. # Fetch default configuration files from the MarlinFirmware/Configurations repository
  277. # https://raw.githubusercontent.com/MarlinFirmware/Configurations/release-2.0.9.7/config/default/
  278. #
  279. # - [disable]
  280. # Comment out all #defines in both Configuration.h and Configuration_adv.h. This is useful
  281. # to start with a clean slate before applying any config: options, so only the options explicitly
  282. # set in config.ini will be enabled in the configuration.
  283. #
  284. # - [flatten] (Not yet implemented)
  285. # Produce a flattened set of Configuration.h and Configuration_adv.h files with only the enabled
  286. # #defines and no comments. A clean look, but context-free.
  287. #
  288. {sec_list}
  289. {ini_fmt.format('ini_config_vers', vers)}
  290. ''' )
  291. if extended_dump:
  292. # Loop through the sections
  293. for skey in sorted(sections):
  294. #print(f" skey: {skey}")
  295. sani = re.sub(r'[- ]+', '_', skey).lower()
  296. outfile.write(f"\n[config:{sani}]\n")
  297. opts = sections[skey]
  298. opts_keys = sorted(opts.keys(), key=lambda x: optsort(x, optorder))
  299. for name in opts_keys:
  300. if name in ignore: continue
  301. val = opts[name]['value']
  302. if val == '': val = 'on'
  303. #print(f" {name} = {val}")
  304. outfile.write(ini_fmt.format(name.lower(), val) + '\n')
  305. else:
  306. # Standard export just dumps config:basic and config:advanced sections
  307. for header in real_config:
  308. outfile.write(f'\n[{filegrp[header]}]\n')
  309. opts = real_config[header]
  310. opts_keys = sorted(opts.keys(), key=lambda x: optsort(x, optorder))
  311. for name in opts_keys:
  312. if name in ignore: continue
  313. val = opts[name]['value']
  314. if val == '': val = 'on'
  315. outfile.write(ini_fmt.format(name.lower(), val) + '\n')
  316. #
  317. # CONFIG_EXPORT 5 or 105 = Config.h
  318. #
  319. if config_dump == 5:
  320. print(yellow + "Generating Config-export.h ...")
  321. config_h = Path('Marlin', 'Config-export.h')
  322. with config_h.open('w') as outfile:
  323. filegrp = { 'Configuration.h':'config:basic', 'Configuration_adv.h':'config:advanced' }
  324. vers = build_defines["CONFIGURATION_H_VERSION"]
  325. dt_string = datetime.utcnow().strftime("%Y-%m-%d at %H:%M:%S")
  326. out_text = f'''/**
  327. * Config.h - Marlin Firmware distilled configuration
  328. * Usage: Place this file in the 'Marlin' folder with the name 'Config.h'.
  329. *
  330. * Exported by Marlin build on {dt_string}.
  331. */
  332. '''
  333. subs = (('Bltouch','BLTouch'),('hchop','hChop'),('Eeprom','EEPROM'),('Gcode','G-code'),('lguard','lGuard'),('Idex','IDEX'),('Lcd','LCD'),('Mpc','MPC'),('Pid','PID'),('Psu','PSU'),('Scara','SCARA'),('Spi','SPI'),('Tmc','TMC'),('Tpara','TPARA'))
  334. define_fmt = '#define {0:40} {1}'
  335. if extended_dump:
  336. # Loop through the sections
  337. for skey in sections:
  338. #print(f" skey: {skey}")
  339. opts = sections[skey]
  340. headed = False
  341. opts_keys = sorted(opts.keys(), key=lambda x: optsort(x, optorder))
  342. for name in opts_keys:
  343. if name in ignore: continue
  344. val = opts[name]['value']
  345. if not headed:
  346. head = reduce(lambda s, r: s.replace(*r), subs, skey.title())
  347. out_text += f"\n//\n// {head}\n//\n"
  348. headed = True
  349. out_text += define_fmt.format(name, val).strip() + '\n'
  350. else:
  351. # Dump config options in just two sections, by file
  352. for header in real_config:
  353. out_text += f'\n/**\n * Overrides for {header}\n */\n'
  354. opts = real_config[header]
  355. opts_keys = sorted(opts.keys(), key=lambda x: optsort(x, optorder))
  356. for name in opts_keys:
  357. if name in ignore: continue
  358. val = opts[name]['value']
  359. out_text += define_fmt.format(name, val).strip() + '\n'
  360. outfile.write(out_text)
  361. #
  362. # CONFIG_EXPORT 3 = schema.json, 13 = schema_grouped.json, 4 = schema.yml
  363. #
  364. if config_dump in (3, 4, 13):
  365. if conf_schema:
  366. #
  367. # 3 = schema.json
  368. #
  369. if config_dump in (3, 13):
  370. print(yellow + "Generating schema.json ...")
  371. schema.dump_json(conf_schema, build_path / 'schema.json')
  372. if config_dump == 13:
  373. schema.group_options(conf_schema)
  374. schema.dump_json(conf_schema, build_path / 'schema_grouped.json')
  375. #
  376. # 4 = schema.yml
  377. #
  378. elif config_dump == 4:
  379. print(yellow + "Generating schema.yml ...")
  380. try:
  381. import yaml
  382. except ImportError:
  383. env.Execute(env.VerboseAction(
  384. '$PYTHONEXE -m pip install "pyyaml"',
  385. "Installing YAML for schema.yml export",
  386. ))
  387. import yaml
  388. schema.dump_yaml(conf_schema, build_path / 'schema.yml')
  389. #
  390. # Produce a JSON file for CONFIGURATION_EMBEDDING or CONFIG_EXPORT == 1 or 101
  391. # Skip if an identical JSON file was already present.
  392. #
  393. if not same_hash and (config_dump == 1 or is_embed):
  394. with marlin_json.open('w') as outfile:
  395. json_data = {}
  396. if extended_dump:
  397. print(yellow + "Extended dump ...")
  398. for header in real_config:
  399. confs = real_config[header]
  400. json_data[header] = {}
  401. for name in confs:
  402. if name in ignore: continue
  403. c = confs[name]
  404. s = c['section']
  405. if s not in json_data[header]: json_data[header][s] = {}
  406. json_data[header][s][name] = c['value']
  407. else:
  408. for header in real_config:
  409. json_data[header] = {}
  410. conf = real_config[header]
  411. #print(f"real_config[{header}]", conf)
  412. for name in conf:
  413. if name in ignore: continue
  414. json_data[header][name] = conf[name]['value']
  415. json_data['__INITIAL_HASH'] = hashes
  416. # Append the source code version and date
  417. json_data['VERSION'] = {
  418. 'DETAILED_BUILD_VERSION': cleaned_build_defines['DETAILED_BUILD_VERSION'],
  419. 'STRING_DISTRIBUTION_DATE': cleaned_build_defines['STRING_DISTRIBUTION_DATE']
  420. }
  421. try:
  422. curver = subprocess.check_output(["git", "describe", "--match=NeVeRmAtCh", "--always"]).strip()
  423. json_data['VERSION']['GIT_REF'] = curver.decode()
  424. except:
  425. pass
  426. json.dump(json_data, outfile, separators=(',', ':'))
  427. #
  428. # The rest only applies to CONFIGURATION_EMBEDDING
  429. #
  430. if not is_embed:
  431. (build_path / 'mc.zip').unlink(missing_ok=True)
  432. return
  433. # Compress the JSON file as much as we can
  434. if not same_hash:
  435. compress_file(marlin_json, json_name, marlin_zip)
  436. # Generate a C source file containing the entire ZIP file as an array
  437. with open('Marlin/src/mczip.h','wb') as result_file:
  438. result_file.write(
  439. b'#ifndef NO_CONFIGURATION_EMBEDDING_WARNING\n'
  440. + b' #warning "Generated file \'mc.zip\' is embedded (Define NO_CONFIGURATION_EMBEDDING_WARNING to suppress this warning.)"\n'
  441. + b'#endif\n'
  442. + b'const unsigned char mc_zip[] PROGMEM = {\n '
  443. )
  444. count = 0
  445. for b in (build_path / 'mc.zip').open('rb').read():
  446. result_file.write(b' 0x%02X,' % b)
  447. count += 1
  448. if count % 16 == 0: result_file.write(b'\n ')
  449. if count % 16: result_file.write(b'\n')
  450. result_file.write(b'};\n')
  451. if __name__ == "__main__":
  452. # Build required. From command line just explain usage.
  453. print("*** THIS SCRIPT USED BY common-dependencies.py ***\n\n"
  454. + "Current options for config and schema export:\n"
  455. + " - marlin_config.json : Build Marlin with CONFIG_EXPORT 1 or 101. (Use CONFIGURATION_EMBEDDING for 'mc.zip')\n"
  456. + " - config.ini : Build Marlin with CONFIG_EXPORT 2 or 102.\n"
  457. + " - schema.json : Run 'schema.py json' (CONFIG_EXPORT 3).\n"
  458. + " - schema_grouped.json : Run 'schema.py group' (CONFIG_EXPORT 13).\n"
  459. + " - schema.yml : Run 'schema.py yml' (CONFIG_EXPORT 4).\n"
  460. + " - Config-export.h : Build Marlin with CONFIG_EXPORT 5 or 105.\n"
  461. )