launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #31281
[Merge] ~ruinedyourlife/launchpad-buildd:feat-rock-build-base into launchpad-buildd:master
Quentin Debhi has proposed merging ~ruinedyourlife/launchpad-buildd:feat-rock-build-base into launchpad-buildd:master.
Commit message:
Add the capability to build rocks
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~ruinedyourlife/launchpad-buildd/+git/launchpad-buildd/+merge/470493
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~ruinedyourlife/launchpad-buildd:feat-rock-build-base into launchpad-buildd:master.
diff --git a/lpbuildd/rock.py b/lpbuildd/rock.py
new file mode 100644
index 0000000..6f7df1e
--- /dev/null
+++ b/lpbuildd/rock.py
@@ -0,0 +1,96 @@
+import os
+
+from lpbuildd.debian import DebianBuildManager, DebianBuildState
+from lpbuildd.proxy import BuildManagerProxyMixin
+
+RETCODE_SUCCESS = 0
+RETCODE_FAILURE_INSTALL = 200
+RETCODE_FAILURE_BUILD = 201
+
+
+class RockBuildState(DebianBuildState):
+ BUILD_ROCK = "BUILD_ROCK"
+
+
+class RockBuildManager(BuildManagerProxyMixin, DebianBuildManager):
+ """Build a rock."""
+
+ backend_name = "lxd"
+ initial_build_state = RockBuildState.BUILD_ROCK
+
+ @property
+ def needs_sanitized_logs(self):
+ return True
+
+ def initiate(self, files, chroot, extra_args):
+ """Initiate a build with a given set of files and chroot."""
+ self.name = extra_args["name"]
+ self.branch = extra_args.get("branch")
+ self.git_repository = extra_args.get("git_repository")
+ self.git_path = extra_args.get("git_path")
+ self.build_path = extra_args.get("build_path")
+ self.channels = extra_args.get("channels", {})
+ self.proxy_url = extra_args.get("proxy_url")
+ self.revocation_endpoint = extra_args.get("revocation_endpoint")
+ self.proxy_service = None
+
+ super().initiate(files, chroot, extra_args)
+
+ def doRunBuild(self):
+ """Run the process to build the rock."""
+ args = []
+ args.extend(self.startProxy())
+ if self.revocation_endpoint:
+ args.extend(["--revocation-endpoint", self.revocation_endpoint])
+ for snap, channel in sorted(self.channels.items()):
+ args.extend(["--channel", f"{snap}={channel}"])
+ if self.branch is not None:
+ args.extend(["--branch", self.branch])
+ if self.git_repository is not None:
+ args.extend(["--git-repository", self.git_repository])
+ if self.git_path is not None:
+ args.extend(["--git-path", self.git_path])
+ if self.build_path is not None:
+ args.extend(["--build-path", self.build_path])
+ args.append(self.name)
+ self.runTargetSubProcess("build-rock", *args)
+
+ def iterate_BUILD_ROCK(self, retcode):
+ """Finished building the rock."""
+ self.stopProxy()
+ self.revokeProxyToken()
+ if retcode == RETCODE_SUCCESS:
+ print("[rock] Returning build status: OK")
+ return self.deferGatherResults()
+ elif (
+ retcode >= RETCODE_FAILURE_INSTALL
+ and retcode <= RETCODE_FAILURE_BUILD
+ ):
+ if not self.alreadyfailed:
+ self._builder.buildFail()
+ print("[rock] Returning build status: Builder failed.")
+ self.alreadyfailed = True
+ else:
+ if not self.alreadyfailed:
+ self._builder.buildFail()
+ print("[rock] Returning build status: Build failed.")
+ self.alreadyfailed = True
+ self.doReapProcesses(self._state)
+
+ def iterateReap_BUILD_ROCK(self, retcode):
+ """Finished reaping after building the rock."""
+ self._state = DebianBuildState.UMOUNT
+ self.doUnmounting()
+
+ def gatherResults(self):
+ """Gather the results of the build and add them to the file cache."""
+ output_path = os.path.join("/home/buildd", self.name)
+ if self.build_path is not None:
+ output_path = os.path.join(output_path, self.build_path)
+ if self.backend.path_exists(output_path):
+ for entry in sorted(self.backend.listdir(output_path)):
+ path = os.path.join(output_path, entry)
+ if self.backend.islink(path):
+ continue
+ if entry.endswith(".rock"):
+ self.addWaitingFileFromBackend(path)
\ No newline at end of file
diff --git a/lpbuildd/target/build_rock.py b/lpbuildd/target/build_rock.py
new file mode 100644
index 0000000..d5002fd
--- /dev/null
+++ b/lpbuildd/target/build_rock.py
@@ -0,0 +1,126 @@
+import logging
+import os
+
+from lpbuildd.target.backend import check_path_escape
+from lpbuildd.target.build_snap import SnapChannelsAction
+from lpbuildd.target.operation import Operation
+from lpbuildd.target.proxy import BuilderProxyOperationMixin
+from lpbuildd.target.snapstore import SnapStoreOperationMixin
+from lpbuildd.target.vcs import VCSOperationMixin
+
+RETCODE_FAILURE_INSTALL = 200
+RETCODE_FAILURE_BUILD = 201
+
+
+logger = logging.getLogger(__name__)
+
+
+class BuildRock(
+ BuilderProxyOperationMixin,
+ VCSOperationMixin,
+ SnapStoreOperationMixin,
+ Operation,
+):
+ description = "Build a rock."
+
+ @classmethod
+ def add_arguments(cls, parser):
+ super().add_arguments(parser)
+ parser.add_argument(
+ "--channel",
+ action=SnapChannelsAction,
+ metavar="SNAP=CHANNEL",
+ dest="channels",
+ default={},
+ help="install SNAP from CHANNEL",
+ )
+ parser.add_argument(
+ "--build-path", default=".", help="location of rock to build."
+ )
+ parser.add_argument("name", help="name of rock to build")
+
+ def __init__(self, args, parser):
+ super().__init__(args, parser)
+ self.buildd_path = os.path.join("/home/buildd", self.args.name)
+
+ def install(self):
+ logger.info("Running install phase")
+ deps = []
+ if self.args.proxy_url:
+ deps.extend(self.proxy_deps)
+ self.install_git_proxy()
+ if self.backend.supports_snapd:
+ # udev is installed explicitly to work around
+ # https://bugs.launchpad.net/snapd/+bug/1731519.
+ # Low maintenance: we can keep udevs as a dependency
+ # since it is a low-level system dependency,
+ # and since it might be broken for older versions.
+ for dep in "snapd", "fuse", "squashfuse", "udev":
+ if self.backend.is_package_available(dep):
+ deps.append(dep)
+ deps.extend(self.vcs_deps)
+ # See charmcraft.provider.CharmcraftBuilddBaseConfiguration.setup.
+ deps.extend(
+ [
+ "python3-pip",
+ "python3-setuptools",
+ ]
+ )
+ self.backend.run(["apt-get", "-y", "install"] + deps)
+ if self.backend.supports_snapd:
+ self.snap_store_set_proxy()
+ for snap_name, channel in sorted(self.args.channels.items()):
+ # rockcraft is handled separately, since it requres --classic,
+ # which disables all sandboxing to ensure it runs with no strict
+ # confinement.
+ if snap_name != "rockcraft":
+ self.backend.run(
+ ["snap", "install", "--channel=%s" % channel, snap_name]
+ )
+ if "rockcraft" in self.args.channels:
+ self.backend.run(
+ [
+ "snap",
+ "install",
+ "--classic",
+ "--channel=%s" % self.args.channels["rockcraft"],
+ "rockcraft",
+ ]
+ )
+ else:
+ self.backend.run(["snap", "install", "--classic", "rockcraft"])
+ # With classic confinement, the snap can access the whole system.
+ # We could build the rock in /build, but we are using /home/buildd
+ # for consistency with other build types.
+ self.backend.run(["mkdir", "-p", "/home/buildd"])
+
+ def repo(self):
+ """Collect git or bzr branch."""
+ logger.info("Running repo phase...")
+ env = self.build_proxy_environment(proxy_url=self.args.proxy_url)
+ self.vcs_fetch(self.args.name, cwd="/home/buildd", env=env)
+ self.vcs_update_status(self.buildd_path)
+
+ def build(self):
+ logger.info("Running build phase...")
+ build_context_path = os.path.join(
+ "/home/buildd", self.args.name, self.args.build_path
+ )
+ check_path_escape(self.buildd_path, build_context_path)
+ env = self.build_proxy_environment(proxy_url=self.args.proxy_url)
+ args = ["rockcraft", "pack", "-v", "--destructive-mode"]
+ self.run_build_command(args, env=env, cwd=build_context_path)
+
+ def run(self):
+ try:
+ self.install()
+ except Exception:
+ logger.exception("Install failed")
+ return RETCODE_FAILURE_INSTALL
+ try:
+ self.repo()
+ self.build()
+ except Exception:
+ logger.exception("Build failed")
+ return RETCODE_FAILURE_BUILD
+ return 0
\ No newline at end of file
diff --git a/lpbuildd/target/cli.py b/lpbuildd/target/cli.py
index 670cf28..8d2ade2 100644
--- a/lpbuildd/target/cli.py
+++ b/lpbuildd/target/cli.py
@@ -9,6 +9,7 @@ from lpbuildd.target.apt import AddTrustedKeys, OverrideSourcesList, Update
from lpbuildd.target.build_charm import BuildCharm
from lpbuildd.target.build_livefs import BuildLiveFS
from lpbuildd.target.build_oci import BuildOCI
+from lpbuildd.target.build_rock import BuildRock
from lpbuildd.target.build_snap import BuildSnap
from lpbuildd.target.generate_translation_templates import (
GenerateTranslationTemplates,
@@ -46,6 +47,7 @@ operations = {
"add-trusted-keys": AddTrustedKeys,
"build-oci": BuildOCI,
"build-charm": BuildCharm,
+ "build-rock": BuildRock,
"buildlivefs": BuildLiveFS,
"buildsnap": BuildSnap,
"generate-translation-templates": GenerateTranslationTemplates,
diff --git a/lpbuildd/target/tests/test_build_rock.py b/lpbuildd/target/tests/test_build_rock.py
new file mode 100644
index 0000000..8f7682d
--- /dev/null
+++ b/lpbuildd/target/tests/test_build_rock.py
@@ -0,0 +1,761 @@
+import json
+import os
+import stat
+import subprocess
+from textwrap import dedent
+
+import responses
+from fixtures import FakeLogger, TempDir
+from systemfixtures import FakeFilesystem
+from testtools.matchers import AnyMatch, MatchesAll, MatchesListwise
+from testtools.testcase import TestCase
+
+from lpbuildd.target.backend import InvalidBuildFilePath
+from lpbuildd.target.build_rock import (
+ RETCODE_FAILURE_BUILD,
+ RETCODE_FAILURE_INSTALL,
+)
+from lpbuildd.target.cli import parse_args
+from lpbuildd.target.tests.matchers import (
+ RanAptGet,
+ RanBuildCommand,
+ RanCommand,
+)
+from lpbuildd.target.tests.test_build_snap import FakeRevisionID, RanSnap
+from lpbuildd.tests.fakebuilder import FakeMethod
+
+
+class TestBuildRock(TestCase):
+ def test_run_build_command_no_env(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.run_build_command(["echo", "hello world"])
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanBuildCommand(
+ ["echo", "hello world"], cwd="/home/buildd/test-image"
+ ),
+ ]
+ ),
+ )
+
+ def test_run_build_command_env(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.run_build_command(
+ ["echo", "hello world"], env={"FOO": "bar baz"}
+ )
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanBuildCommand(
+ ["echo", "hello world"],
+ FOO="bar baz",
+ cwd="/home/buildd/test-image",
+ )
+ ]
+ ),
+ )
+
+ def test_install_channels(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--channel=core=candidate",
+ "--channel=core18=beta",
+ "--channel=rockcraft=edge",
+ "--branch",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.install()
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanAptGet(
+ "install", "bzr", "python3-pip", "python3-setuptools"
+ ),
+ RanSnap("install", "--channel=candidate", "core"),
+ RanSnap("install", "--channel=beta", "core18"),
+ RanSnap(
+ "install", "--classic", "--channel=edge", "rockcraft"
+ ),
+ RanCommand(["mkdir", "-p", "/home/buildd"]),
+ ]
+ ),
+ )
+
+ def test_install_bzr(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.install()
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanAptGet(
+ "install", "bzr", "python3-pip", "python3-setuptools"
+ ),
+ RanSnap("install", "--classic", "rockcraft"),
+ RanCommand(["mkdir", "-p", "/home/buildd"]),
+ ]
+ ),
+ )
+
+ def test_install_git(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--git-repository",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.install()
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanAptGet(
+ "install", "git", "python3-pip", "python3-setuptools"
+ ),
+ RanSnap("install", "--classic", "rockcraft"),
+ RanCommand(["mkdir", "-p", "/home/buildd"]),
+ ]
+ ),
+ )
+
+ @responses.activate
+ def test_install_snap_store_proxy(self):
+ store_assertion = dedent(
+ """\
+ type: store
+ store: store-id
+ url: http://snap-store-proxy.example
+
+ body
+ """
+ )
+
+ def respond(request):
+ return 200, {"X-Assertion-Store-Id": "store-id"}, store_assertion
+
+ responses.add_callback(
+ "GET",
+ "http://snap-store-proxy.example/v2/auth/store/assertions",
+ callback=respond,
+ )
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--git-repository",
+ "lp:foo",
+ "--snap-store-proxy-url",
+ "http://snap-store-proxy.example/",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.install()
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanAptGet(
+ "install", "git", "python3-pip", "python3-setuptools"
+ ),
+ RanCommand(
+ ["snap", "ack", "/dev/stdin"],
+ input_text=store_assertion,
+ ),
+ RanCommand(
+ ["snap", "set", "core", "proxy.store=store-id"]
+ ),
+ RanSnap("install", "--classic", "rockcraft"),
+ RanCommand(["mkdir", "-p", "/home/buildd"]),
+ ]
+ ),
+ )
+
+ def test_install_proxy(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--git-repository",
+ "lp:foo",
+ "--proxy-url",
+ "http://proxy.example:3128/",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.bin = "/builderbin"
+ self.useFixture(FakeFilesystem()).add("/builderbin")
+ os.mkdir("/builderbin")
+ with open("/builderbin/lpbuildd-git-proxy", "w") as proxy_script:
+ proxy_script.write("proxy script\n")
+ os.fchmod(proxy_script.fileno(), 0o755)
+ build_rock.install()
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanAptGet(
+ "install",
+ "python3",
+ "socat",
+ "git",
+ "python3-pip",
+ "python3-setuptools",
+ ),
+ RanSnap("install", "--classic", "rockcraft"),
+ RanCommand(["mkdir", "-p", "/home/buildd"]),
+ ]
+ ),
+ )
+ self.assertEqual(
+ (b"proxy script\n", stat.S_IFREG | 0o755),
+ build_rock.backend.backend_fs[
+ "/usr/local/bin/lpbuildd-git-proxy"
+ ],
+ )
+
+ def test_repo_bzr(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.build_path = self.useFixture(TempDir()).path
+ build_rock.backend.run = FakeRevisionID("42")
+ build_rock.repo()
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanBuildCommand(
+ ["bzr", "branch", "lp:foo", "test-image"],
+ cwd="/home/buildd",
+ ),
+ RanBuildCommand(
+ ["bzr", "revno"],
+ cwd="/home/buildd/test-image",
+ get_output=True,
+ universal_newlines=True,
+ ),
+ ]
+ ),
+ )
+ status_path = os.path.join(build_rock.backend.build_path, "status")
+ with open(status_path) as status:
+ self.assertEqual({"revision_id": "42"}, json.load(status))
+
+ def test_repo_git(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--git-repository",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.build_path = self.useFixture(TempDir()).path
+ build_rock.backend.run = FakeRevisionID("0" * 40)
+ build_rock.repo()
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanBuildCommand(
+ ["git", "clone", "-n", "lp:foo", "test-image"],
+ cwd="/home/buildd",
+ ),
+ RanBuildCommand(
+ ["git", "checkout", "-q", "HEAD"],
+ cwd="/home/buildd/test-image",
+ ),
+ RanBuildCommand(
+ [
+ "git",
+ "submodule",
+ "update",
+ "--init",
+ "--recursive",
+ ],
+ cwd="/home/buildd/test-image",
+ ),
+ RanBuildCommand(
+ ["git", "rev-parse", "HEAD^{}"],
+ cwd="/home/buildd/test-image",
+ get_output=True,
+ universal_newlines=True,
+ ),
+ ]
+ ),
+ )
+ status_path = os.path.join(build_rock.backend.build_path, "status")
+ with open(status_path) as status:
+ self.assertEqual({"revision_id": "0" * 40}, json.load(status))
+
+ def test_repo_git_with_path(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--git-repository",
+ "lp:foo",
+ "--git-path",
+ "next",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.build_path = self.useFixture(TempDir()).path
+ build_rock.backend.run = FakeRevisionID("0" * 40)
+ build_rock.repo()
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanBuildCommand(
+ ["git", "clone", "-n", "lp:foo", "test-image"],
+ cwd="/home/buildd",
+ ),
+ RanBuildCommand(
+ ["git", "checkout", "-q", "next"],
+ cwd="/home/buildd/test-image",
+ ),
+ RanBuildCommand(
+ [
+ "git",
+ "submodule",
+ "update",
+ "--init",
+ "--recursive",
+ ],
+ cwd="/home/buildd/test-image",
+ ),
+ RanBuildCommand(
+ ["git", "rev-parse", "next^{}"],
+ cwd="/home/buildd/test-image",
+ get_output=True,
+ universal_newlines=True,
+ ),
+ ]
+ ),
+ )
+ status_path = os.path.join(build_rock.backend.build_path, "status")
+ with open(status_path) as status:
+ self.assertEqual({"revision_id": "0" * 40}, json.load(status))
+
+ def test_repo_git_with_tag_path(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--git-repository",
+ "lp:foo",
+ "--git-path",
+ "refs/tags/1.0",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.build_path = self.useFixture(TempDir()).path
+ build_rock.backend.run = FakeRevisionID("0" * 40)
+ build_rock.repo()
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanBuildCommand(
+ ["git", "clone", "-n", "lp:foo", "test-image"],
+ cwd="/home/buildd",
+ ),
+ RanBuildCommand(
+ ["git", "checkout", "-q", "refs/tags/1.0"],
+ cwd="/home/buildd/test-image",
+ ),
+ RanBuildCommand(
+ [
+ "git",
+ "submodule",
+ "update",
+ "--init",
+ "--recursive",
+ ],
+ cwd="/home/buildd/test-image",
+ ),
+ RanBuildCommand(
+ ["git", "rev-parse", "refs/tags/1.0^{}"],
+ cwd="/home/buildd/test-image",
+ get_output=True,
+ universal_newlines=True,
+ ),
+ ]
+ ),
+ )
+ status_path = os.path.join(build_rock.backend.build_path, "status")
+ with open(status_path) as status:
+ self.assertEqual({"revision_id": "0" * 40}, json.load(status))
+
+ def test_repo_proxy(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--git-repository",
+ "lp:foo",
+ "--proxy-url",
+ "http://proxy.example:3128/",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.build_path = self.useFixture(TempDir()).path
+ build_rock.backend.run = FakeRevisionID("0" * 40)
+ build_rock.repo()
+ env = {
+ "http_proxy": "http://proxy.example:3128/",
+ "https_proxy": "http://proxy.example:3128/",
+ "GIT_PROXY_COMMAND": "/usr/local/bin/lpbuildd-git-proxy",
+ "SNAPPY_STORE_NO_CDN": "1",
+ }
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanBuildCommand(
+ ["git", "clone", "-n", "lp:foo", "test-image"],
+ cwd="/home/buildd",
+ **env,
+ ),
+ RanBuildCommand(
+ ["git", "checkout", "-q", "HEAD"],
+ cwd="/home/buildd/test-image",
+ **env,
+ ),
+ RanBuildCommand(
+ [
+ "git",
+ "submodule",
+ "update",
+ "--init",
+ "--recursive",
+ ],
+ cwd="/home/buildd/test-image",
+ **env,
+ ),
+ RanBuildCommand(
+ ["git", "rev-parse", "HEAD^{}"],
+ cwd="/home/buildd/test-image",
+ get_output=True,
+ universal_newlines=True,
+ ),
+ ]
+ ),
+ )
+ status_path = os.path.join(build_rock.backend.build_path, "status")
+ with open(status_path) as status:
+ self.assertEqual({"revision_id": "0" * 40}, json.load(status))
+
+ def test_build(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.add_dir("/build/test-directory")
+ build_rock.build()
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanBuildCommand(
+ ["rockcraft", "pack", "-v", "--destructive-mode"],
+ cwd="/home/buildd/test-image/.",
+ ),
+ ]
+ ),
+ )
+
+ def test_build_with_path(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "--build-path",
+ "build-aux/",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.add_dir("/build/test-directory")
+ build_rock.build()
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanBuildCommand(
+ ["rockcraft", "pack", "-v", "--destructive-mode"],
+ cwd="/home/buildd/test-image/build-aux/",
+ ),
+ ]
+ ),
+ )
+
+ def test_build_proxy(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "--proxy-url",
+ "http://proxy.example:3128/",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.build()
+ env = {
+ "http_proxy": "http://proxy.example:3128/",
+ "https_proxy": "http://proxy.example:3128/",
+ "GIT_PROXY_COMMAND": "/usr/local/bin/lpbuildd-git-proxy",
+ "SNAPPY_STORE_NO_CDN": "1",
+ }
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesListwise(
+ [
+ RanBuildCommand(
+ ["rockcraft", "pack", "-v", "--destructive-mode"],
+ cwd="/home/buildd/test-image/.",
+ **env,
+ ),
+ ]
+ ),
+ )
+
+ def test_run_succeeds(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.build_path = self.useFixture(TempDir()).path
+ build_rock.backend.run = FakeRevisionID("42")
+ self.assertEqual(0, build_rock.run())
+ self.assertThat(
+ build_rock.backend.run.calls,
+ MatchesAll(
+ AnyMatch(
+ RanAptGet(
+ "install", "bzr", "python3-pip", "python3-setuptools"
+ ),
+ ),
+ AnyMatch(
+ RanBuildCommand(
+ ["bzr", "branch", "lp:foo", "test-image"],
+ cwd="/home/buildd",
+ )
+ ),
+ AnyMatch(
+ RanBuildCommand(
+ ["rockcraft", "pack", "-v", "--destructive-mode"],
+ cwd="/home/buildd/test-image/.",
+ )
+ ),
+ ),
+ )
+
+ def test_run_install_fails(self):
+ class FailInstall(FakeMethod):
+ def __call__(self, run_args, *args, **kwargs):
+ super().__call__(run_args, *args, **kwargs)
+ if run_args[0] == "apt-get":
+ raise subprocess.CalledProcessError(1, run_args)
+
+ self.useFixture(FakeLogger())
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.run = FailInstall()
+ self.assertEqual(RETCODE_FAILURE_INSTALL, build_rock.run())
+
+ def test_run_repo_fails(self):
+ class FailRepo(FakeMethod):
+ def __call__(self, run_args, *args, **kwargs):
+ super().__call__(run_args, *args, **kwargs)
+ if run_args[:2] == ["bzr", "branch"]:
+ raise subprocess.CalledProcessError(1, run_args)
+
+ self.useFixture(FakeLogger())
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.run = FailRepo()
+ self.assertEqual(RETCODE_FAILURE_BUILD, build_rock.run())
+
+ def test_run_build_fails(self):
+ class FailBuild(FakeMethod):
+ def __call__(self, run_args, *args, **kwargs):
+ super().__call__(run_args, *args, **kwargs)
+ if run_args[0] == "rockcraft":
+ raise subprocess.CalledProcessError(1, run_args)
+
+ self.useFixture(FakeLogger())
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.build_path = self.useFixture(TempDir()).path
+ build_rock.backend.run = FailBuild()
+ self.assertEqual(RETCODE_FAILURE_BUILD, build_rock.run())
+
+ def test_build_with_invalid_build_path_parent(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "--build-path",
+ "../",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.add_dir("/build/test-directory")
+ self.assertRaises(InvalidBuildFilePath, build_rock.build)
+
+ def test_build_with_invalid_build_path_absolute(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "--build-path",
+ "/etc",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.backend.add_dir("/build/test-directory")
+ self.assertRaises(InvalidBuildFilePath, build_rock.build)
+
+ def test_build_with_invalid_build_path_symlink(self):
+ args = [
+ "build-rock",
+ "--backend=fake",
+ "--series=xenial",
+ "--arch=amd64",
+ "1",
+ "--branch",
+ "lp:foo",
+ "--build-path",
+ "build/",
+ "test-image",
+ ]
+ build_rock = parse_args(args=args).operation
+ build_rock.buildd_path = self.useFixture(TempDir()).path
+ os.symlink(
+ "/etc/hosts", os.path.join(build_rock.buildd_path, "build")
+ )
+ self.assertRaises(InvalidBuildFilePath, build_rock.build)
diff --git a/lpbuildd/tests/test_rock.py b/lpbuildd/tests/test_rock.py
new file mode 100644
index 0000000..c66c982
--- /dev/null
+++ b/lpbuildd/tests/test_rock.py
@@ -0,0 +1,242 @@
+import base64
+import os
+
+import responses
+from fixtures import EnvironmentVariable, TempDir
+from testtools import TestCase
+from testtools.deferredruntest import AsynchronousDeferredRunTest
+from twisted.internet import defer
+
+from lpbuildd.rock import RockBuildManager, RockBuildState
+from lpbuildd.tests.fakebuilder import FakeBuilder
+from lpbuildd.tests.matchers import HasWaitingFiles
+
+
+class MockBuildManager(RockBuildManager):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.commands = []
+ self.iterators = []
+
+ def runSubProcess(self, path, command, iterate=None, env=None):
+ self.commands.append([path] + command)
+ if iterate is None:
+ iterate = self.iterate
+ self.iterators.append(iterate)
+ return 0
+
+
+class TestRockBuildManagerIteration(TestCase):
+ """Run RockBuildManager through its iteration steps."""
+
+ run_tests_with = AsynchronousDeferredRunTest.make_factory(timeout=5)
+
+ def setUp(self):
+ super().setUp()
+ self.working_dir = self.useFixture(TempDir()).path
+ builder_dir = os.path.join(self.working_dir, "builder")
+ home_dir = os.path.join(self.working_dir, "home")
+ for dir in (builder_dir, home_dir):
+ os.mkdir(dir)
+ self.useFixture(EnvironmentVariable("HOME", home_dir))
+ self.builder = FakeBuilder(builder_dir)
+ self.buildid = "123"
+ self.buildmanager = MockBuildManager(self.builder, self.buildid)
+ self.buildmanager._cachepath = self.builder._cachepath
+
+ def getState(self):
+ """Retrieve build manager's state."""
+ return self.buildmanager._state
+
+ @defer.inlineCallbacks
+ def startBuild(self, args=None, options=None):
+ # The build manager's iterate() kicks off the consecutive states
+ # after INIT.
+ extra_args = {
+ "series": "xenial",
+ "arch_tag": "i386",
+ "name": "test-rock",
+ }
+ if args is not None:
+ extra_args.update(args)
+ original_backend_name = self.buildmanager.backend_name
+ self.buildmanager.backend_name = "fake"
+ self.buildmanager.initiate({}, "chroot.tar.gz", extra_args)
+ self.buildmanager.backend_name = original_backend_name
+
+ # Skip states that are done in DebianBuildManager to the state
+ # directly before BUILD_ROCK.
+ self.buildmanager._state = RockBuildState.UPDATE
+
+ # BUILD_ROCK: Run the builder's payload to build the rock.
+ yield self.buildmanager.iterate(0)
+ self.assertEqual(RockBuildState.BUILD_ROCK, self.getState())
+ expected_command = [
+ "sharepath/bin/in-target",
+ "in-target",
+ "build-rock",
+ "--backend=lxd",
+ "--series=xenial",
+ "--arch=i386",
+ self.buildid,
+ ]
+ if options is not None:
+ expected_command.extend(options)
+ expected_command.append("test-rock")
+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
+ self.assertEqual(
+ self.buildmanager.iterate, self.buildmanager.iterators[-1]
+ )
+ self.assertFalse(self.builder.wasCalled("chrootFail"))
+
+ def test_status(self):
+ # The build manager returns saved status information on request.
+ self.assertEqual({}, self.buildmanager.status())
+ status_path = os.path.join(
+ self.working_dir, "home", "build-%s" % self.buildid, "status"
+ )
+ os.makedirs(os.path.dirname(status_path))
+ with open(status_path, "w") as status_file:
+ status_file.write('{"revision_id": "foo"}')
+ self.assertEqual({"revision_id": "foo"}, self.buildmanager.status())
+
+ @defer.inlineCallbacks
+ def test_iterate(self):
+ # The build manager iterates a normal build from start to finish.
+ args = {
+ "git_repository": "https://git.launchpad.dev/~example/+git/rock",
+ "git_path": "master",
+ }
+ expected_options = [
+ "--git-repository",
+ "https://git.launchpad.dev/~example/+git/rock",
+ "--git-path",
+ "master",
+ ]
+ yield self.startBuild(args, expected_options)
+
+ log_path = os.path.join(self.buildmanager._cachepath, "buildlog")
+ with open(log_path, "w") as log:
+ log.write("I am a build log.")
+
+ self.buildmanager.backend.add_file(
+ "/home/buildd/test-rock/test-rock_0_all.rock", b"I am rocking."
+ )
+
+ # After building the package, reap processes.
+ yield self.buildmanager.iterate(0)
+ expected_command = [
+ "sharepath/bin/in-target",
+ "in-target",
+ "scan-for-processes",
+ "--backend=lxd",
+ "--series=xenial",
+ "--arch=i386",
+ self.buildid,
+ ]
+
+ self.assertEqual(RockBuildState.BUILD_ROCK, self.getState())
+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
+ self.assertNotEqual(
+ self.buildmanager.iterate, self.buildmanager.iterators[-1]
+ )
+ self.assertFalse(self.builder.wasCalled("buildFail"))
+ self.assertThat(
+ self.builder,
+ HasWaitingFiles.byEquality(
+ {
+ "test-rock_0_all.rock": b"I am rocking.",
+ }
+ ),
+ )
+
+ # Control returns to the DebianBuildManager in the UMOUNT state.
+ self.buildmanager.iterateReap(self.getState(), 0)
+ expected_command = [
+ "sharepath/bin/in-target",
+ "in-target",
+ "umount-chroot",
+ "--backend=lxd",
+ "--series=xenial",
+ "--arch=i386",
+ self.buildid,
+ ]
+ self.assertEqual(RockBuildState.UMOUNT, self.getState())
+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
+ self.assertEqual(
+ self.buildmanager.iterate, self.buildmanager.iterators[-1]
+ )
+ self.assertFalse(self.builder.wasCalled("buildFail"))
+
+ @defer.inlineCallbacks
+ def test_iterate_build_path(self):
+ # The build manager iterates a build using build_path from start to
+ # finish.
+ args = {
+ "git_repository": "https://git.launchpad.dev/~example/+git/rock",
+ "git_path": "master",
+ "build_path": "rock",
+ }
+ expected_options = [
+ "--git-repository",
+ "https://git.launchpad.dev/~example/+git/rock",
+ "--git-path",
+ "master",
+ "--build-path",
+ "rock",
+ ]
+ yield self.startBuild(args, expected_options)
+
+ log_path = os.path.join(self.buildmanager._cachepath, "buildlog")
+ with open(log_path, "w") as log:
+ log.write("I am a build log.")
+
+ self.buildmanager.backend.add_file(
+ "/home/buildd/test-rock/rock/test-rock_0_all.rock",
+ b"I am rocking.",
+ )
+
+ # After building the package, reap processes.
+ yield self.buildmanager.iterate(0)
+ expected_command = [
+ "sharepath/bin/in-target",
+ "in-target",
+ "scan-for-processes",
+ "--backend=lxd",
+ "--series=xenial",
+ "--arch=i386",
+ self.buildid,
+ ]
+
+ self.assertEqual(RockBuildState.BUILD_ROCK, self.getState())
+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
+ self.assertNotEqual(
+ self.buildmanager.iterate, self.buildmanager.iterators[-1]
+ )
+ self.assertFalse(self.builder.wasCalled("buildFail"))
+ self.assertThat(
+ self.builder,
+ HasWaitingFiles.byEquality(
+ {
+ "test-rock_0_all.rock": b"I am rocking.",
+ }
+ ),
+ )
+
+ # Control returns to the DebianBuildManager in the UMOUNT state.
+ self.buildmanager.iterateReap(self.getState(), 0)
+ expected_command = [
+ "sharepath/bin/in-target",
+ "in-target",
+ "umount-chroot",
+ "--backend=lxd",
+ "--series=xenial",
+ "--arch=i386",
+ self.buildid,
+ ]
+ self.assertEqual(RockBuildState.UMOUNT, self.getState())
+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
+ self.assertEqual(
+ self.buildmanager.iterate, self.buildmanager.iterators[-1]
+ )
+ self.assertFalse(self.builder.wasCalled("buildFail"))
\ No newline at end of file