scripts: subtools builder - a starting point.
The starting point is the developer-run script that can build and
export a subtool with a single command.
This CL implements fetching a base SDK, setting it up for building
subtools, and the ability to install packages into the subtools
builder chroot.
BUG=b:277992359
TEST=./bin/build_sdk_subtools shellcheck
Change-Id: Ie30733fcb48f69a10f58fc9fc4b73aa37f7f5142
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/chromite/+/4617263
Reviewed-by: Mike Frysinger <vapier@chromium.org>
Commit-Queue: Trent Apted <tapted@chromium.org>
Tested-by: Trent Apted <tapted@chromium.org>
diff --git a/scripts/build_sdk_subtools_unittest.py b/scripts/build_sdk_subtools_unittest.py
new file mode 100644
index 0000000..4c346e5
--- /dev/null
+++ b/scripts/build_sdk_subtools_unittest.py
@@ -0,0 +1,167 @@
+# Copyright 2023 The ChromiumOS Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Unit tests for build_sdk_subtools."""
+
+import os
+from pathlib import Path
+from unittest import mock
+
+import pytest
+
+from chromite.lib import commandline
+from chromite.lib import cros_build_lib
+from chromite.scripts import build_sdk_subtools
+
+
+@pytest.fixture(name="outside_chroot")
+def outside_chroot_fixture():
+ """Mocks IsInsideChroot to be False."""
+ with mock.patch.object(
+ cros_build_lib, "IsInsideChroot", return_value=False
+ ) as outside_chroot:
+ yield outside_chroot
+
+
+@pytest.fixture(name="mock_emerge")
+def mock_emerge_fixture():
+ """Stubs the build_sdk_subtools emerge helper and sets it up to run."""
+ with mock.patch.multiple(
+ "chromite.scripts.build_sdk_subtools",
+ _run_system_emerge=mock.DEFAULT,
+ _is_inside_subtools_chroot=mock.DEFAULT,
+ ) as mocks:
+ mocks["_is_inside_subtools_chroot"].return_value = True
+ yield mocks["_run_system_emerge"]
+
+
+def test_must_run_outside_sdk(caplog) -> None:
+ """Tests build_sdk_subtools complains if run in the chroot."""
+ with pytest.raises(cros_build_lib.DieSystemExit) as error_info:
+ build_sdk_subtools.main()
+ assert error_info.value.code == 1
+ assert "build_sdk_subtools must be run outside the chroot" in caplog.text
+
+
+def test_cros_sdk(run_mock, outside_chroot) -> None:
+ """Tests the steps leading up to the cros_sdk invocation."""
+ # Fake a failure from cros_sdk to ensure it propagates.
+ run_mock.SetDefaultCmdResult(returncode=42)
+
+ assert build_sdk_subtools.main() == 42
+ assert outside_chroot.called
+ assert run_mock.call_count == 1
+ assert run_mock.call_args_list[0].args[0] == [
+ "cros_sdk",
+ "--chroot",
+ "/mnt/host/source/chroot/build/amd64-subtools-host",
+ "--create",
+ "--skip-chroot-upgrade",
+ ]
+
+
+def test_cros_sdk_clean(run_mock, outside_chroot) -> None:
+ """Tests steps leading up to the cros_sdk invocation with --clean."""
+ run_mock.SetDefaultCmdResult(returncode=42)
+
+ assert build_sdk_subtools.main(["--clean"]) == 42
+ assert outside_chroot.called
+ assert run_mock.call_count == 1
+ cros_sdk_cmd = run_mock.call_args_list[0].args[0]
+ assert cros_sdk_cmd[0] == "cros_sdk"
+ assert "--delete" in cros_sdk_cmd
+
+
+def test_cros_sdk_output_dir(run_mock, outside_chroot) -> None:
+ """Tests steps leading up to the cros_sdk invocation with --output-dir."""
+ run_mock.SetDefaultCmdResult(returncode=42)
+
+ assert build_sdk_subtools.main(["--output-dir", "/foo"]) == 42
+ assert outside_chroot.called
+ cros_sdk_cmd = run_mock.call_args_list[0].args[0]
+ chroot_arg_index = cros_sdk_cmd.index("/mnt/host/source/chroot/foo")
+ assert cros_sdk_cmd[0] == "cros_sdk"
+ assert cros_sdk_cmd[chroot_arg_index - 1] == "--chroot"
+
+
+def test_chroot_required_after_cros_sdk(run_mock, outside_chroot) -> None:
+ """Tests that build_sdk_subtools will ask for chroot when setup."""
+ with pytest.raises(commandline.ChrootRequiredError) as error_info:
+ build_sdk_subtools.main(["--no-setup-chroot"])
+
+ assert run_mock.call_count == 0
+ assert outside_chroot.called
+ assert error_info.value.cmd == ["build_sdk_subtools", "--no-setup-chroot"]
+ assert error_info.value.chroot_args == [
+ "--chroot",
+ "/mnt/host/source/chroot/build/amd64-subtools-host",
+ ]
+
+
+def test_chroots_into_output_dir(run_mock, outside_chroot) -> None:
+ """Tests that --output-dir is consumed properly after setup."""
+ with pytest.raises(commandline.ChrootRequiredError) as error_info:
+ build_sdk_subtools.main(["--no-setup-chroot", "--output-dir", "/foo"])
+
+ assert run_mock.call_count == 0
+ assert outside_chroot.called
+ assert error_info.value.chroot_args == [
+ "--chroot",
+ "/mnt/host/source/chroot/foo",
+ ]
+
+
+def test_setup_sdk_invocation(run_mock, outside_chroot) -> None:
+ """Tests the SDK setup invocation, before it becomes a subtools chroot."""
+ # Fake success from cros_sdk, failure from _setup_base_sdk().
+ run_mock.SetDefaultCmdResult(returncode=0)
+ run_mock.AddCmdResult(
+ ["sudo", "--", "build_sdk_subtools", "--relaunch-for-setup"],
+ returncode=42,
+ )
+
+ # Avoid bots passing CROS_CACHEDIR via `sudo` and messing up cmd matching.
+ with mock.patch.dict("os.environ"):
+ os.environ.pop("CROS_CACHEDIR", None)
+ assert build_sdk_subtools.main() == 42
+
+ assert run_mock.call_count == 2
+ assert outside_chroot.called
+
+ sudo_run_cmd = run_mock.call_args_list[1]
+
+ assert sudo_run_cmd.args[0] == [
+ "sudo",
+ "--",
+ "build_sdk_subtools",
+ "--relaunch-for-setup",
+ ]
+ assert sudo_run_cmd.kwargs["enter_chroot"] is True
+ assert sudo_run_cmd.kwargs["chroot_args"] == [
+ "--chroot",
+ "/mnt/host/source/chroot/build/amd64-subtools-host",
+ ]
+ # Stop here: Actually running `--relaunch-for-setup` principally wants to
+ # mutate the SDK state as root, which is too messy as a unit test.
+
+
+def test_default_package(mock_emerge) -> None:
+ """Tests a default virtual package is provided to update packages."""
+ assert build_sdk_subtools.main() == 0
+ assert mock_emerge.call_count == 1
+ emerge_cmd = mock_emerge.call_args.args[0]
+ assert Path("/mnt/host/source/chromite/bin/parallel_emerge") in emerge_cmd
+ assert emerge_cmd[-1] == "virtual/target-sdk-subtools"
+
+
+def test_provided_package(mock_emerge) -> None:
+ """Tests the default package can be passed in from command line."""
+ assert build_sdk_subtools.main(["vim"]) == 0
+ assert mock_emerge.call_args.args[0][-1] == "vim"
+
+
+def test_skip_package_update(mock_emerge) -> None:
+ """Tests --skip-package-update will not try to emerge anything."""
+ assert build_sdk_subtools.main(["--no-update-packages"]) == 0
+ assert mock_emerge.call_count == 0