#!/usr/bin/env python3 import glob import os import shutil import subprocess import sys import shlex from argparse import ArgumentParser from pathlib import Path as P PARSER = ArgumentParser() PARSER.add_argument("command", choices=["build", "test"]) PARSER.add_argument("build_dir", type=P) PARSER.add_argument("src_dir", type=P) PARSER.add_argument("root_dir", type=P) PARSER.add_argument("target", choices=["release", "debug"]) PARSER.add_argument("prefix", type=P) PARSER.add_argument("libdir", type=P) PARSER.add_argument("--version", default=None) PARSER.add_argument("--bin", default=None, type=P) PARSER.add_argument("--features", nargs="+", default=[]) PARSER.add_argument("--packages", nargs="+", default=[]) PARSER.add_argument("--examples", nargs="+", default=[]) PARSER.add_argument("--lib-suffixes", nargs="+", default=[]) PARSER.add_argument("--exe-suffix") PARSER.add_argument("--depfile") PARSER.add_argument("--disable-doc", action="store_true", default=False) def shlex_join(args): if hasattr(shlex, "join"): return shlex.join(args) return " ".join([shlex.quote(arg) for arg in args]) def generate_depfile_for(fpath): file_stem = fpath.parent / fpath.stem depfile_content = "" with open(f"{file_stem}.d", "r") as depfile: for line in depfile.readlines(): if line.startswith(str(file_stem)): # We can't blindly split on `:` because on Windows that's part # of the drive letter. Lucky for us, the format of the dep file # is one of: # # /path/to/output: /path/to/src1 /path/to/src2 # /path/to/output: # # So we parse these two cases specifically if line.endswith(":"): output = line[:-1] srcs = "" else: output, srcs = line.split(": ", maxsplit=2) all_deps = [] for src in srcs.split(" "): all_deps.append(src) src = P(src) if src.name == "lib.rs": # `rustc` doesn't take `Cargo.toml` into account # but we need to cargo_toml = src.parent.parent / "Cargo.toml" if cargo_toml.exists(): all_deps.append(str(cargo_toml)) depfile_content += f"{output}: {' '.join(all_deps)}\n" return depfile_content if __name__ == "__main__": opts = PARSER.parse_args() logdir = opts.root_dir / "meson-logs" logfile_path = logdir / f"{opts.src_dir.name}-cargo-wrapper.log" logfile = open(logfile_path, mode="w", buffering=1) print(opts, file=logfile) cargo_target_dir = opts.build_dir / "target" env = os.environ.copy() if "PKG_CONFIG_PATH" in env: pkg_config_path = env["PKG_CONFIG_PATH"].split(os.pathsep) else: pkg_config_path = [] pkg_config_path.append(str(opts.root_dir / "meson-uninstalled")) env["PKG_CONFIG_PATH"] = os.pathsep.join(pkg_config_path) if "NASM" in env: env["PATH"] = os.pathsep.join([os.path.dirname(env["NASM"]), env["PATH"]]) rustc_target = None if "RUSTC" in env: rustc_cmdline = shlex.split(env["RUSTC"]) # grab target from RUSTFLAGS rust_flags = rustc_cmdline[1:] + shlex.split(env.get("RUSTFLAGS", "")) if "--target" in rust_flags: rustc_target_idx = rust_flags.index("--target") _ = rust_flags.pop(rustc_target_idx) # drop '--target' rustc_target = rust_flags.pop(rustc_target_idx) env["RUSTFLAGS"] = shlex_join(rust_flags) env["RUSTC"] = rustc_cmdline[0] features = opts.features if opts.command == "build": cargo_cmd = ["cargo"] if opts.bin or opts.examples: cargo_cmd += ["build"] else: cargo_cmd += ["cbuild"] if not opts.disable_doc: features += ["doc"] if opts.target == "release": cargo_cmd.append("--release") elif opts.command == "test": # cargo test cargo_cmd = ["cargo", "ctest", "--no-fail-fast", "--color=always"] else: print("Unknown command:", opts.command, file=logfile) sys.exit(1) if rustc_target: cargo_cmd += ["--target", rustc_target] if features: cargo_cmd += ["--features", ",".join(features)] cargo_cmd += ["--target-dir", cargo_target_dir] cargo_cmd += ["--manifest-path", opts.src_dir / "Cargo.toml"] if opts.bin: cargo_cmd.extend(["--bin", opts.bin.name]) else: if not opts.examples: cargo_cmd.extend( ["--prefix", opts.prefix, "--libdir", opts.prefix / opts.libdir] ) for p in opts.packages: cargo_cmd.extend(["-p", p]) for e in opts.examples: cargo_cmd.extend(["--example", e]) def run(cargo_cmd, env): print(cargo_cmd, env, file=logfile) try: subprocess.run(cargo_cmd, env=env, cwd=opts.src_dir, check=True) except subprocess.SubprocessError: sys.exit(1) run(cargo_cmd, env) if opts.command == "build": target_dir = cargo_target_dir / "**" / opts.target if opts.bin: exe = glob.glob( str(target_dir / opts.bin) + opts.exe_suffix, recursive=True )[0] shutil.copy2(exe, opts.build_dir) depfile_content = generate_depfile_for(P(exe)) else: # Copy so files to build dir depfile_content = "" for suffix in opts.lib_suffixes: for f in glob.glob(str(target_dir / f"*.{suffix}"), recursive=True): libfile = P(f) depfile_content += generate_depfile_for(libfile) copied_file = opts.build_dir / libfile.name try: if copied_file.stat().st_mtime == libfile.stat().st_mtime: print(f"{copied_file} has not changed.", file=logfile) continue except FileNotFoundError: pass print(f"Copying {copied_file}", file=logfile) shutil.copy2(f, opts.build_dir) # Copy examples to builddir for example in opts.examples: example_glob = str(target_dir / "examples" / example) + opts.exe_suffix exe = glob.glob(example_glob, recursive=True)[0] shutil.copy2(exe, opts.build_dir) depfile_content += generate_depfile_for(P(exe)) with open(opts.depfile, "w") as depfile: depfile.write(depfile_content) # Copy generated pkg-config files for f in glob.glob(str(target_dir / "*.pc"), recursive=True): shutil.copy(f, opts.build_dir) # Move -uninstalled.pc to meson-uninstalled uninstalled = opts.build_dir / "meson-uninstalled" os.makedirs(uninstalled, exist_ok=True) for f in opts.build_dir.glob("*-uninstalled.pc"): # move() does not allow us to update the file so remove it if it already exists dest = uninstalled / P(f).name if dest.exists(): dest.unlink() # move() takes paths from Python3.9 on if (sys.version_info.major >= 3) and (sys.version_info.minor >= 9): shutil.move(f, uninstalled) else: shutil.move(str(f), str(uninstalled))