blob: 6621a7471d0faab3d6e2f7a4d6e130c513dce9b3 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2021 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Regenerate all project configs locally and generate a diff for them."""
import argparse
import atexit
import collections
import functools
import glob
import itertools
import json
import logging
import multiprocessing
import multiprocessing.pool
import os
import pathlib
import shutil
import subprocess
import sys
import tempfile
import time
from common import utilities
# resolve relative directories
this_dir = pathlib.Path(os.path.dirname(os.path.abspath(__file__)))
path_projects = this_dir / "../../project"
def run_config(config, logname=None):
"""Run a single config job, return diff of current and new output.
Args:
config: (program, project) to configure
logname: Filename to redirect stderr to from config
default is to suppress the output
"""
# open logfile as /dev/null by default so we don't have to check for it
logfile = open("/dev/null", "w")
if logname:
logfile = open(logname, "a")
def run_diff(cmd, current, output):
"""Execute cmd and diff the current and output files"""
stdout = ""
try:
stdout = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
check=False,
text=True,
shell=True,
).stdout
finally:
logfile.write(stdout)
# otherwise run diff
return utilities.jqdiff(
current if current.exists() else None,
output if output.exists() else None,
)
#### start of function body
program, project = config
logfile.write("== {}/{} ==================\n".format(program, project))
# path to project repo and config bundle
path_repo = (path_projects / program / project).resolve()
path_config = (path_repo / "generated/config.jsonproto").resolve()
# create temporary directory for output
diff = ""
with tempfile.TemporaryDirectory() as scratch:
scratch = pathlib.Path(scratch)
cmd = "cd {}; {} --no-proto --output-dir {} {}".format(
path_repo.resolve(),
(path_repo / "config/bin/gen_config").resolve(),
scratch,
path_repo / "config.star",
)
path_config_new = scratch / "generated/config.jsonproto"
logfile.write("repo path: {}\n".format(path_repo))
logfile.write("old config: {}\n".format(path_config))
logfile.write("new config: {}\n".format(path_config_new))
logfile.write("running: {}\n".format(cmd))
logfile.write("\n")
diff = run_diff(cmd, path_config, path_config_new)
logfile.write("\n\n")
logfile.close()
return ("{}-{}".format(program, project), diff)
def run_configs(args, configs):
"""Regenerate configuration for each project in configs.
Generate an über diff showing the changes that the current ToT
configuration code would generate vs what's currently committed.
Write the result to the output file specified on the command line.
Args:
args: command line arguments from argparse
configs: list of BackfillConfig instances to execute
Return:
nothing
"""
# create a logfile if requested
kwargs = {}
if args.logfile:
# open and close the logfile to truncate it so backfills can append
# We can't pickle the file object and send it as an argument with
# multiprocessing, so this is a workaround for that limitation
with open(args.logfile, "w"):
kwargs["logname"] = args.logfile
nproc = 32
diffs = {}
nconfig = len(configs)
with multiprocessing.Pool(processes=nproc) as pool:
results = pool.imap_unordered(
functools.partial(run_config, **kwargs), configs, chunksize=1)
for ii, result in enumerate(results, 1):
sys.stderr.write(
utilities.clear_line("[{}/{}] Processing configs".format(ii,
nconfig)))
if result:
key, diff = result
diffs[key] = diff
sys.stderr.write(utilities.clear_line("[✔] Processing configs"))
# generate final über diff showing all the changes
with open(args.diff, "w") as ofile:
for name, result in sorted(diffs.items()):
ofile.write("## ---------------------\n")
ofile.write("## diff for {}\n".format(name))
ofile.write("\n")
ofile.write(result + "\n")
def main():
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawTextHelpFormatter,
)
parser.add_argument(
"--diff",
type=str,
required=True,
help="target file for diff on config.jsonproto payload",
)
parser.add_argument(
"-l",
"--logfile",
type=str,
help="target file to log output from regens",
)
args = parser.parse_args()
# glob out all the config.stars in the project directory
strpath = str(path_projects)
config_stars = [
os.path.relpath(fname, strpath)
for fname in glob.glob(strpath + "/*/*/config.star")
]
# break program/project out of path
configs = [
tuple(config_star.split("/")[0:2],) for config_star in config_stars
]
run_configs(args, sorted(configs))
if __name__ == "__main__":
main()