Skip to content

Commit

Permalink
New command cleanbuild (using lxd)
Browse files Browse the repository at this point in the history
This new command's intention is to make building with a clean
slate and easy chore.

LP: #1480144

Signed-off-by: Sergio Schvezov <sergio.schvezov@ubuntu.com>
  • Loading branch information
sergiusens committed Feb 19, 2016
1 parent 0adb5cc commit 5e527fb
Show file tree
Hide file tree
Showing 15 changed files with 463 additions and 28 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Expand Up @@ -31,7 +31,7 @@ before_script:
- sudo lxc config device add xenial /dev/sda1 disk source=$(pwd) path=$(pwd)
# Install the snapcraft dependencies.
- sudo lxc exec xenial -- apt-get update
- sudo lxc exec xenial -- apt-get install -y pyflakes python-flake8 python3.5 python3-apt python3-docopt python3-coverage python3-fixtures python3-flake8 python3-jsonschema python3-mccabe python3-mock python3-pep8 python3-pexpect python3-pip python3-requests python3-requests-oauthlib python3-responses python3-ssoclient python3-testscenarios python3-testtools python3-xdg python3-yaml python3-lxml squashfs-tools python3-progressbar python3-requests-toolbelt
- sudo lxc exec xenial -- apt-get install -y pyflakes python-flake8 python3.5 python3-apt python3-docopt python3-coverage python3-fixtures python3-flake8 python3-jsonschema python3-mccabe python3-mock python3-pep8 python3-pexpect python3-pip python3-requests python3-requests-oauthlib python3-responses python3-ssoclient python3-testscenarios python3-testtools python3-xdg python3-yaml python3-lxml squashfs-tools python3-progressbar python3-requests-toolbelt python3-petname
script:
- sudo -E lxc exec xenial -- su - ubuntu -c "cd $(pwd); TEST_USER_PASSWORD=$TEST_USER_PASSWORD ./runtests.sh $TEST_SUITE"
after_success:
Expand Down
5 changes: 4 additions & 1 deletion debian/control
Expand Up @@ -10,6 +10,7 @@ Build-Depends: debhelper (>= 9),
python3-fixtures,
python3-jsonschema,
python3-lxml,
python3-petname,
python3-pkg-resources,
python3-progressbar,
python3-requests,
Expand All @@ -27,10 +28,12 @@ Standards-Version: 3.9.6

Package: snapcraft
Architecture: all
Depends: python3-apt,
Depends: lxd,
python3-apt,
python3-docopt,
python3-jsonschema,
python3-lxml,
python3-petname,
python3-pkg-resources,
python3-progressbar,
python3-requests,
Expand Down
1 change: 1 addition & 0 deletions debian/tests/control
Expand Up @@ -5,6 +5,7 @@ Depends: @builddeps@
Tests: integrationtests examplestests
Restrictions: allow-stderr, isolation-container, rw-build-tree
Depends: @,
lxd,
python3-pep8,
pyflakes,
python-flake8,
Expand Down
11 changes: 11 additions & 0 deletions integration_tests/test_snap.py
Expand Up @@ -75,6 +75,17 @@ def test_snap(self):
os.path.join('snap', 'bin', 'not-wrapped.wrapper'),
Not(FileExists()))

def test_cleanbuild(self):
project_dir = 'assemble'
self.run_snapcraft('cleanbuild', project_dir)
os.chdir(project_dir)

snap_source_path = 'assemble_1.0_source.tar.bz2'
self.assertThat(snap_source_path, FileExists())

snap_file_path = 'assemble_1.0_{}.snap'.format(get_arch())
self.assertThat(snap_file_path, FileExists())

