mirror of
https://github.com/xoseperez/espurna.git
synced 2026-03-07 00:37:04 +01:00
should not be kept indefinitely, since sources may've been changed already ci cache dir is expected to only survive for the current build session, prefer similar behaviour for local build tests
278 lines
7.5 KiB
Python
Executable File
278 lines
7.5 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Copyright (C) 2019-2021 by Maxim Prokhorov <prokhorov dot max at outlook dot com>
|
|
#
|
|
# 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 <http://www.gnu.org/licenses/>.
|
|
|
|
import argparse
|
|
import datetime
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import subprocess
|
|
import time
|
|
|
|
from espurna_utils.display import clr, Color
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
|
log = logging.getLogger("main")
|
|
|
|
|
|
PWD = pathlib.Path(__file__).resolve().parent
|
|
|
|
ROOT_PATH = (PWD / "..").resolve()
|
|
TEST_PATH = (ROOT_PATH / "test" / "build").resolve()
|
|
|
|
CONFIG_PATH = TEST_PATH / "config"
|
|
CACHE_PATH = TEST_PATH / "cache"
|
|
BUILD_PATH = ROOT_PATH / ".pio" / "build"
|
|
|
|
CACHE_TIMEDELTA = datetime.timedelta(days=1)
|
|
TIMEDELTA_PARAM = {
|
|
"d": "days",
|
|
"h": "hours",
|
|
"m": "minutes",
|
|
"s": "seconds",
|
|
}
|
|
|
|
|
|
def bold(string: str):
|
|
return clr(Color.BOLD, string)
|
|
|
|
|
|
def format_configurations(configurations: list[pathlib.Path]):
|
|
return "\n".join(str(cfg) for cfg in configurations)
|
|
|
|
|
|
def pluralize(string: str, length: int):
|
|
if length > 1:
|
|
return f"{string}s"
|
|
|
|
return string
|
|
|
|
|
|
def make_timedelta(string: str) -> datetime.timedelta:
|
|
if not string:
|
|
return datetime.timedelta(seconds=0)
|
|
|
|
if string.isdigit():
|
|
suffix = "d"
|
|
else:
|
|
suffix = string[-1]
|
|
string = string[:-1]
|
|
|
|
param = TIMEDELTA_PARAM[suffix]
|
|
value = int(string, 10)
|
|
|
|
return datetime.timedelta(**{param: value})
|
|
|
|
|
|
def cache_cleanup(cache_path: pathlib.Path, offset: datetime.timedelta):
|
|
now = datetime.datetime.now()
|
|
|
|
for pair in cache_path.iterdir():
|
|
# {CACHE_DIR} / AA / AA...rest of the hash...
|
|
if not pair.is_dir():
|
|
continue
|
|
|
|
for f in pair.iterdir():
|
|
mtime_raw = f.stat().st_mtime
|
|
mtime_dt = datetime.datetime.fromtimestamp(mtime_raw)
|
|
|
|
if now - mtime_dt > offset:
|
|
f.unlink()
|
|
|
|
if not any(pair.iterdir()):
|
|
pair.rmdir()
|
|
|
|
|
|
def build_configurations(args: argparse.Namespace, configurations: list[pathlib.Path]):
|
|
cache_path = args.cache_path.resolve()
|
|
|
|
cache_cleanup(cache_path, args.expire_cache)
|
|
|
|
cmd = ["platformio", "run"]
|
|
if args.silent:
|
|
cmd.extend(["-s"])
|
|
cmd.extend(["-e", args.environment])
|
|
|
|
build_time = datetime.timedelta(seconds=0)
|
|
|
|
while configurations:
|
|
cfg = configurations.pop()
|
|
log.info("%s contents\n%s", bold(cfg.name), cfg.read_text())
|
|
|
|
os_env = os.environ.copy()
|
|
os_env["PLATFORMIO_BUILD_CACHE_DIR"] = cache_path.resolve().as_posix()
|
|
if args.single_source:
|
|
os_env["ESPURNA_BUILD_SINGLE_SOURCE"] = "1"
|
|
|
|
os_env["PLATFORMIO_BUILD_SRC_FLAGS"] = " ".join(
|
|
[
|
|
'-DMANUFACTURER=\\"TEST_BUILD\\"',
|
|
'-DDEVICE=\\"{}\\"'.format(cfg.stem.replace(" ", "_").upper()),
|
|
'-include "{}"'.format(cfg.resolve().as_posix()),
|
|
]
|
|
)
|
|
|
|
build_start = time.time()
|
|
try:
|
|
subprocess.check_call(cmd, env=os_env)
|
|
except subprocess.CalledProcessError:
|
|
log.error("%s failed to build", bold(str(cfg)))
|
|
if configurations:
|
|
log.info(
|
|
"%s %s left\n%s",
|
|
bold(str(len(configurations))),
|
|
pluralize("configuration", len(configurations)),
|
|
format_configurations(configurations),
|
|
)
|
|
raise
|
|
|
|
diff = datetime.timedelta(seconds=time.time() - build_start)
|
|
|
|
firmware_bin = args.build_path / args.environment / "firmware.bin"
|
|
|
|
log.info(
|
|
"%s finished in %s, %s is %s bytes",
|
|
*(
|
|
bold(str(x))
|
|
for x in (cfg, diff, firmware_bin, firmware_bin.stat().st_size)
|
|
),
|
|
)
|
|
|
|
build_time += diff
|
|
|
|
if build_time:
|
|
log.info("Done after %s", bold(str(build_time)))
|
|
|
|
|
|
def main(args: argparse.Namespace):
|
|
if not args.environment:
|
|
log.error("No environment selected")
|
|
return
|
|
|
|
log.info("Using [env:%s]", bold(args.environment))
|
|
|
|
configurations = [pathlib.Path(x) for x in args.configurations]
|
|
if not configurations:
|
|
configurations = [x for x in args.config_path.glob("*.h")]
|
|
|
|
configurations = [x.resolve() for x in configurations]
|
|
|
|
if args.start_from:
|
|
offset = 0
|
|
for n, p in enumerate(configurations, start=1):
|
|
if args.start_from in p.name:
|
|
offset = n
|
|
break
|
|
|
|
if offset:
|
|
for cfg in configurations[offset:]:
|
|
log.info("Skipping %s", cfg)
|
|
configurations = configurations[:offset]
|
|
|
|
if args.filter:
|
|
configurations = [p for p in configurations for f in args.filter if f in p.name]
|
|
|
|
if not configurations:
|
|
log.error("No configurations selected")
|
|
return
|
|
|
|
log.info(
|
|
"Found %s %s\n%s",
|
|
bold(str(len(configurations))),
|
|
pluralize("configuration", len(configurations)),
|
|
format_configurations(configurations),
|
|
)
|
|
|
|
if args.build:
|
|
build_configurations(args, configurations)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser(
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
|
)
|
|
parser.add_argument("-e", "--environment", help="PIO environment")
|
|
|
|
parser.add_argument(
|
|
"--start-from",
|
|
help="Skip configurations until this string is found in configuration filename",
|
|
default="",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--filter",
|
|
action="append",
|
|
help="Only build configurations with filenames containing this string",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--silent",
|
|
default=True,
|
|
action=argparse.BooleanOptionalAction,
|
|
help="Silence PlatformIO output",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--single-source",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=True,
|
|
help="Use SCons 'unity' build",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--build",
|
|
action=argparse.BooleanOptionalAction,
|
|
default=True,
|
|
help="Build specified configurations",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--config-path",
|
|
default=CONFIG_PATH,
|
|
type=pathlib.Path,
|
|
help="Directory with build test configuration headers",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--cache-path",
|
|
default=CACHE_PATH,
|
|
type=pathlib.Path,
|
|
help="PlatformIO SCons cache path",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--build-path",
|
|
default=BUILD_PATH,
|
|
type=pathlib.Path,
|
|
help="PlatformIO build path",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--expire-cache",
|
|
default=CACHE_TIMEDELTA,
|
|
type=make_timedelta,
|
|
help="PlatformIO cache expiration time (NUMBER or NUMBER{d,h,m,s} for days, hours, minutes or seconds respectively)}",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"configurations",
|
|
nargs="*",
|
|
default=[],
|
|
)
|
|
|
|
main(parser.parse_args())
|