launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #19115
[Merge] lp:~cjwatson/launchpad-buildd/snapcraft into lp:launchpad-buildd
Colin Watson has proposed merging lp:~cjwatson/launchpad-buildd/snapcraft into lp:launchpad-buildd.
Commit message:
Add support for building snaps (LP: #1476405).
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
Related bugs:
Bug #1476405 in launchpad-buildd: "Add support for building snaps"
https://bugs.launchpad.net/launchpad-buildd/+bug/1476405
For more details, see:
https://code.launchpad.net/~cjwatson/launchpad-buildd/snapcraft/+merge/266541
Add support for building snaps.
I've successfully tested this end-to-end locally. Doing that requires fetching plainbox, ubuntu-snappy, and snapcraft from an assortment of locations (https://launchpad.net/~hardware-certification/+archive/ubuntu/public, the wily primary archive, and https://launchpad.net/~snappy-dev/+archive/ubuntu/snapcraft-daily respectively), building them in a local PPA, and then setting that PPA's reference as the value of config.snappy.tools_archive (once that code lands). For now it's quite a complicated procedure, but it does work; it'll be easier to run on production since the appropriate PPA already exists.
--
Your team Launchpad code reviewers is requested to review the proposed merge of lp:~cjwatson/launchpad-buildd/snapcraft into lp:launchpad-buildd.
=== modified file 'Makefile'
--- Makefile 2015-06-15 16:48:51 +0000
+++ Makefile 2015-07-31 11:57:12 +0000
@@ -33,5 +33,6 @@
lpbuildd.tests.test_check_implicit_pointer_functions \
lpbuildd.tests.test_harness \
lpbuildd.tests.test_livefs \
+ lpbuildd.tests.test_snap \
lpbuildd.tests.test_sourcepackagerecipe \
lpbuildd.tests.test_translationtemplatesbuildmanager
=== modified file 'buildd-slave.tac'
--- buildd-slave.tac 2015-05-11 14:37:01 +0000
+++ buildd-slave.tac 2015-07-31 11:57:12 +0000
@@ -23,6 +23,7 @@
from lpbuildd.livefs import LiveFilesystemBuildManager
from lpbuildd.log import RotatableFileLogObserver
from lpbuildd.slave import XMLRPCBuildDSlave
+from lpbuildd.snap import SnapBuildManager
from lpbuildd.sourcepackagerecipe import SourcePackageRecipeBuildManager
from lpbuildd.translationtemplates import TranslationTemplatesBuildManager
@@ -41,6 +42,7 @@
slave.registerBuilder(
TranslationTemplatesBuildManager, 'translation-templates')
slave.registerBuilder(LiveFilesystemBuildManager, "livefs")
+slave.registerBuilder(SnapBuildManager, "snap")
application = service.Application('BuildDSlave')
application.addComponent(
=== modified file 'buildlivefs'
--- buildlivefs 2014-06-24 14:59:08 +0000
+++ buildlivefs 2015-07-31 11:57:12 +0000
@@ -8,11 +8,15 @@
from optparse import OptionParser
import os
-import re
import subprocess
import sys
import traceback
+from lpbuildd.util import (
+ set_personality,
+ shell_escape,
+ )
+
RETCODE_SUCCESS = 0
RETCODE_FAILURE_INSTALL = 200
@@ -29,41 +33,6 @@
return os.path.join(os.environ["HOME"], "build-" + build_id, *extra)
-non_meta_re = re.compile(r'^[a-zA-Z0-9+,./:=@_-]+$')
-
-def shell_escape(arg):
- if non_meta_re.match(arg):
- return arg
- else:
- return "'%s'" % arg.replace("'", "'\\''")
-
-
-linux32_arches = [
- "armel",
- "armhf",
- "hppa",
- "i386",
- "lpia",
- "mips",
- "mipsel",
- "powerpc",
- "s390",
- "sparc",
- ]
-linux64_arches = [
- "alpha",
- "amd64",
- "arm64",
- "hppa64",
- "ia64",
- "ppc64",
- "ppc64el",
- "s390x",
- "sparc64",
- "x32",
- ]
-
-
class LiveFSBuilder:
"""Builds a live file system."""
@@ -77,10 +46,7 @@
:param args: the command and arguments to run.
"""
- if self.options.arch in linux32_arches:
- args = ["linux32"] + args
- elif self.options.arch in linux64_arches:
- args = ["linux64"] + args
+ args = set_personality(self.options.arch, args)
if echo:
print "Running in chroot: %s" % ' '.join(
"'%s'" % arg for arg in args)
=== added file 'buildsnap'
--- buildsnap 1970-01-01 00:00:00 +0000
+++ buildsnap 2015-07-31 11:57:12 +0000
@@ -0,0 +1,157 @@
+#! /usr/bin/python -u
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+"""A script that builds a snap."""
+
+from __future__ import print_function
+
+__metaclass__ = type
+
+from optparse import OptionParser
+import os
+import subprocess
+import sys
+import traceback
+
+from lpbuildd.util import (
+ set_personality,
+ shell_escape,
+ )
+
+
+RETCODE_SUCCESS = 0
+RETCODE_FAILURE_INSTALL = 200
+RETCODE_FAILURE_BUILD = 201
+
+
+def get_build_path(build_id, *extra):
+ """Generate a path within the build directory.
+
+ :param build_id: the build id to use.
+ :param extra: the extra path segments within the build directory.
+ :return: the generated path.
+ """
+ return os.path.join(os.environ["HOME"], "build-" + build_id, *extra)
+
+
+class SnapBuilder:
+ """Builds a snap."""
+
+ def __init__(self, options, name):
+ self.options = options
+ self.name = name
+ self.chroot_path = get_build_path(
+ self.options.build_id, 'chroot-autobuild')
+ # Set to False for local testing if your chroot doesn't have an
+ # appropriate certificate for your codehosting system.
+ self.ssl_verify = True
+
+ def chroot(self, args, echo=False):
+ """Run a command in the chroot.
+
+ :param args: the command and arguments to run.
+ :param echo: if True, print the command before executing it.
+ """
+ args = set_personality(self.options.arch, args)
+ if echo:
+ print(
+ "Running in chroot: %s" % ' '.join(
+ "'%s'" % arg for arg in args))
+ sys.stdout.flush()
+ subprocess.check_call([
+ "/usr/bin/sudo", "/usr/sbin/chroot", self.chroot_path] + args)
+
+ def run_build_command(self, args, path="/build", env=None, echo=False):
+ """Run a build command in the chroot.
+
+ This is unpleasant because we need to run it in /build under sudo
+ chroot, and there's no way to do this without either a helper
+ program in the chroot or unpleasant quoting. We go for the
+ unpleasant quoting.
+
+ :param args: the command and arguments to run.
+ :param path: the working directory to use in the chroot.
+ :param env: dictionary of additional environment variables to set.
+ :param echo: if True, print the command before executing it.
+ """
+ args = [shell_escape(arg) for arg in args]
+ path = shell_escape(path)
+ if env:
+ args = ["env"] + [
+ "%s=%s" % (key, shell_escape(value))
+ for key, value in env.items()] + args
+ command = "cd %s && %s" % (path, " ".join(args))
+ self.chroot(["/bin/sh", "-c", command], echo=echo)
+
+ def install(self):
+ deps = ["snapcraft"]
+ if self.options.branch is not None:
+ deps.append("bzr")
+ else:
+ deps.append("git")
+ # Fixed better by
+ # https://code.launchpad.net/~cjwatson/snapcraft/depend-sudo/+merge/266533;
+ # drop this when that has been merged.
+ deps.append("sudo")
+ self.chroot(["apt-get", "-y", "install"] + deps)
+
+ def build(self):
+ if self.options.branch is not None:
+ self.run_build_command([
+ "bzr", "branch", self.options.branch, self.name])
+ else:
+ assert self.options.git_repository is not None
+ assert self.options.git_path is not None
+ if not self.ssl_verify:
+ env = {"GIT_SSL_NO_VERIFY": "1"}
+ else:
+ env = None
+ self.run_build_command([
+ "git", "clone", "-b", self.options.git_path,
+ self.options.git_repository, self.name],
+ env=env)
+ self.run_build_command(
+ ["snapcraft", "assemble"], path=os.path.join("/build", self.name))
+
+
+def main():
+ parser = OptionParser("%prog [options] NAME")
+ parser.add_option("--build-id", help="build identifier")
+ parser.add_option(
+ "--arch", metavar="ARCH", help="build for architecture ARCH")
+ parser.add_option(
+ "--branch", metavar="BRANCH", help="build from this Bazaar branch")
+ parser.add_option(
+ "--git-repository", metavar="REPOSITORY",
+ help="build from this Git repository")
+ parser.add_option(
+ "--git-path", metavar="REF-PATH",
+ help="build from this ref path in REPOSITORY")
+ options, args = parser.parse_args()
+ if (options.git_repository is None) != (options.git_path is None):
+ parser.error(
+ "must provide both --git-repository and --git-path or neither")
+ if (options.branch is None) == (options.git_repository is None):
+ parser.error(
+ "must provide exactly one of --branch and --git-repository")
+ if len(args) != 1:
+ parser.error(
+ "must provide a package name and no other positional arguments")
+ [name] = args
+ builder = SnapBuilder(options, name)
+ try:
+ builder.install()
+ except Exception:
+ traceback.print_exc()
+ return RETCODE_FAILURE_INSTALL
+ try:
+ builder.build()
+ except Exception:
+ traceback.print_exc()
+ return RETCODE_FAILURE_BUILD
+ return RETCODE_SUCCESS
+
+
+if __name__ == "__main__":
+ sys.exit(main())
=== modified file 'debian/changelog'
--- debian/changelog 2015-07-23 12:23:33 +0000
+++ debian/changelog 2015-07-31 11:57:12 +0000
@@ -7,6 +7,7 @@
the situation rather than trusting just the definite information.
* Handle architecture restrictions, architecture qualifications, and
restriction formulas (build profiles) in build-dependencies.
+ * Add support for building snaps (LP: #1476405).
-- Colin Watson <cjwatson@xxxxxxxxxx> Thu, 16 Jul 2015 14:00:16 +0100
=== modified file 'debian/rules'
--- debian/rules 2015-07-05 12:43:54 +0000
+++ debian/rules 2015-07-31 11:57:12 +0000
@@ -19,7 +19,7 @@
slavebins = unpack-chroot mount-chroot update-debian-chroot sbuild-package \
scan-for-processes umount-chroot remove-build override-sources-list \
buildrecipe generate-translation-templates slave-prep buildlivefs \
- sudo-wrapper
+ sudo-wrapper buildsnap
BUILDDUID=65500
BUILDDGID=65500
=== added file 'lpbuildd/snap.py'
--- lpbuildd/snap.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/snap.py 2015-07-31 11:57:12 +0000
@@ -0,0 +1,95 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import os
+import shutil
+
+from lpbuildd.debian import (
+ DebianBuildManager,
+ DebianBuildState,
+ get_build_path,
+ )
+
+
+RETCODE_SUCCESS = 0
+RETCODE_FAILURE_INSTALL = 200
+RETCODE_FAILURE_BUILD = 201
+
+
+class SnapBuildState(DebianBuildState):
+ BUILD_SNAP = "BUILD_SNAP"
+
+
+class SnapBuildManager(DebianBuildManager):
+ """Build a snap."""
+
+ initial_build_state = SnapBuildState.BUILD_SNAP
+
+ def __init__(self, slave, buildid, **kwargs):
+ super(SnapBuildManager, self).__init__(slave, buildid, **kwargs)
+ self.build_snap_path = os.path.join(self._slavebin, "buildsnap")
+
+ def initiate(self, files, chroot, extra_args):
+ """Initiate a build with a given set of files and chroot."""
+ self.build_path = get_build_path(
+ self.home, self._buildid, "chroot-autobuild", "build")
+ if os.path.isdir(self.build_path):
+ shutil.rmtree(self.build_path)
+
+ 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")
+
+ super(SnapBuildManager, self).initiate(files, chroot, extra_args)
+
+ def doRunBuild(self):
+ """Run the process to build the snap."""
+ args = [
+ "buildsnap",
+ "--build-id", self._buildid,
+ "--arch", self.arch_tag,
+ ]
+ 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])
+ args.append(self.name)
+ self.runSubProcess(self.build_snap_path, args)
+
+ def iterate_BUILD_SNAP(self, retcode):
+ """Finished building the snap."""
+ if retcode == RETCODE_SUCCESS:
+ self.gatherResults()
+ print("Returning build status: OK")
+ elif (retcode >= RETCODE_FAILURE_INSTALL and
+ retcode <= RETCODE_FAILURE_BUILD):
+ if not self.alreadyfailed:
+ self._slave.buildFail()
+ print("Returning build status: Build failed.")
+ self.alreadyfailed = True
+ else:
+ if not self.alreadyfailed:
+ self._slave.builderFail()
+ print("Returning build status: Builder failed.")
+ self.alreadyfailed = True
+ self.doReapProcesses(self._state)
+
+ def iterateReap_BUILD_SNAP(self, retcode):
+ """Finished reaping after building the snap."""
+ 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(self.build_path, self.name)
+ if not os.path.exists(output_path):
+ return
+ for entry in sorted(os.listdir(output_path)):
+ path = os.path.join(output_path, entry)
+ if entry.endswith(".snap") and not os.path.islink(path):
+ self._slave.addWaitingFile(path)
=== added file 'lpbuildd/tests/test_snap.py'
--- lpbuildd/tests/test_snap.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/tests/test_snap.py 2015-07-31 11:57:12 +0000
@@ -0,0 +1,121 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import os
+import shutil
+import tempfile
+
+from testtools import TestCase
+
+from lpbuildd.snap import (
+ SnapBuildManager,
+ SnapBuildState,
+ )
+from lpbuildd.tests.fakeslave import FakeSlave
+
+
+class MockBuildManager(SnapBuildManager):
+ def __init__(self, *args, **kwargs):
+ super(MockBuildManager, self).__init__(*args, **kwargs)
+ self.commands = []
+ self.iterators = []
+
+ def runSubProcess(self, path, command, iterate=None):
+ self.commands.append([path] + command)
+ if iterate is None:
+ iterate = self.iterate
+ self.iterators.append(iterate)
+ return 0
+
+
+class TestSnapBuildManagerIteration(TestCase):
+ """Run SnapBuildManager through its iteration steps."""
+ def setUp(self):
+ super(TestSnapBuildManagerIteration, self).setUp()
+ self.working_dir = tempfile.mkdtemp()
+ self.addCleanup(lambda: shutil.rmtree(self.working_dir))
+ slave_dir = os.path.join(self.working_dir, "slave")
+ home_dir = os.path.join(self.working_dir, "home")
+ for dir in (slave_dir, home_dir):
+ os.mkdir(dir)
+ self.slave = FakeSlave(slave_dir)
+ self.buildid = "123"
+ self.buildmanager = MockBuildManager(self.slave, self.buildid)
+ self.buildmanager.home = home_dir
+ self.buildmanager._cachepath = self.slave._cachepath
+ self.build_dir = os.path.join(
+ home_dir, "build-%s" % self.buildid, "chroot-autobuild", "build")
+
+ def getState(self):
+ """Retrieve build manager's state."""
+ return self.buildmanager._state
+
+ def startBuild(self):
+ # The build manager's iterate() kicks off the consecutive states
+ # after INIT.
+ extra_args = {
+ "arch_tag": "i386",
+ "name": "test-snap",
+ "git_repository": "https://git.launchpad.dev/~example/+git/snap",
+ "git_path": "master",
+ }
+ self.buildmanager.initiate({}, "chroot.tar.gz", extra_args)
+
+ # Skip states that are done in DebianBuildManager to the state
+ # directly before BUILD_SNAP.
+ self.buildmanager._state = SnapBuildState.UPDATE
+
+ # BUILD_SNAP: Run the slave's payload to build the snap package.
+ self.buildmanager.iterate(0)
+ self.assertEqual(SnapBuildState.BUILD_SNAP, self.getState())
+ expected_command = [
+ "sharepath/slavebin/buildsnap", "buildsnap",
+ "--build-id", self.buildid, "--arch", "i386",
+ "--git-repository", "https://git.launchpad.dev/~example/+git/snap",
+ "--git-path", "master",
+ "test-snap",
+ ]
+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
+ self.assertEqual(
+ self.buildmanager.iterate, self.buildmanager.iterators[-1])
+ self.assertFalse(self.slave.wasCalled("chrootFail"))
+
+ def test_iterate(self):
+ # The build manager iterates a normal build from start to finish.
+ self.startBuild()
+
+ log_path = os.path.join(self.buildmanager._cachepath, "buildlog")
+ log = open(log_path, "w")
+ log.write("I am a build log.")
+ log.close()
+
+ output_dir = os.path.join(self.build_dir, "test-snap")
+ os.makedirs(output_dir)
+ snap_path = os.path.join(output_dir, "test-snap_0_all.snap")
+ with open(snap_path, "w") as snap:
+ snap.write("I am a snap package.")
+
+ # After building the package, reap processes.
+ self.buildmanager.iterate(0)
+ expected_command = [
+ "sharepath/slavebin/scan-for-processes", "scan-for-processes",
+ self.buildid,
+ ]
+ self.assertEqual(SnapBuildState.BUILD_SNAP, self.getState())
+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
+ self.assertNotEqual(
+ self.buildmanager.iterate, self.buildmanager.iterators[-1])
+ self.assertFalse(self.slave.wasCalled("buildFail"))
+ self.assertEqual([((snap_path,), {})], self.slave.addWaitingFile.calls)
+
+ # Control returns to the DebianBuildManager in the UMOUNT state.
+ self.buildmanager.iterateReap(self.getState(), 0)
+ expected_command = [
+ "sharepath/slavebin/umount-chroot", "umount-chroot", self.buildid]
+ self.assertEqual(SnapBuildState.UMOUNT, self.getState())
+ self.assertEqual(expected_command, self.buildmanager.commands[-1])
+ self.assertEqual(
+ self.buildmanager.iterate, self.buildmanager.iterators[-1])
+ self.assertFalse(self.slave.wasCalled("buildFail"))
=== added file 'lpbuildd/util.py'
--- lpbuildd/util.py 1970-01-01 00:00:00 +0000
+++ lpbuildd/util.py 2015-07-31 11:57:12 +0000
@@ -0,0 +1,50 @@
+# Copyright 2015 Canonical Ltd. This software is licensed under the
+# GNU Affero General Public License version 3 (see the file LICENSE).
+
+__metaclass__ = type
+
+import re
+
+
+non_meta_re = re.compile(r'^[a-zA-Z0-9+,./:=@_-]+$')
+
+def shell_escape(arg):
+ if non_meta_re.match(arg):
+ return arg
+ else:
+ return "'%s'" % arg.replace("'", "'\\''")
+
+
+linux32_arches = [
+ "armel",
+ "armhf",
+ "hppa",
+ "i386",
+ "lpia",
+ "mips",
+ "mipsel",
+ "powerpc",
+ "s390",
+ "sparc",
+ ]
+linux64_arches = [
+ "alpha",
+ "amd64",
+ "arm64",
+ "hppa64",
+ "ia64",
+ "ppc64",
+ "ppc64el",
+ "s390x",
+ "sparc64",
+ "x32",
+ ]
+
+
+def set_personality(arch, args):
+ if arch in linux32_arches:
+ return ["linux32"] + args
+ elif arch in linux64_arches:
+ return ["linux64"] + args
+ else:
+ return args
Follow ups