Repository: bitcoin/bitcoin
File: ci/test/02_run_container.py (Python)

Code before:
# file COPYING or https://opensource.org/license/mit/.

import os
import shlex
import subprocess
import sys


def run(cmd, **kwargs):
    print("+ " + shlex.join(cmd), flush=True)
    try:
        return subprocess.run(cmd, check=True, **kwargs)
    except Exception as e:
        sys.exit(e)


def main():
    print("Export only allowed settings:")
    settings = run(
        ["bash", "-c", "grep export ./ci/test/00_setup_env*.sh"],
        stdout=subprocess.PIPE,
        text=True,
        encoding="utf8",
    ).stdout.splitlines()
    settings = set(l.split("=")[0].split("export ")[1] for l in settings)
    # Add "hidden" settings, which are never exported, manually. Otherwise,
    # they will not be passed on.
    settings.update([
        "BASE_BUILD_DIR",
        "CI_FAILFAST_TEST_LEAVE_DANGLING",
    ])

    # Append $USER to /tmp/env to support multi-user systems and $CONTAINER_NAME
    # to allow support starting multiple runs simultaneously by the same user.
    env_file = "/tmp/env-{u}-{c}".format(
        u=os.getenv("USER"),
        c=os.getenv("CONTAINER_NAME"),
    )
    with open(env_file, "w", encoding="utf8") as file:
        for k, v in os.environ.items():
            if k in settings:
                file.write(f"{k}={v}\n")
    run(["cat", env_file])

    run(["./ci/test/02_run_container.sh"])  # run the remainder


if __name__ == "__main__":
    main()


Review comment (bug):
we compute `M` from `MAKEJOBS` and then always emit a cpuset.
CI normally uses `-jN` with N > 0, but for edge/local setups, if `MAKEJOBS` ends up as `-j0` (or missing/malformed?), `M` can be `0` -> cpus is `[]` → we emit `--cpuset-cpus= (empty)`. Would it be ok to skip the flag when M <= 0 (and optionally guard parsing)?

```py
def maybe_cpuset():
    if os.getenv("HAVE_CGROUP_CPUSET"):
        P = int(run(['nproc'], stdout=subprocess.PIPE).stdout)
        mj = os.getenv("MAKEJOBS", "-j1")  # maybe a default for local runs?
        try:
            M = min(P, int(mj.lstrip("-j")))
        except (TypeError, ValueError):
            return []  # bad/missing MAKEJOBS -> just skip?
        if M > 0:
            cpus = sorted(random.sample(range(P), M))
            return [f"--cpuset-cpus={','.join(map(str, cpus))}"]
    return []
```

This does not change normal CI behavior but just avoids an odd empty cpuset in edge cases. If you prefer strictness, we could alternatively `sys.exit` instead?

Code after:
# file COPYING or https://opensource.org/license/mit/.

import os
import shlex
import subprocess
import sys


def run(cmd, **kwargs):
    print("+ " + shlex.join(cmd), flush=True)
    try:
        return subprocess.run(cmd, check=True, **kwargs)
    except Exception as e:
        sys.exit(e)


def main():
    print("Export only allowed settings:")
    settings = run(
        ["bash", "-c", "grep export ./ci/test/00_setup_env*.sh"],
        stdout=subprocess.PIPE,
        text=True,
        encoding="utf8",
    ).stdout.splitlines()
    settings = set(l.split("=")[0].split("export ")[1] for l in settings)
    # Add "hidden" settings, which are never exported, manually. Otherwise,
    # they will not be passed on.
    settings.update([
        "BASE_BUILD_DIR",
        "CI_FAILFAST_TEST_LEAVE_DANGLING",
    ])

    # Append $USER to /tmp/env to support multi-user systems and $CONTAINER_NAME
    # to allow support starting multiple runs simultaneously by the same user.
    env_file = "/tmp/env-{u}-{c}".format(
        u=os.getenv("USER"),
        c=os.getenv("CONTAINER_NAME"),
    )
    with open(env_file, "w", encoding="utf8") as file:
        for k, v in os.environ.items():
            if k in settings:
                file.write(f"{k}={v}\n")
    run(["cat", env_file])

    if not os.getenv("DANGER_RUN_CI_ON_HOST"):
        CI_IMAGE_LABEL = "bitcoin-ci-test"

        # Use buildx unconditionally
        # Using buildx is required to properly load the correct driver, for use with registry caching. Neither build, nor BUILDKIT=1 currently do this properly
        cmd_build = ["docker", "buildx", "build"]
        cmd_build += [