def test_snap_directory(self):
project_dir = 'assemble'
self.run_snapcraft('snap', project_dir)
Expand Down
3 changes: 1 addition & 2 deletions integration_tests/test_stage.py
Expand Up @@ -32,6 +32,5 @@ def test_conflicts(self):
self.assertEqual(1, exception.returncode)
expected = (
"Parts 'p1' and 'p2' have the following file paths in common "
"which have different contents:\n"
"bin/test\n")
"which have\ndifferent contents: bin/test\n")
self.assertThat(exception.output, EndsWith(expected))
87 changes: 87 additions & 0 deletions snapcraft/commands/cleanbuild.py
@@ -0,0 +1,87 @@
#!/usr/bin/python3
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2015, 2016 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
snapcraft cleanbuild
Creates a lxd container to build the snap.
This is a way to guarantee that the snapcraft.yaml used with associated
local sources is not using any dependencies local to the developer system.
The cleanbuild command requires a properly setup lxd environment that
can connect to external networks. Refer to the "Ubuntu Desktop and
Ubuntu Server" section on
https://linuxcontainers.org/lxd/getting-started-cli
to get started.
Usage:
cleanbuild [options]
Options:
-h --help show this help message and exit.
"""

import logging
import os.path
import tarfile

from docopt import docopt

from snapcraft import repo
from snapcraft.lxd import Cleanbuilder
from snapcraft.common import format_snap_name

from snapcraft.yaml import load_config

logger = logging.getLogger(__name__)


def _create_tar_filter(tar_filename):
def _tar_filter(tarinfo):
fn = tarinfo.name
if fn.startswith('./parts/') and not fn.startswith('./parts/plugins'):
return None
elif fn in ('./stage', './snap', tar_filename):
return None
elif fn.endswith('.snap'):
return None
return tarinfo
return _tar_filter


def main(argv=None):
argv = [] if argv is None else argv
docopt(__doc__, argv=argv)

if not repo.is_package_installed('lxd'):
raise EnvironmentError(
'The lxd package is not installed, in order to use `cleanbuild` '
'you must install lxd onto your system. Refer to the '
'"Ubuntu Desktop and Ubuntu Server" section on '
'https://linuxcontainers.org/lxd/getting-started-cli/'
'#ubuntu-desktop-and-ubuntu-server to enable a proper setup.')

config = load_config()
tar_filename = '{}_{}_source.tar.bz2'.format(
config.data['name'], config.data['version'])

with tarfile.open(tar_filename, 'w:bz2') as t:
t.add(os.path.curdir, filter=_create_tar_filter(tar_filename))

snap_filename = format_snap_name(config.data)
Cleanbuilder(snap_filename, tar_filename).execute()
9 changes: 2 additions & 7 deletions snapcraft/commands/upload.py
Expand Up @@ -34,27 +34,22 @@

import snapcraft.yaml
from snapcraft.commands import snap
from snapcraft.common import format_snap_name
from snapcraft.config import load_config
from snapcraft.storeapi import upload


logger = logging.getLogger(__name__)


def _format_snap_name(snap):
snap['arch'] = (snap['architectures'][0]
if len(snap['architectures']) == 1 else 'multi')
return '{name}_{version}_{arch}.snap'.format(**snap)


def main(argv=None):
"""Upload snap package to the Ubuntu Store."""
argv = argv if argv else []
docopt(__doc__, argv=argv)

# make sure the full lifecycle is executed
yaml_config = snapcraft.yaml.load_config()
snap_filename = _format_snap_name(yaml_config.data)
snap_filename = format_snap_name(yaml_config.data)

if not os.path.exists(snap_filename):
logger.info(
Expand Down
26 changes: 19 additions & 7 deletions snapcraft/common.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2015 Canonical Ltd
# Copyright (C) 2015, 2016 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
Expand Down Expand Up @@ -86,22 +86,34 @@ def run_output(cmd, **kwargs):
}


class PlatformError(Exception):

def __init__(self):
super().__init__(
'{0} is not supported, please log a bug at '
'https://bugs.launchpad.net/snapcraft/+filebug?'
'field.title=please+add+support+for+{0}'.format(
platform.machine()))


def get_arch():
try:
return _DEB_TRANSLATIONS[platform.machine()]['arch']
except KeyError:
raise EnvironmentError(
'{} is not supported, please log a bug at'
'https://bugs.launchpad.net/snapcraft'.format(platform.machine()))
raise PlatformError()


def get_arch_triplet():
try:
return _DEB_TRANSLATIONS[platform.machine()]['triplet']
except KeyError:
raise EnvironmentError(
'{} is not supported, please log a bug at'
'https://bugs.launchpad.net/snapcraft'.format(platform.machine()))
raise PlatformError()


def format_snap_name(snap):
snap['arch'] = (snap['architectures'][0]
if len(snap['architectures']) == 1 else 'multi')
return '{name}_{version}_{arch}.snap'.format(**snap)


def get_partsdir():
Expand Down
105 changes: 105 additions & 0 deletions snapcraft/lxd.py
@@ -0,0 +1,105 @@
#!/usr/bin/python3
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2016 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import logging
import os
from contextlib import contextmanager
from subprocess import check_call, CalledProcessError
from time import sleep

import petname

from snapcraft.common import get_arch


logger = logging.getLogger(__name__)

_DEFAULT_IMAGE_SERVER = 'https://images.linuxcontainers.org:8443'
_NETWORK_PROBE_COMMAND = \
'import urllib.request; urllib.request.urlopen("{}", timeout=5)'.format(
'http://start.ubuntu.com/connectivity-check.html')
_PROXY_KEYS = ['http_proxy', 'https_proxy', 'no_proxy', 'ftp_proxy']


class Cleanbuilder:

def __init__(self, snap_output, project, server=_DEFAULT_IMAGE_SERVER):
self._snap_output = snap_output
self._project = project
self._container_name = 'snapcraft-{}'.format(
petname.Generate(3, '-'))
self._server = server

def _push_file(self, src, dst):
check_call(['lxc', 'file', 'push',
src, '{}/{}'.format(self._container_name, dst)])

def _pull_file(self, src, dst):
check_call(['lxc', 'file', 'pull',
'{}/{}'.format(self._container_name, src), dst])

def _container_run(self, cmd):
check_call(['lxc', 'exec', self._container_name, '--'] + cmd)

@contextmanager
def _create_container(self):
try:
remote_tmp = petname.Generate(2, '-')
check_call(['lxc', 'remote', 'add', remote_tmp, self._server])
check_call(['lxc', 'launch',
'{}:ubuntu/xenial/{}'.format(remote_tmp, get_arch()),
self._container_name])
yield
finally:
check_call(['lxc', 'stop', self._container_name])
check_call(['lxc', 'remote', 'remove', remote_tmp])

def execute(self):
with self._create_container():
self._setup_project()
self._wait_for_network()
self._container_run(['apt-get', 'update'])
self._container_run(['apt-get', 'install', 'snapcraft', '-y'])
self._container_run(
['snapcraft', 'snap', '--output', self._snap_output])
self._pull_snap()

def _setup_project(self):
logger.info('Setting up container with project assets')
dst = os.path.join('/root', os.path.basename(self._project))
self._push_file(self._project, dst)
self._container_run(['tar', 'xvf', dst])

def _pull_snap(self):
src = os.path.join('/root', self._snap_output)
self._pull_file(src, self._snap_output)
logger.info('Retrieved {}'.format(self._snap_output))

def _wait_for_network(self):
logger.info('Waiting for a network connection...')
not_connected = True
retry_count = 5
while not_connected:
sleep(5)
try:
self._container_run(['python3', '-c', _NETWORK_PROBE_COMMAND])
not_connected = False
except CalledProcessError as e:
retry_count -= 1
if retry_count == 0:
raise e
logger.info('Network connection established')
7 changes: 5 additions & 2 deletions snapcraft/main.py
@@ -1,7 +1,7 @@
#!/usr/bin/python3
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2015 Canonical Ltd
# Copyright (C) 2015, 2016 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
Expand Down Expand Up @@ -38,6 +38,7 @@
The available lifecycle commands are:
clean Remove content - cleans downloads, builds or install artifacts.
cleanbuild Create a snap using a clean environment managed by lxd.
pull Download or retrieve artifacts defined for a part.
build Build artifacts defined for a part.
stage Stage the part's built artifacts into the common staging area.
Expand All @@ -53,6 +54,7 @@

import pkg_resources
import sys
import textwrap

from docopt import docopt

Expand All @@ -69,6 +71,7 @@
'pull',
'build',
'clean',
'cleanbuild',
'stage',
'strip',
'snap',
Expand All @@ -95,7 +98,7 @@ def main():
try:
commands.load(args['COMMAND']).main(argv=args['ARGS'])
except Exception as e:
sys.exit(e)
sys.exit(textwrap.fill(str(e)))


if __name__ == '__main__':
Expand Down

0 comments on commit 5e527fb

Please sign in to comment.