#!/usr/bin/env python # # Copyright (C) 2020 by Maxim Prokhorov # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import argparse import re import shlex import configparser import collections Build = collections.namedtuple("Build", "env extends build_flags build_src_flags") def expand_variables(cfg, value): RE_VARS = re.compile(r"\$\{.*?\}") for var in RE_VARS.findall(value): section, option = var.replace("${", "").replace("}", "").split(".", 1) value = value.replace(var, expand_variables(cfg, cfg.get(section, option))) return value def get_builds(cfg): RE_NEWLINE = re.compile(r"\r\n|\n") BASE_BUILD_FLAGS = set( shlex.split(expand_variables(cfg, cfg.get("env", "build_flags"))) ) for section in cfg.sections(): if (not section.startswith("env:")) or ( section.startswith("env:esp8266-") and section.endswith("-base") ): continue build_flags = None build_src_flags = None try: build_flags = cfg.get(section, "build_flags") build_flags = RE_NEWLINE.sub(" ", build_flags).strip() build_flags = " ".join( BASE_BUILD_FLAGS ^ set(shlex.split(expand_variables(cfg, build_flags))) ) except configparser.NoOptionError: pass try: build_src_flags = cfg.get(section, "build_src_flags") build_src_flags = RE_NEWLINE.sub(" ", build_src_flags).strip() build_src_flags = expand_variables(cfg, build_src_flags) except configparser.NoOptionError: pass yield Build( section.replace("env:", ""), cfg.get(section, "extends").replace("env:", ""), build_flags, build_src_flags, ) def find_any(string, values): for value in values: if value in string: return True return False def generate_lines(builds, ignore): minimal = [] generic = [] for build in builds: if find_any(build.env, ignore): continue flags = [] if build.build_flags: flags.append(f'PLATFORMIO_BUILD_FLAGS="{build.build_flags}"') if build.build_src_flags: flags.append(f'ESPURNA_FLAGS="{build.build_src_flags}"') flags.append(f'ESPURNA_BUILD_NAME="{build.env}"') cmd = ["env"] cmd.extend(flags) cmd.extend(["pio", "run", "-e", build.extends, "-s", "-t", "build-and-copy"]) line = " ".join(cmd) # push minimal variants to the front as they definetly include global build_flags output = generic if "ESPURNA_MINIMAL" in build.build_src_flags: output = minimal output.append(line) return minimal + generic def every(seq, nth, total): index = 0 for value in seq: if index == nth: yield value index = (index + 1) % total def parse_args(): parser = argparse.ArgumentParser() parser.add_argument( "--destination", help="Where to place the resulting .bin", required=True ) parser.add_argument( "--single-source", help="Combine .cpp files into one to speed up compilation", action="store_true", default=True, ) parser.add_argument( "--ignore", help="Do not build envs that contain the string(s)", action="append" ) builder_nth = parser.add_argument_group(title="Synchronize parallel builds in CI") builder_nth.add_argument("--builder-id", type=int, required=True) builder_nth.add_argument("--builder-total", type=int, required=True) full_version = parser.add_argument_group( title="Fully replace the version string for the build system" ) full_version.add_argument("--full-version") version_parts = parser.add_argument_group( "Replace parts of the version string that would have been detected by the build system" ) version_parts.add_argument("--version") version_parts.add_argument("--revision") version_parts.add_argument("--suffix") return parser.parse_args() if __name__ == "__main__": args = parse_args() Config = configparser.ConfigParser() with open("platformio.ini", "r") as f: Config.read_file(f) builder_total = args.builder_total builder_id = args.builder_id if builder_id >= builder_total: raise ValueError(f"* {builder_id=} >= {builder_total=} *") builds = every(get_builds(Config), builder_id, builder_total) print("#!/bin/bash") print("set -e -x") variables = [ ["ESPURNA_BUILD_DESTINATION", args.destination], ["ESPURNA_BUILD_SINGLE_SOURCE", int(args.single_source)], ["ESPURNA_BUILD_FULL_VERSION", args.full_version], ["ESPURNA_BUILD_VERSION", args.version], ["ESPURNA_BUILD_REVISION", args.revision], ["ESPURNA_BUILD_VERSION_SUFFIX", args.suffix], ] for var, value in variables: if value or not value is None: print(f'export {var}="{value}"') print('trap "ls -R ${ESPURNA_BUILD_DESTINATION}" EXIT') print(f'echo "Selected build ID {builder_id + 1}/{builder_total}"') for line in generate_lines(builds, args.ignore or ()): print(line)