You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
276 lines
9.9 KiB
276 lines
9.9 KiB
# |
|
# signature.py |
|
# |
|
import schema |
|
|
|
import subprocess,re,json,hashlib |
|
from datetime import datetime |
|
from pathlib import Path |
|
|
|
# |
|
# Return all macro names in a header as an array, so we can take |
|
# the intersection with the preprocessor output, giving a decent |
|
# reflection of all enabled options that (probably) came from the |
|
# configuration files. We end up with the actual configured state, |
|
# better than what the config files say. You can then use the |
|
# resulting config.ini to produce more exact configuration files. |
|
# |
|
def extract_defines(filepath): |
|
f = open(filepath, encoding="utf8").read().split("\n") |
|
a = [] |
|
for line in f: |
|
sline = line.strip() |
|
if sline[:7] == "#define": |
|
# Extract the key here (we don't care about the value) |
|
kv = sline[8:].strip().split() |
|
a.append(kv[0]) |
|
return a |
|
|
|
# Compute the SHA256 hash of a file |
|
def get_file_sha256sum(filepath): |
|
sha256_hash = hashlib.sha256() |
|
with open(filepath,"rb") as f: |
|
# Read and update hash string value in blocks of 4K |
|
for byte_block in iter(lambda: f.read(4096),b""): |
|
sha256_hash.update(byte_block) |
|
return sha256_hash.hexdigest() |
|
|
|
# |
|
# Compress a JSON file into a zip file |
|
# |
|
import zipfile |
|
def compress_file(filepath, storedname, outpath): |
|
with zipfile.ZipFile(outpath, 'w', compression=zipfile.ZIP_BZIP2, compresslevel=9) as zipf: |
|
zipf.write(filepath, arcname=storedname, compress_type=zipfile.ZIP_BZIP2, compresslevel=9) |
|
|
|
# |
|
# Compute the build signature. The idea is to extract all defines in the configuration headers |
|
# to build a unique reversible signature from this build so it can be included in the binary |
|
# We can reverse the signature to get a 1:1 equivalent configuration file |
|
# |
|
def compute_build_signature(env): |
|
if 'BUILD_SIGNATURE' in env: |
|
return |
|
|
|
# Definitions from these files will be kept |
|
files_to_keep = [ 'Marlin/Configuration.h', 'Marlin/Configuration_adv.h' ] |
|
|
|
build_path = Path(env['PROJECT_BUILD_DIR'], env['PIOENV']) |
|
|
|
# Check if we can skip processing |
|
hashes = '' |
|
for header in files_to_keep: |
|
hashes += get_file_sha256sum(header)[0:10] |
|
|
|
marlin_json = build_path / 'marlin_config.json' |
|
marlin_zip = build_path / 'mc.zip' |
|
|
|
# Read existing config file |
|
try: |
|
with marlin_json.open() as infile: |
|
conf = json.load(infile) |
|
if conf['__INITIAL_HASH'] == hashes: |
|
# Same configuration, skip recomputing the building signature |
|
compress_file(marlin_json, 'marlin_config.json', marlin_zip) |
|
return |
|
except: |
|
pass |
|
|
|
# Get enabled config options based on preprocessor |
|
from preprocessor import run_preprocessor |
|
complete_cfg = run_preprocessor(env) |
|
|
|
# Dumb #define extraction from the configuration files |
|
conf_defines = {} |
|
all_defines = [] |
|
for header in files_to_keep: |
|
defines = extract_defines(header) |
|
# To filter only the define we want |
|
all_defines += defines |
|
# To remember from which file it cames from |
|
conf_defines[header.split('/')[-1]] = defines |
|
|
|
r = re.compile(r"\(+(\s*-*\s*_.*)\)+") |
|
|
|
# First step is to collect all valid macros |
|
defines = {} |
|
for line in complete_cfg: |
|
|
|
# Split the define from the value |
|
key_val = line[8:].strip().decode().split(' ') |
|
key, value = key_val[0], ' '.join(key_val[1:]) |
|
|
|
# Ignore values starting with two underscore, since it's low level |
|
if len(key) > 2 and key[0:2] == "__" : |
|
continue |
|
# Ignore values containing a parenthesis (likely a function macro) |
|
if '(' in key and ')' in key: |
|
continue |
|
|
|
# Then filter dumb values |
|
if r.match(value): |
|
continue |
|
|
|
defines[key] = value if len(value) else "" |
|
|
|
# |
|
# Continue to gather data for CONFIGURATION_EMBEDDING or CONFIG_EXPORT |
|
# |
|
if not ('CONFIGURATION_EMBEDDING' in defines or 'CONFIG_EXPORT' in defines): |
|
return |
|
|
|
# Second step is to filter useless macro |
|
resolved_defines = {} |
|
for key in defines: |
|
# Remove all boards now |
|
if key.startswith("BOARD_") and key != "BOARD_INFO_NAME": |
|
continue |
|
# Remove all keys ending by "_NAME" as it does not make a difference to the configuration |
|
if key.endswith("_NAME") and key != "CUSTOM_MACHINE_NAME": |
|
continue |
|
# Remove all keys ending by "_T_DECLARED" as it's a copy of extraneous system stuff |
|
if key.endswith("_T_DECLARED"): |
|
continue |
|
# Remove keys that are not in the #define list in the Configuration list |
|
if key not in all_defines + [ 'DETAILED_BUILD_VERSION', 'STRING_DISTRIBUTION_DATE' ]: |
|
continue |
|
|
|
# Don't be that smart guy here |
|
resolved_defines[key] = defines[key] |
|
|
|
# Generate a build signature now |
|
# We are making an object that's a bit more complex than a basic dictionary here |
|
data = {} |
|
data['__INITIAL_HASH'] = hashes |
|
# First create a key for each header here |
|
for header in conf_defines: |
|
data[header] = {} |
|
|
|
# Then populate the object where each key is going to (that's a O(N^2) algorithm here...) |
|
for key in resolved_defines: |
|
for header in conf_defines: |
|
if key in conf_defines[header]: |
|
data[header][key] = resolved_defines[key] |
|
|
|
# Every python needs this toy |
|
def tryint(key): |
|
try: |
|
return int(defines[key]) |
|
except: |
|
return 0 |
|
|
|
config_dump = tryint('CONFIG_EXPORT') |
|
|
|
# |
|
# Produce an INI file if CONFIG_EXPORT == 2 |
|
# |
|
if config_dump == 2: |
|
print("Generating config.ini ...") |
|
config_ini = build_path / 'config.ini' |
|
with config_ini.open('w') as outfile: |
|
ignore = ('CONFIGURATION_H_VERSION', 'CONFIGURATION_ADV_H_VERSION', 'CONFIG_EXPORT') |
|
filegrp = { 'Configuration.h':'config:basic', 'Configuration_adv.h':'config:advanced' } |
|
vers = defines["CONFIGURATION_H_VERSION"] |
|
dt_string = datetime.now().strftime("%Y-%m-%d at %H:%M:%S") |
|
ini_fmt = '{0:40}{1}\n' |
|
outfile.write( |
|
'#\n' |
|
+ '# Marlin Firmware\n' |
|
+ '# config.ini - Options to apply before the build\n' |
|
+ '#\n' |
|
+ f'# Generated by Marlin build on {dt_string}\n' |
|
+ '#\n' |
|
+ '\n' |
|
+ '[config:base]\n' |
|
+ ini_fmt.format('ini_use_config', ' = all') |
|
+ ini_fmt.format('ini_config_vers', f' = {vers}') |
|
) |
|
# Loop through the data array of arrays |
|
for header in data: |
|
if header.startswith('__'): |
|
continue |
|
outfile.write('\n[' + filegrp[header] + ']\n') |
|
for key in sorted(data[header]): |
|
if key not in ignore: |
|
val = 'on' if data[header][key] == '' else data[header][key] |
|
outfile.write(ini_fmt.format(key.lower(), ' = ' + val)) |
|
|
|
# |
|
# Produce a schema.json file if CONFIG_EXPORT == 3 |
|
# |
|
if config_dump >= 3: |
|
try: |
|
conf_schema = schema.extract() |
|
except Exception as exc: |
|
print("Error: " + str(exc)) |
|
conf_schema = None |
|
|
|
if conf_schema: |
|
# |
|
# Produce a schema.json file if CONFIG_EXPORT == 3 |
|
# |
|
if config_dump in (3, 13): |
|
print("Generating schema.json ...") |
|
schema.dump_json(conf_schema, build_path / 'schema.json') |
|
if config_dump == 13: |
|
schema.group_options(conf_schema) |
|
schema.dump_json(conf_schema, build_path / 'schema_grouped.json') |
|
|
|
# |
|
# Produce a schema.yml file if CONFIG_EXPORT == 4 |
|
# |
|
elif config_dump == 4: |
|
print("Generating schema.yml ...") |
|
try: |
|
import yaml |
|
except ImportError: |
|
env.Execute(env.VerboseAction( |
|
'$PYTHONEXE -m pip install "pyyaml"', |
|
"Installing YAML for schema.yml export", |
|
)) |
|
import yaml |
|
schema.dump_yaml(conf_schema, build_path / 'schema.yml') |
|
|
|
# Append the source code version and date |
|
data['VERSION'] = {} |
|
data['VERSION']['DETAILED_BUILD_VERSION'] = resolved_defines['DETAILED_BUILD_VERSION'] |
|
data['VERSION']['STRING_DISTRIBUTION_DATE'] = resolved_defines['STRING_DISTRIBUTION_DATE'] |
|
try: |
|
curver = subprocess.check_output(["git", "describe", "--match=NeVeRmAtCh", "--always"]).strip() |
|
data['VERSION']['GIT_REF'] = curver.decode() |
|
except: |
|
pass |
|
|
|
# |
|
# Produce a JSON file for CONFIGURATION_EMBEDDING or CONFIG_EXPORT == 1 |
|
# |
|
if config_dump == 1 or 'CONFIGURATION_EMBEDDING' in defines: |
|
with marlin_json.open('w') as outfile: |
|
json.dump(data, outfile, separators=(',', ':')) |
|
|
|
# |
|
# The rest only applies to CONFIGURATION_EMBEDDING |
|
# |
|
if not 'CONFIGURATION_EMBEDDING' in defines: |
|
return |
|
|
|
# Compress the JSON file as much as we can |
|
compress_file(marlin_json, 'marlin_config.json', marlin_zip) |
|
|
|
# Generate a C source file for storing this array |
|
with open('Marlin/src/mczip.h','wb') as result_file: |
|
result_file.write( |
|
b'#ifndef NO_CONFIGURATION_EMBEDDING_WARNING\n' |
|
+ b' #warning "Generated file \'mc.zip\' is embedded (Define NO_CONFIGURATION_EMBEDDING_WARNING to suppress this warning.)"\n' |
|
+ b'#endif\n' |
|
+ b'const unsigned char mc_zip[] PROGMEM = {\n ' |
|
) |
|
count = 0 |
|
for b in (build_path / 'mc.zip').open('rb').read(): |
|
result_file.write(b' 0x%02X,' % b) |
|
count += 1 |
|
if count % 16 == 0: |
|
result_file.write(b'\n ') |
|
if count % 16: |
|
result_file.write(b'\n') |
|
result_file.write(b'};\n')
|
|
|