diff -Nru snapcraft-2.34/bin/snapcraft-classic snapcraft-2.35/bin/snapcraft-classic --- snapcraft-2.34/bin/snapcraft-classic 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/bin/snapcraft-classic 2017-11-01 19:41:33.000000000 +0000 @@ -1,21 +1,5 @@ #!/bin/sh -case $SNAP_ARCH in -amd64) - TRIPLET="x86_64-linux-gnu" - ;; -armhf) - TRIPLET="arm-linux-gnueabihf" - ;; -arm64) - TRIPLET="aarch64-linux-gnu" - ;; -*) - TRIPLET="$(uname -p)-linux-gnu" - ;; -esac - -export LD_LIBRARY_PATH=$SNAP/usr/lib:$SNAP/usr/lib/$TRIPLET export MAGIC=$SNAP/usr/share/file/magic.mgc exec $SNAP/usr/bin/python3 $SNAP/bin/snapcraft "$@" diff -Nru snapcraft-2.34/CODE_STYLE.md snapcraft-2.35/CODE_STYLE.md --- snapcraft-2.34/CODE_STYLE.md 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/CODE_STYLE.md 2017-11-01 19:41:33.000000000 +0000 @@ -15,6 +15,19 @@ We adhere to the Style Guide for Python Code documented in the [PEP 8][2]. +## Multiline strings + +For multiline strings, we prefer to use `textwrap.dedent`: + + # end first line with \ to avoid the empty line! + s = textwrap.dedent("""\ + hello + world + """) + print(repr(s)) # prints 'hello\n world\n' + +(from https://docs.python.org/3/library/textwrap.html#textwrap.dedent) + ## Tests * When asserting for equality, we prefer to use the `Equals` matcher from @@ -25,4 +38,4 @@ ``` [1]: TESTING.md -[2]: https://www.python.org/dev/peps/pep-0008 \ No newline at end of file +[2]: https://www.python.org/dev/peps/pep-0008 diff -Nru snapcraft-2.34/debian/changelog snapcraft-2.35/debian/changelog --- snapcraft-2.34/debian/changelog 2017-09-11 14:12:19.000000000 +0000 +++ snapcraft-2.35/debian/changelog 2017-11-01 19:41:33.000000000 +0000 @@ -1,3 +1,162 @@ +snapcraft (2.35) xenial; urgency=medium + + [ Sergio Schvezov ] + * pluginhandler: error out on scriptlet errors + * meta: ensure main keys are ordered in snap.yaml + * demos: remove the py?-project demos + * store: switch to new endpoints + * static: fix flake8 errors in setup.py + * docker: add the environment variable to setup core (#1576) + * cli: add the pack command (#1565) + * store: handle revoked developers (#1554) + * lifecycle: split into its own package (#1626) + * libraries: exclude the full set of libc6 (#1632) + * lxd: better surfacing of errors (#1647) + * tests: fork skip into snaps_tests (#1652) + * sources: use arfile to extract debs (#1729) + * tests: dotnet only works on 16.04 (#1732) + * unit tests: make the check for output less strict (#1738) + * New upstream release (LP: #1729417) + + [ Kyle Fazzari ] + * project_loader: aliases are deprecated + * catkin plugin: don't assume catkin is in underlay + * catkin plugin: only append PYTHONPATH if set + * plugins: extract python finder functions + * plugins: extract sitecustomize logic from python + * dirs: set plugin, schema, and library dir for snap + * plugins: extract pip from python plugin + * catkin plugin: support rosdep pip dependencies (#1581) + * plugins: add ros2 boostrapper (#1582) + * travis: run snapd tests only if not cron (#1592) + * snapcraft.yaml: don't re-use build dir (#1601) + * schema: improve invalid app, hook, and part errors (#1615) + * plugins: build-attributes is already in the state (#1620) + * tests: skip catkin test on non-xenial (#1621) + * tests: don't hit internet in ros2 units (#1619) + * states: add scriptlets to build state (#1618) + * schema: sync patterns with snapd (#1622) + * snap: remove leaking LD_LIBRARY_PATH (#1635) + * store: guide to account creation upon login (#1616) + * repo: add elementary to deb distros (#1637) + * internal: more gracefully determine host OS (#1636) + * integration tests: skip shared ROS test on non-xenial (#1656) + * internal: don't reuse variable in OsRelease (#1653) + * integration tests: remove ruby version (#1727) + * unit tests: reset log level after test (#1735) + * autotools: cross-compile using --host instead of env (#1654) + * catkin plugin: check for pip packages in part only (#1717) + * ruby plugin: be smarter about arch-specific paths (#1730) + * demo tests: bump catkin timeout by a lot (#1731) + * many: account for python shebang args in rewrite + + [ Leo Arias ] + * typo: replace occured with occurred + * node plugin: record installed node packages in manifest + * node plugin: record the yarn.lock file + * tests: fix the TEST_STORE environment variable + * tests: add integration tests for build snaps + * recording: record the machine information collected by uname + * tests: add unit tests for the ruby plugin + * recording: record the packages installed in the machine + * tests: simplify a little the data in nodejs unit tests + * ci: use travis conditionals + * recording: record build-snaps installed during the pull + * tests: replace the first batch of demo tests with snapd integration tests + * rust plugin: record the Cargo.lock file + * rust plugin: record the versions of rustup, rustc and cargo + * tests: move ruby demo test to snapd integration suite (#1596) + * recording: record the snaps installed on the machine (#1567) + * style: use dedent for multiline strings (#1584) + * tests: refactor the fake snapd to not hardcode values (#1569) + * code style: remove the extra quotes in the dedent example (#1594) + * recording: do not crash when snapd is not installed (#1598) + * tests: fix the skip of snapd integration tests in armhf (#1595) + * tests: fix the duplicate plainbox test scenarios (#1608) + * tests: reenable the cleanbuild integration test (#1610) + * tests: remove the duplicate nodejs integration tests (#1609) + * tests: add /snap/bin to PATH in autopkgtests (#1603) + * tests: add the slow tag for ros snapd integration test (#1602) + * lxd: fix the unit test for the user id map (#1629) + * tests: allow to select a suite when running autopkgtests (#1630) + * tests: use the snapcraft snap for the integration tests (#1625) + * tests: move the plainbox test to the integration suite (#1642) + * lxd: fix the push in container builds (#1644) + * tests: use the common base handler on the fake snapd server (#1724) + * tests: split the integration autopkgtests (#1716) + * tests: in autopkgtests, use a tempdir in home, not in the tmpfs (#1657) + * recording: record information from the image in container builds (#1633) + * recording: pass the build info flag to the container (#1736) + * tests: add the home plug to the plainbox snap (#1740) + + [ Christian Dywan ] + * lxd: mount project folder via sshfs in case of a remote (#1302) + * lxd: Only pass target arch if specified explicitly + * repo: friendly, helpful error for unsupported distros (#1586) + * lxd: instructions for /etc/sub{u,g}id after failed start (#1553) + * lxd: pass SNAPCRAFT_PARTS_URI through into container (#1585) + * lxd: use SUDO_UID for ID mapping (#1588) + * lxd: don't inject local snaps on a different arch (#1577) + * lxd: don't re-inject the same snaps (#1568) + * lxd: split container classes into different files (#1627) + * cli: update parts cache in the container (#1546) + * lifecycle: clean after deleting container (#1587) + * lxd: snapcraft refresh in containers (#1412) + * lxd: distinguish argless clean from clean -s pull (#1655) + * lxd: refresh remote container (#1739) + * cli: pass remote from container_config to clean method (#1737) + + [ Mark Lee ] + * project_loader: quote more environment variable values (#1578) + + [ James Beedy ] + * ruby plugin: new plugin + + [ Aleix Pol ] + * repo: return a proper value in DummyRepo + * common: do not fail over on empty or faulty lines in os-release + + [ Jeff Dickey ] + * ci: install git in Dockerfile for '{version: git}' usage (#1575) + + [ Michael Vogt ] + * meta: add and adapter property for apps + + [ Chris Ratliff ] + * catkin plugin: allow ROS_MASTER_URI change (#1572) + + [ Rakesh Singh ] + * dotnet plugin: new plugin (#1574) + + [ Martin Wimpress ] + * nodejs plugin: update default node engine to 6.11.4 (#1589) + + [ Colin Watson ] + * options: fix core-dynamic-linker on ppc64el/s390x (#1600) + + [ Paolo Pisati ] + * kbuild plugin: if the parts build dir already contains a .config file, return immediately (#1606) + * cross compilation: enable cross compilation of i386 kernel on x86-64 … (#1613) + + [ Jonathan Cave ] + * plainbox-provider plugin: init PROVIDERPATH (#1611) + + [ Carlo Lobrano ] + * store: fix StoreReleaseError format for BAD REQUEST error (#1599) + * sources: get svn revision with --show-item flag (#1648) + * Removed dependency on VERSION_ID in os-release (#1623) + + [ Alfonso Sanchez Beato ] + * kernel plugin: use latest stable core snap (#1624) + + [ Nathan Haines ] + * ruby plugin: new stable release 2.4.2 (#1645) + + [ Marius Gripsgard ] + * sources: workaround for ZipFile.extractall not preserving permissions (#1723) + + -- Sergio Schvezov Wed, 01 Nov 2017 19:41:33 +0000 + snapcraft (2.34) xenial; urgency=medium [ Sergio Schvezov ] diff -Nru snapcraft-2.34/debian/tests/control snapcraft-2.35/debian/tests/control --- snapcraft-2.34/debian/tests/control 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/debian/tests/control 2017-11-01 19:41:33.000000000 +0000 @@ -1,4 +1,4 @@ -Tests: integrationtests +Tests: integrationtests-general, integrationtests-store, integrationtests-plugins, integrationtests-snapd Restrictions: needs-root, allow-stderr, isolation-container, rw-build-tree Depends: @, bzr, diff -Nru snapcraft-2.34/debian/tests/integrationtests snapcraft-2.35/debian/tests/integrationtests --- snapcraft-2.34/debian/tests/integrationtests 2017-09-10 17:25:47.000000000 +0000 +++ snapcraft-2.35/debian/tests/integrationtests 2017-11-01 19:41:33.000000000 +0000 @@ -4,6 +4,17 @@ echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers.d/autopkgtest -su ubuntu -c "SNAPCRAFT_FROM_INSTALLED=1 TEST_STORE=fake ADT_TEST=1 python3 -m unittest discover -b -v -s integration_tests" -su ubuntu -c "SNAPCRAFT_FROM_INSTALLED=1 TEST_STORE=fake ADT_TEST=1 python3 -m unittest discover -b -v -s integration_tests/store" -su ubuntu -c "SNAPCRAFT_FROM_INSTALLED=1 TEST_STORE=fake ADT_TEST=1 python3 -m unittest discover -b -v -s integration_tests/plugins" +if [ -z "$SNAPCRAFT_AUTOPKGTEST_SUITES" ]; then + suites="integration_tests integration_tests/store integration_tests/plugins integration_tests/snapd" +else + suites=$SNAPCRAFT_AUTOPKGTEST_SUITES +fi + +if [ -z "$SNAPCRAFT_SLOW_TESTS" ]; then + SNAPCRAFT_SLOW_TESTS=1 +fi + +mkdir --parents /home/ubuntu/autopkgtest_tmp --mode 777 +for suite in $suites; do + su ubuntu -c "SNAPCRAFT_FROM_INSTALLED=1 SNAPCRAFT_SLOW_TESTS=${SNAPCRAFT_SLOW_TESTS} TEST_STORE=fake ADT_TEST=1 PATH=/snap/bin:$PATH TMPDIR=\$HOME/autopkgtest_tmp python3 -m unittest discover -b -v -s ${suite}" +done diff -Nru snapcraft-2.34/debian/tests/integrationtests-general snapcraft-2.35/debian/tests/integrationtests-general --- snapcraft-2.34/debian/tests/integrationtests-general 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/debian/tests/integrationtests-general 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e + +script_path="$(dirname "$0")" +SNAPCRAFT_AUTOPKGTEST_SUITES=integration_tests $script_path/integrationtests diff -Nru snapcraft-2.34/debian/tests/integrationtests-plugins snapcraft-2.35/debian/tests/integrationtests-plugins --- snapcraft-2.34/debian/tests/integrationtests-plugins 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/debian/tests/integrationtests-plugins 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e + +script_path="$(dirname "$0")" +SNAPCRAFT_AUTOPKGTEST_SUITES=integration_tests/plugins $script_path/integrationtests diff -Nru snapcraft-2.34/debian/tests/integrationtests-snapd snapcraft-2.35/debian/tests/integrationtests-snapd --- snapcraft-2.34/debian/tests/integrationtests-snapd 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/debian/tests/integrationtests-snapd 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e + +script_path="$(dirname "$0")" +SNAPCRAFT_AUTOPKGTEST_SUITES=integration_tests/snapd $script_path/integrationtests diff -Nru snapcraft-2.34/debian/tests/integrationtests-store snapcraft-2.35/debian/tests/integrationtests-store --- snapcraft-2.34/debian/tests/integrationtests-store 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/debian/tests/integrationtests-store 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e + +script_path="$(dirname "$0")" +SNAPCRAFT_AUTOPKGTEST_SUITES=integration_tests/store $script_path/integrationtests diff -Nru snapcraft-2.34/demos/godd/snap/snapcraft.yaml snapcraft-2.35/demos/godd/snap/snapcraft.yaml --- snapcraft-2.34/demos/godd/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/godd/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -name: godd -version: 1.0 -summary: Simple dd like tool -description: - Written in go with support for device auto-detection via libgudev, - you would need to use hw-assign to access devices. -confinement: strict - -apps: - godd: - command: bin/godd - plugs: [mount-observe] - -parts: - godd: - plugin: go - source: https://github.com/mvo5/godd - source-type: git - go-importpath: github.com/mvo5/godd - build-packages: [gcc, libgudev-1.0-dev] diff -Nru snapcraft-2.34/demos/gopaste/gopaste snapcraft-2.35/demos/gopaste/gopaste --- snapcraft-2.34/demos/gopaste/gopaste 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/gopaste/gopaste 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ -#!/bin/sh - -set -x - -cd $SNAP_DATA -cp $SNAP/web.template . -cp -r $SNAP/static . -export HOME=$SNAP_DATA - -exec $SNAP/bin/gopasted -port 8080 -external-host snappy diff -Nru snapcraft-2.34/demos/gopaste/snap/snapcraft.yaml snapcraft-2.35/demos/gopaste/snap/snapcraft.yaml --- snapcraft-2.34/demos/gopaste/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/gopaste/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -name: gopaste -version: 1.0 -summary: Simple pasting tool -description: Runs a service that allows you to paste to and share. -confinement: strict - -apps: - gopaste: - command: bin/gopaste - daemon: simple - plugs: - - network - - network-bind - -parts: - gopaste: - plugin: go - source: https://github.com/wisnij/gopaste.git - source-type: git - assets: - plugin: dump - source: https://github.com/wisnij/gopaste.git - source-type: git - snap: - - static - - web.template - glue: - plugin: dump - source: . - organize: - gopaste: bin/ - filesets: - bin: [bin/gopaste] - stage: [$bin] - snap: [$bin] diff -Nru snapcraft-2.34/demos/libpipeline/Makefile snapcraft-2.35/demos/libpipeline/Makefile --- snapcraft-2.34/demos/libpipeline/Makefile 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/libpipeline/Makefile 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -# -*- Mode:Makefile; indent-tabs-mode:t; tab-width:4 -*- - -all: - gcc -o test ./test.c $(CFLAGS) $(LDFLAGS) -lpipeline - -install: - install -d -m755 $(DESTDIR)/bin/ - install -m755 ./test $(DESTDIR)/bin/test - -clean: - rm -f test diff -Nru snapcraft-2.34/demos/libpipeline/snap/snapcraft.yaml snapcraft-2.35/demos/libpipeline/snap/snapcraft.yaml --- snapcraft-2.34/demos/libpipeline/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/libpipeline/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -name: pipelinetest -version: 1.0 -summary: Libpipeline example -description: | - This is an example package of an autotools project built with snapcraft - using a remote source. -confinement: strict - -apps: - pipelinetest: - command: ./bin/test - -parts: - pipelinetest: - plugin: make - source: . - after: - - libpipeline - libpipeline: - plugin: autotools - source: lp:~mterry/libpipeline/printf diff -Nru snapcraft-2.34/demos/libpipeline/test.c snapcraft-2.35/demos/libpipeline/test.c --- snapcraft-2.34/demos/libpipeline/test.c 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/libpipeline/test.c 1970-01-01 00:00:00.000000000 +0000 @@ -1,18 +0,0 @@ -#include -#include - -int main() -{ - pipeline *p; - int status; - - printf("running echo test | grep s | grep t\n"); - - p = pipeline_new (); - pipeline_command_args (p, "echo", "test", NULL); - pipeline_command_args (p, "grep", "s", NULL); - pipeline_command_args (p, "grep", "t", NULL); - status = pipeline_run (p); - - return status; -} diff -Nru snapcraft-2.34/demos/mosquitto/conf/mosquitto.conf snapcraft-2.35/demos/mosquitto/conf/mosquitto.conf --- snapcraft-2.34/demos/mosquitto/conf/mosquitto.conf 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/mosquitto/conf/mosquitto.conf 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -user root diff -Nru snapcraft-2.34/demos/mosquitto/launchers/publish.py snapcraft-2.35/demos/mosquitto/launchers/publish.py --- snapcraft-2.34/demos/mosquitto/launchers/publish.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/mosquitto/launchers/publish.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,37 +0,0 @@ -#!/usr/bin/env python3 - -import argparse - -from paho.mqtt import client as mqtt_client - - -DEFAULT_HOST = 'localhost' -DEFAULT_PORT = 1883 - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument( - 'host', nargs='?', default=DEFAULT_HOST, - help=('The IP or hostname of the MQTT server. ' - 'Defaults to {}.'.format(DEFAULT_HOST))) - parser.add_argument( - 'port', type=int, nargs='?', default=DEFAULT_PORT, - help=('The port of the MQTT server. ' - 'Defaults to {}.'.format(DEFAULT_PORT))) - parser.add_argument('topic', help='The topic to publish to.') - parser.add_argument('payload', help='The payload to send to the topic.') - args = parser.parse_args() - - client = mqtt_client.Client() - client.connect(args.host, args.port) - - try: - client.loop_start() - client.publish(args.topic, args.payload) - finally: - client.loop_stop() - - -if __name__ == "__main__": - main() diff -Nru snapcraft-2.34/demos/mosquitto/launchers/subscribe.py snapcraft-2.35/demos/mosquitto/launchers/subscribe.py --- snapcraft-2.34/demos/mosquitto/launchers/subscribe.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/mosquitto/launchers/subscribe.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import os -import sys - -from paho.mqtt import client as mqtt_client - - -DEFAULT_HOST = 'localhost' -DEFAULT_PORT = 1883 - -topic = '' - - -def on_connect(client, userdata, unused1, unused2): - # Ignore the unused arguments. - del unused1, unused2 - _log('MQTT subscriber connected.') - client.subscribe(topic) - - -def on_message(unused1, unused2, message): - # Ignore the unused arguments. - del unused1, unused2 - _log(message.topic + ' ' + str(message.payload)) - if message.payload == b'exit': - # XXX Exit on response to a received message simplifyies the tests - # --elopio - 2017-03-04 - sys.exit(0) - - -def _log(message): - print(message) - log_file_path = os.path.join( - os.getenv('SNAP_USER_DATA'), 'mosquitto.subscriber.log') - with open(log_file_path, 'a') as log_file: - log_file.write(message + '\n') - - -def main(): - parser = argparse.ArgumentParser() - parser.add_argument( - 'host', nargs='?', default=DEFAULT_HOST, - help=('The IP or hostname of the MQTT server. ' - 'Defaults to {}.'.format(DEFAULT_HOST))) - parser.add_argument( - 'port', type=int, nargs='?', default=DEFAULT_PORT, - help=('The port of the MQTT server. ' - 'Defaults to {}.'.format(DEFAULT_PORT))) - parser.add_argument('topic', help='The topic to subscribe to.') - args = parser.parse_args() - - client = mqtt_client.Client() - client.on_connect = on_connect - client.on_message = on_message - - global topic - topic = args.topic - client.connect(args.host, args.port) - client.loop_forever() - - -if __name__ == "__main__": - main() diff -Nru snapcraft-2.34/demos/mosquitto/snap/snapcraft.yaml snapcraft-2.35/demos/mosquitto/snap/snapcraft.yaml --- snapcraft-2.34/demos/mosquitto/snap/snapcraft.yaml 2017-09-10 17:25:47.000000000 +0000 +++ snapcraft-2.35/demos/mosquitto/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,31 +0,0 @@ -name: mosquitto -version: 0.1 -summary: mosquitto server and client -description: MQTT example with a server, a publisher and a subscriber. -confinement: strict - -apps: - mosquitto: - command: usr/sbin/mosquitto -c $SNAP/mosquitto.conf - daemon: simple - plugs: [network, network-bind] - subscribe: - command: bin/subscribe - plugs: [network, network-bind] - publish: - command: bin/publish - plugs: [network, network-bind] - - -parts: - mosquitto: - plugin: dump - source: conf - stage-packages: [mosquitto] - mqtt-client: - plugin: dump - source: launchers - organize: - subscribe.py: bin/subscribe - publish.py: bin/publish - after: [mqtt-paho-python3] diff -Nru snapcraft-2.34/demos/plainbox-test-tool/2016.com.example_simple/manage.py snapcraft-2.35/demos/plainbox-test-tool/2016.com.example_simple/manage.py --- snapcraft-2.34/demos/plainbox-test-tool/2016.com.example_simple/manage.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/plainbox-test-tool/2016.com.example_simple/manage.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,11 +0,0 @@ -#!/usr/bin/env python3 -from plainbox.provider_manager import setup, N_ - - -setup( - name='plainbox-provider-simple', - namespace='2016.com.example', - version="1.0", - description=N_("A really simple Plainbox provider"), - gettext_domain="2016_com_example_simple", -) diff -Nru snapcraft-2.34/demos/plainbox-test-tool/2016.com.example_simple/units/simple.pxu snapcraft-2.35/demos/plainbox-test-tool/2016.com.example_simple/units/simple.pxu --- snapcraft-2.34/demos/plainbox-test-tool/2016.com.example_simple/units/simple.pxu 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/plainbox-test-tool/2016.com.example_simple/units/simple.pxu 1970-01-01 00:00:00.000000000 +0000 @@ -1,32 +0,0 @@ - -unit: category -id: simple -_name: Simple - -unit: job -id: always-pass -category_id: simple -_summary: A test that always passes -_description: - A test that always passes - . - This simple test will always succeed, assuming your - platform has a 'true' command that returns 0. -plugin: shell -estimated_duration: 0.01 -command: true -flags: preserve-locale - -unit: job -id: always-fail -category_id: simple -_summary: A test that always fails -_description: - A test that always fails - . - This simple test will always fail, assuming your - platform has a 'false' command that returns 1. -plugin: shell -estimated_duration: 0.01 -command: false -flags: preserve-locale diff -Nru snapcraft-2.34/demos/plainbox-test-tool/launchers/plainbox-wrapper snapcraft-2.35/demos/plainbox-test-tool/launchers/plainbox-wrapper --- snapcraft-2.34/demos/plainbox-test-tool/launchers/plainbox-wrapper 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/plainbox-test-tool/launchers/plainbox-wrapper 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -#!/bin/sh -export PATH="$PATH:$SNAP/usr/sbin" -exec python3 $(which plainbox) "$@" diff -Nru snapcraft-2.34/demos/plainbox-test-tool/snap/snapcraft.yaml snapcraft-2.35/demos/plainbox-test-tool/snap/snapcraft.yaml --- snapcraft-2.34/demos/plainbox-test-tool/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/plainbox-test-tool/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,40 +0,0 @@ -name: plainbox-test-tool -summary: A Plainbox Test Tool -description: > - The Plainbox tool itself coupled with a Provider which contains the test - definitions -version: 0.1 -confinement: strict - -apps: - plainbox: - command: bin/plainbox-wrapper - -parts: - # The testing framework - plainbox-local: - plugin: python3 - python-packages: - - plainbox - - requests-oauthlib - - xlsxwriter - build-packages: - - libxml2-dev - - libxslt1-dev - - zlib1g-dev - - build-essential - # The test definitions - simple-plainbox-provider: - plugin: plainbox-provider - source: ./2016.com.example_simple - after: [plainbox-local] - # A wrapper script - launchers: - plugin: dump - source: . - organize: - launchers/plainbox-wrapper: bin/plainbox-wrapper - filesets: - wrapper: [bin/plainbox-wrapper] - stage: [$wrapper] - snap: [$wrapper] diff -Nru snapcraft-2.34/demos/py2-project/Makefile snapcraft-2.35/demos/py2-project/Makefile --- snapcraft-2.34/demos/py2-project/Makefile 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/py2-project/Makefile 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -# -*- Mode: Makefile; indent-tabs-mode:t; tab-width: 4 -*- - -all: - -install: - mkdir -p $(DESTDIR)/bin - cp -a sha3sum.py $(DESTDIR)/bin/sha3sum - chmod a+x $(DESTDIR)/bin/sha3sum diff -Nru snapcraft-2.34/demos/py2-project/sha3sum.py snapcraft-2.35/demos/py2-project/sha3sum.py --- snapcraft-2.34/demos/py2-project/sha3sum.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/py2-project/sha3sum.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -#!/usr/bin/env python2 - -import sys - -import spongeshaker.sha3 - - -if __name__ == '__main__': - # 224 is the default from sha3sum - h = spongeshaker.sha3.sha3_224() - with open(sys.argv[1], 'rb') as fp: - data = fp.read() - h.update(data) - print(h.hexdigest()) diff -Nru snapcraft-2.34/demos/py2-project/snap/snapcraft.yaml snapcraft-2.35/demos/py2-project/snap/snapcraft.yaml --- snapcraft-2.34/demos/py2-project/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/py2-project/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -name: spongeshaker -version: 0 -summary: A python sha3 implementation -description: A python2 project using snapcraft -confinement: strict - -apps: - sha3sum: - command: ./bin/sha3sum - -parts: - spongeshaker: - plugin: python2 - source: https://github.com/markokr/spongeshaker.git - source-type: git - build-packages: [gcc, libc6-dev] - make-project: - plugin: make - source: . diff -Nru snapcraft-2.34/demos/py3-project/sha3sum.py snapcraft-2.35/demos/py3-project/sha3sum.py --- snapcraft-2.34/demos/py3-project/sha3sum.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/py3-project/sha3sum.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -import sys - -import spongeshaker.sha3 - - -if __name__ == '__main__': - # 224 is the default from sha3sum - h = spongeshaker.sha3.sha3_224() - with open(sys.argv[1], 'rb') as fp: - data = fp.read() - h.update(data) - print(h.hexdigest()) diff -Nru snapcraft-2.34/demos/py3-project/snap/snapcraft.yaml snapcraft-2.35/demos/py3-project/snap/snapcraft.yaml --- snapcraft-2.34/demos/py3-project/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/py3-project/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -name: spongeshaker -version: 0 -summary: A python sha3 implementation -description: A python3 project using snapcraft -confinement: strict - -apps: - sha3sum: - command: bin/sha3sum - -parts: - spongeshaker: - plugin: python3 - source: https://github.com/markokr/spongeshaker.git - source-type: git - build-packages: [gcc, libc6-dev] - sha3: - plugin: dump - source: . - organize: - sha3sum.py: bin/sha3sum diff -Nru snapcraft-2.34/demos/ros/snap/snapcraft.yaml snapcraft-2.35/demos/ros/snap/snapcraft.yaml --- snapcraft-2.34/demos/ros/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/ros/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -name: ros-example -version: 1.0 -summary: ROS Example -description: Contains talker/listener ROS packages and a .launch file. -confinement: strict - -apps: - launch-project: - command: roslaunch listener talk_and_listen.launch - plugs: [network-bind] - -parts: - ros-project: - plugin: catkin - source: . - catkin-packages: - - talker - - listener - include-roscore: true diff -Nru snapcraft-2.34/demos/ros/src/CMakeLists.txt snapcraft-2.35/demos/ros/src/CMakeLists.txt --- snapcraft-2.34/demos/ros/src/CMakeLists.txt 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/ros/src/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,47 +0,0 @@ -# toplevel CMakeLists.txt for a catkin workspace -# catkin/cmake/toplevel.cmake - -cmake_minimum_required(VERSION 2.8.3) - -# optionally provide a cmake file in the workspace to override arbitrary stuff -include(workspace.cmake OPTIONAL) - -set(CATKIN_TOPLEVEL TRUE) - -# include catkin directly or via find_package() -if(EXISTS "${CMAKE_SOURCE_DIR}/catkin/cmake/all.cmake" AND EXISTS "${CMAKE_SOURCE_DIR}/catkin/CMakeLists.txt") - set(catkin_EXTRAS_DIR "${CMAKE_SOURCE_DIR}/catkin/cmake") - # include all.cmake without add_subdirectory to let it operate in same scope - include(catkin/cmake/all.cmake NO_POLICY_SCOPE) - add_subdirectory(catkin) - -else() - # use either CMAKE_PREFIX_PATH explicitly passed to CMake as a command line argument - # or CMAKE_PREFIX_PATH from the environment - if(NOT DEFINED CMAKE_PREFIX_PATH) - if(NOT "$ENV{CMAKE_PREFIX_PATH}" STREQUAL "") - string(REPLACE ":" ";" CMAKE_PREFIX_PATH $ENV{CMAKE_PREFIX_PATH}) - endif() - endif() - - # list of catkin workspaces - set(catkin_search_path "") - foreach(path ${CMAKE_PREFIX_PATH}) - if(EXISTS "${path}/.CATKIN_WORKSPACE") - list(FIND catkin_search_path ${path} _index) - if(_index EQUAL -1) - list(APPEND catkin_search_path ${path}) - endif() - endif() - endforeach() - - # search for catkin in all workspaces - set(CATKIN_TOPLEVEL_FIND_PACKAGE TRUE) - find_package(catkin REQUIRED - NO_POLICY_SCOPE - PATHS ${catkin_search_path} - NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) - unset(CATKIN_TOPLEVEL_FIND_PACKAGE) -endif() - -catkin_workspace() diff -Nru snapcraft-2.34/demos/ros/src/listener/CMakeLists.txt snapcraft-2.35/demos/ros/src/listener/CMakeLists.txt --- snapcraft-2.34/demos/ros/src/listener/CMakeLists.txt 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/ros/src/listener/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -cmake_minimum_required(VERSION 2.8.3) -project(listener) - -find_package(catkin REQUIRED COMPONENTS - rospy - std_msgs -) - -catkin_package() - -install(PROGRAMS - scripts/listener_node - DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} -) - -install(FILES - talk_and_listen.launch - DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} -) diff -Nru snapcraft-2.34/demos/ros/src/listener/package.xml snapcraft-2.35/demos/ros/src/listener/package.xml --- snapcraft-2.34/demos/ros/src/listener/package.xml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/ros/src/listener/package.xml 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ - - - listener - 0.0.0 - The listener package - me - GPLv3 - catkin - rospy - std_msgs - rospy - std_msgs - diff -Nru snapcraft-2.34/demos/ros/src/listener/scripts/listener_node snapcraft-2.35/demos/ros/src/listener/scripts/listener_node --- snapcraft-2.34/demos/ros/src/listener/scripts/listener_node 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/ros/src/listener/scripts/listener_node 1970-01-01 00:00:00.000000000 +0000 @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -import rospy -from std_msgs.msg import String - - -def callback(data): - rospy.loginfo('I heard %s', data.data) - - if rospy.get_param('~exit-after-receive', False): - rospy.signal_shutdown( - 'Requested to exit after message received. Exiting now.') - - -def listener(): - rospy.init_node('listener') - - rospy.Subscriber('babble', String, callback) - - rospy.spin() - -if __name__ == '__main__': - listener() diff -Nru snapcraft-2.34/demos/ros/src/listener/talk_and_listen.launch snapcraft-2.35/demos/ros/src/listener/talk_and_listen.launch --- snapcraft-2.34/demos/ros/src/listener/talk_and_listen.launch 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/ros/src/listener/talk_and_listen.launch 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ - - - - - - - - - - diff -Nru snapcraft-2.34/demos/ros/src/talker/CMakeLists.txt snapcraft-2.35/demos/ros/src/talker/CMakeLists.txt --- snapcraft-2.34/demos/ros/src/talker/CMakeLists.txt 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/ros/src/talker/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -cmake_minimum_required(VERSION 2.8.3) -project(talker) - -find_package(catkin REQUIRED COMPONENTS - roscpp - std_msgs -) - -catkin_package() - -include_directories(${catkin_INCLUDE_DIRS}) - -add_executable(talker_node src/talker_node.cpp) - -target_link_libraries(talker_node ${catkin_LIBRARIES}) - -install(TARGETS talker_node - ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} - LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} - RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} -) diff -Nru snapcraft-2.34/demos/ros/src/talker/package.xml snapcraft-2.35/demos/ros/src/talker/package.xml --- snapcraft-2.34/demos/ros/src/talker/package.xml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/ros/src/talker/package.xml 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ - - - talker - 0.0.0 - The talker package - me - GPLv3 - catkin - roscpp - std_msgs - roscpp - std_msgs - diff -Nru snapcraft-2.34/demos/ros/src/talker/src/talker_node.cpp snapcraft-2.35/demos/ros/src/talker/src/talker_node.cpp --- snapcraft-2.34/demos/ros/src/talker/src/talker_node.cpp 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/ros/src/talker/src/talker_node.cpp 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -#include - -#include -#include - -int main(int argc, char **argv) -{ - ros::init(argc, argv, "talker"); - - ros::NodeHandle nodeHandle; - - ros::Publisher publisher = nodeHandle.advertise("chatter", 1); - - ros::Rate loopRate(10); - - int count = 0; - while (ros::ok()) - { - std_msgs::String message; - - std::stringstream stream; - stream << "Hello world " << count++; - message.data = stream.str(); - - ROS_INFO("%s", message.data.c_str()); - - publisher.publish(message); - - ros::spinOnce(); - - loopRate.sleep(); - } - - return 0; -} diff -Nru snapcraft-2.34/demos/rosinstall/ros_tutorials.rosinstall snapcraft-2.35/demos/rosinstall/ros_tutorials.rosinstall --- snapcraft-2.34/demos/rosinstall/ros_tutorials.rosinstall 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/rosinstall/ros_tutorials.rosinstall 1970-01-01 00:00:00.000000000 +0000 @@ -1,4 +0,0 @@ -- git: - local-name: ros_tutorials - uri: https://github.com/ros/ros_tutorials.git - version: kinetic-devel diff -Nru snapcraft-2.34/demos/rosinstall/snap/snapcraft.yaml snapcraft-2.35/demos/rosinstall/snap/snapcraft.yaml --- snapcraft-2.34/demos/rosinstall/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/rosinstall/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,22 +0,0 @@ -name: rosinstall-demo -version: '1.0' -summary: Demo using a rosinstall file. -description: | - Build Catkin packages contained in a repo from a rosinstall file. - -grade: stable -confinement: strict - -apps: - run: - command: roslaunch quittable_listener talk_and_listen.launch - plugs: [network, network-bind] - -parts: - rosinstall-part: - plugin: catkin - rosdistro: kinetic - rosinstall-files: [ros_tutorials.rosinstall] - catkin-packages: - - roscpp_tutorials - - quittable_listener diff -Nru snapcraft-2.34/demos/rosinstall/src/quittable_listener/CMakeLists.txt snapcraft-2.35/demos/rosinstall/src/quittable_listener/CMakeLists.txt --- snapcraft-2.34/demos/rosinstall/src/quittable_listener/CMakeLists.txt 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/rosinstall/src/quittable_listener/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -cmake_minimum_required(VERSION 2.8.3) -project(quittable_listener) - -find_package(catkin REQUIRED COMPONENTS - rospy - std_msgs -) - -catkin_package() - -install(PROGRAMS - scripts/listener_node - DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} -) - -install(FILES - talk_and_listen.launch - DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} -) diff -Nru snapcraft-2.34/demos/rosinstall/src/quittable_listener/package.xml snapcraft-2.35/demos/rosinstall/src/quittable_listener/package.xml --- snapcraft-2.34/demos/rosinstall/src/quittable_listener/package.xml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/rosinstall/src/quittable_listener/package.xml 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ - - - quittable_listener - 0.0.0 - The listener package - me - GPLv3 - catkin - rospy - std_msgs - rospy - std_msgs - diff -Nru snapcraft-2.34/demos/rosinstall/src/quittable_listener/scripts/listener_node snapcraft-2.35/demos/rosinstall/src/quittable_listener/scripts/listener_node --- snapcraft-2.34/demos/rosinstall/src/quittable_listener/scripts/listener_node 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/rosinstall/src/quittable_listener/scripts/listener_node 1970-01-01 00:00:00.000000000 +0000 @@ -1,23 +0,0 @@ -#!/usr/bin/env python - -import rospy -from std_msgs.msg import String - - -def callback(data): - rospy.loginfo('I heard %s', data.data) - - if rospy.get_param('~exit-after-receive', False): - rospy.signal_shutdown( - 'Requested to exit after message received. Exiting now.') - - -def listener(): - rospy.init_node('listener') - - rospy.Subscriber('babble', String, callback) - - rospy.spin() - -if __name__ == '__main__': - listener() diff -Nru snapcraft-2.34/demos/rosinstall/src/quittable_listener/talk_and_listen.launch snapcraft-2.35/demos/rosinstall/src/quittable_listener/talk_and_listen.launch --- snapcraft-2.34/demos/rosinstall/src/quittable_listener/talk_and_listen.launch 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/rosinstall/src/quittable_listener/talk_and_listen.launch 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ - - - - - - - - - - diff -Nru snapcraft-2.34/demos/scons/hello.c snapcraft-2.35/demos/scons/hello.c --- snapcraft-2.34/demos/scons/hello.c 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/scons/hello.c 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -/* hello.c */ -#include -int main(int argc, char* argv[]) -{ - printf("Hello world\n"); - return 0; -} diff -Nru snapcraft-2.34/demos/scons/SConstruct snapcraft-2.35/demos/scons/SConstruct --- snapcraft-2.34/demos/scons/SConstruct 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/scons/SConstruct 1970-01-01 00:00:00.000000000 +0000 @@ -1,12 +0,0 @@ -# SConstruct -import os - -env = Environment() - -destdir = os.environ.get("DESTDIR") -if destdir == None: - destdir = "" - -hello = env.Program(["hello.c"]) -env.Install(destdir + "/usr/bin", hello) -env.Alias('install', destdir + "/usr/bin") diff -Nru snapcraft-2.34/demos/scons/snap/snapcraft.yaml snapcraft-2.35/demos/scons/snap/snapcraft.yaml --- snapcraft-2.34/demos/scons/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/scons/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ -name: scons-hello -version: 0 -summary: A scons example -description: this is not much more than an example -confinement: strict -grade: stable - -apps: - scons-hello: - command: wrapper - -parts: - local: - plugin: scons - source: . - scons-options: [--debug=explain] - organize: - ../build/wrapper: bin/ - diff -Nru snapcraft-2.34/demos/scons/wrapper snapcraft-2.35/demos/scons/wrapper --- snapcraft-2.34/demos/scons/wrapper 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/scons/wrapper 1970-01-01 00:00:00.000000000 +0000 @@ -1,3 +0,0 @@ -#!/bin/bash - -$SNAP/usr/bin/hello diff -Nru snapcraft-2.34/demos/shared-ros/Makefile snapcraft-2.35/demos/shared-ros/Makefile --- snapcraft-2.34/demos/shared-ros/Makefile 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/demos/shared-ros/Makefile 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,16 @@ +all: ros-app/*.snap + +ros-base/*.snap: + cd ros-base && snapcraft + +ros-app/ros-base.tar.bz2: ros-base/*.snap + tar czf ros-app/ros-base.tar.bz2 -C ros-base stage/ + +ros-app/*.snap: ros-app/ros-base.tar.bz2 + cd ros-app && snapcraft + +.PHONY: clean +clean: + cd ros-base && snapcraft clean + cd ros-app && snapcraft clean + rm -f ros-app/*.tar.bz2 ros-app/*.snap ros-base/*.snap diff -Nru snapcraft-2.34/demos/shared-ros/README.md snapcraft-2.35/demos/shared-ros/README.md --- snapcraft-2.34/demos/shared-ros/README.md 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/shared-ros/README.md 2017-11-01 19:41:33.000000000 +0000 @@ -14,6 +14,8 @@ ## Build procedure +See the Makefile for explicit steps, but the general idea is: + 1. Build ros-base snap. 2. Tar ros-base staging area: `tar czf ros-base.tar.bz2 stage/` 3. Copy that tarball into ros-app (as required by its `ros-base` part). diff -Nru snapcraft-2.34/demos/shout/snap/snapcraft.yaml snapcraft-2.35/demos/shout/snap/snapcraft.yaml --- snapcraft-2.34/demos/shout/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/shout/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,18 +0,0 @@ -name: shout -version: 0.52.0 -summary: A self hosted web IRC client -description: This example is not really production quality -confinement: strict - -apps: - server: - command: bin/shout --home $SNAP_DATA - daemon: simple - plugs: [network, network-bind] - -parts: - shout: - plugin: nodejs - node-packages: - - shout - node-package-manager: 'yarn' diff -Nru snapcraft-2.34/demos/webchat/index.html snapcraft-2.35/demos/webchat/index.html --- snapcraft-2.34/demos/webchat/index.html 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/webchat/index.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,40 +0,0 @@ - - - - Snappy example chat - - - -
    -
    - - - -
    User:
    Message:
    -
    - - - - - - - diff -Nru snapcraft-2.34/demos/webchat/package.json snapcraft-2.35/demos/webchat/package.json --- snapcraft-2.34/demos/webchat/package.json 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/webchat/package.json 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ -{ - "name": "webchat-for-a-snap", - "version": "0.0.1", - "description": "Intended as a nodejs app in a snap", - "license": "GPL-3.0", - "author": "Sergio Schvezov ", - "private": true, - "bin": "./webchat.js", - "dependencies": { - "express": "^4.10.2", - "socket.io": "^1.3.7" - } -} diff -Nru snapcraft-2.34/demos/webchat/README.md snapcraft-2.35/demos/webchat/README.md --- snapcraft-2.34/demos/webchat/README.md 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/webchat/README.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,4 +0,0 @@ -# Snapcraft example - -This is a simple package to be *snapcrafted*. -It presents a simple chat system. diff -Nru snapcraft-2.34/demos/webchat/snap/snapcraft.yaml snapcraft-2.35/demos/webchat/snap/snapcraft.yaml --- snapcraft-2.34/demos/webchat/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/webchat/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ -name: webchat -version: 0.0.1 -summary: A simple nodejs based webchat -description: This example is not really production quality -confinement: strict - -apps: - webchat: - command: bin/webchat-for-a-snap - daemon: simple - plugs: [network-bind] - -parts: - webchat: - source: . - plugin: nodejs - node-package-manager: yarn diff -Nru snapcraft-2.34/demos/webchat/webchat.js snapcraft-2.35/demos/webchat/webchat.js --- snapcraft-2.34/demos/webchat/webchat.js 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/demos/webchat/webchat.js 1970-01-01 00:00:00.000000000 +0000 @@ -1,21 +0,0 @@ -#!/usr/bin/env node - -var app = require('express')(); -var http = require('http').Server(app); -var io = require('socket.io')(http); - -app.get('/', function(req, res){ - res.sendFile(__dirname + '/index.html'); -}); - - -io.on('connection', function(socket){ - socket.broadcast.emit('Hello snapcrafter'); - socket.on('chat message', function(user, msg){ - io.emit('chat message', user, msg); - }); -}); - -http.listen(3000, function(){ - console.log('listening on *:3000'); -}); diff -Nru snapcraft-2.34/Dockerfile snapcraft-2.35/Dockerfile --- snapcraft-2.34/Dockerfile 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/Dockerfile 2017-11-01 19:41:33.000000000 +0000 @@ -3,7 +3,15 @@ # Enable multiverse as snapcraft cleanbuild does. RUN sed -i 's/ universe/ universe multiverse/' /etc/apt/sources.list -RUN apt-get update && apt-get dist-upgrade --yes && apt-get install snapcraft --yes && apt-get autoclean --yes && apt-get clean --yes +RUN apt-get update && \ + apt-get dist-upgrade --yes && \ + apt-get install --yes \ + git \ + snapcraft \ + && \ + apt-get autoclean --yes && \ + apt-get clean --yes # Required by click. ENV LC_ALL C.UTF-8 +ENV SNAPCRAFT_SETUP_CORE 1 diff -Nru snapcraft-2.34/integration_tests/containers/test_cleanbuild.py snapcraft-2.35/integration_tests/containers/test_cleanbuild.py --- snapcraft-2.34/integration_tests/containers/test_cleanbuild.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/containers/test_cleanbuild.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,44 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2017 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 . + +import subprocess + +from testtools.matchers import FileExists + +import integration_tests + + +class CleanbuildTestCase(integration_tests.TestCase): + + def run_snapcraft_cleanbuild(self, project_dir): + if project_dir: + self.copy_project_to_cwd(project_dir) + + command = [self.snapcraft_command, '-d', 'cleanbuild'] + popen = subprocess.Popen( + command, stdout=subprocess.PIPE, universal_newlines=True) + for line in iter(popen.stdout.readline, ''): + print(line) + popen.stdout.close() + return_code = popen.wait() + if return_code: + raise subprocess.CalledProcessError(return_code, command) + + def test_cleanbuild(self): + self.run_snapcraft_cleanbuild('basic') + + snap_file_path = 'basic_0.1_all.snap' + self.assertThat(snap_file_path, FileExists()) diff -Nru snapcraft-2.34/integration_tests/containers/test_container_builds.py snapcraft-2.35/integration_tests/containers/test_container_builds.py --- snapcraft-2.34/integration_tests/containers/test_container_builds.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/containers/test_container_builds.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,50 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import subprocess + +import fixtures +from testtools.matchers import FileExists + +import integration_tests + + +class ContainerBuildsTestCase(integration_tests.TestCase): + + def setUp(self): + super().setUp() + self.useFixture( + fixtures.EnvironmentVariable('SNAPCRAFT_CONTAINER_BUILDS', '1')) + + def run_snapcraft(self, project_dir): + if project_dir: + self.copy_project_to_cwd(project_dir) + + command = [self.snapcraft_command, '--debug'] + popen = subprocess.Popen( + command, stdout=subprocess.PIPE, universal_newlines=True) + for line in iter(popen.stdout.readline, ''): + print(line) + popen.stdout.close() + return_code = popen.wait() + if return_code: + raise subprocess.CalledProcessError(return_code, command) + + def test_container_build(self): + self.run_snapcraft('basic') + + snap_file_path = 'basic_0.1_all.snap' + self.assertThat(snap_file_path, FileExists()) diff -Nru snapcraft-2.34/integration_tests/__init__.py snapcraft-2.35/integration_tests/__init__.py --- snapcraft-2.34/integration_tests/__init__.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/__init__.py 2017-11-01 19:41:33.000000000 +0000 @@ -15,6 +15,7 @@ # along with this program. If not, see . import fileinput +import glob import os import re import shutil @@ -33,8 +34,9 @@ from unittest import mock from testtools import content from testtools.matchers import MatchesRegex + from snapcraft import ProjectOptions as _ProjectOptions -from snapcraft.internal.common import get_os_release_info +from snapcraft.internal.os_release import OsRelease from snapcraft.tests import ( fixture_setup, subprocess_utils @@ -107,14 +109,17 @@ self.prime_dir = 'prime' self.deb_arch = _ProjectOptions().deb_arch - self.distro_series = get_os_release_info()['VERSION_CODENAME'] + release = OsRelease() + self.distro_series = release.version_codename() def run_snapcraft( - self, command, project_dir=None, debug=True, + self, command=None, project_dir=None, debug=True, pre_func=lambda: None, env=None): if project_dir: self.copy_project_to_cwd(project_dir) + if command is None: + command = [] if isinstance(command, str): command = [command] snapcraft_command = [self.snapcraft_command] @@ -366,6 +371,12 @@ self.test_store = fixture_setup.TestStore() self.useFixture(self.test_store) + def is_store_fake(self): + return (os.getenv('TEST_STORE') or 'fake') == 'fake' + + def is_store_staging(self): + return os.getenv('TEST_STORE') == 'staging' + def login(self, email=None, password=None, expect_success=True): email = email or self.test_store.user_email password = password or self.test_store.user_password @@ -373,7 +384,9 @@ process = pexpect.spawn(self.snapcraft_command, ['login']) process.expect_exact( - 'Enter your Ubuntu One SSO credentials.\r\n' + 'Enter your Ubuntu One e-mail address and password.\r\n' + 'If you do not have an Ubuntu One account, you can create one at ' + 'https://dashboard.snapcraft.io/openid/login\r\n' 'Email: ') process.sendline(email) process.expect_exact('Password: ') @@ -386,8 +399,7 @@ def logout(self): output = self.run_snapcraft('logout') - expected = (r'.*Clearing credentials for Ubuntu One SSO.\n' - r'Credentials cleared.\n.*') + expected = (r'.*Credentials cleared.\n.*') self.assertThat(output, MatchesRegex(expected, flags=re.DOTALL)) def register(self, snap_name, private=False, wait=True): @@ -424,7 +436,9 @@ self.snapcraft_command, ['register-key', key_name]) process.expect_exact( - 'Enter your Ubuntu One SSO credentials.\r\n' + 'Enter your Ubuntu One e-mail address and password.\r\n' + 'If you do not have an Ubuntu One account, you can create one at ' + 'https://dashboard.snapcraft.io/openid/login\r\n' 'Email: ') process.sendline(email) process.expect_exact('Password: ') @@ -586,6 +600,30 @@ return process.exitstatus +class SnapdIntegrationTestCase(TestCase): + + slow_test = False + + def setUp(self): + super().setUp() + if (self.slow_test and + not os.environ.get('SNAPCRAFT_SLOW_TESTS', False)): + self.skipTest('Not running slow tests') + if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': + self.skipTest("The autopkgtest armhf runners can't install snaps") + + def install_snap(self): + try: + subprocess.check_output( + ['sudo', 'snap', 'install', + glob.glob('*.snap')[0], + '--dangerous'], + stderr=subprocess.STDOUT, universal_newlines=True) + except subprocess.CalledProcessError as e: + self.addDetail('output', content.text_content(e.output)) + raise + + def get_package_version(package_name, series, deb_arch): # http://people.canonical.com/~ubuntu-archive/madison.cgi?package=hello&a=amd64&c=&s=zesty&text=on params = { diff -Nru snapcraft-2.34/integration_tests/plugins/test_autotools_plugin.py snapcraft-2.35/integration_tests/plugins/test_autotools_plugin.py --- snapcraft-2.34/integration_tests/plugins/test_autotools_plugin.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/plugins/test_autotools_plugin.py 2017-11-01 19:41:33.000000000 +0000 @@ -29,8 +29,8 @@ self.run_snapcraft('stage', 'autotools-hello') binary_output = self.get_output_ignoring_non_zero_exit( - os.path.join(self.stage_dir, 'bin', 'test')) - self.assertThat(binary_output, Equals('Hello world\n')) + os.path.join(self.stage_dir, 'bin', 'hello')) + self.assertThat(binary_output, Equals('Hello, world!\n')) def test_cross_compiling(self): if snapcraft.ProjectOptions().deb_arch != 'amd64': @@ -39,5 +39,5 @@ self.run_snapcraft(['build', '--target-arch=arm64'], 'autotools-hello') binary = os.path.join(self.parts_dir, 'make-project', 'install', 'bin', - 'test') + 'hello') self.assertThat(binary, HasArchitecture('aarch64')) diff -Nru snapcraft-2.34/integration_tests/plugins/test_nodejs_plugin.py snapcraft-2.35/integration_tests/plugins/test_nodejs_plugin.py --- snapcraft-2.34/integration_tests/plugins/test_nodejs_plugin.py 2017-09-10 17:25:47.000000000 +0000 +++ snapcraft-2.35/integration_tests/plugins/test_nodejs_plugin.py 2017-11-01 19:41:33.000000000 +0000 @@ -26,7 +26,6 @@ integration_tests.TestCase): scenarios = [ - ('default', dict(package_manager='')), ('npm', dict(package_manager='npm')), ('yarn', dict(package_manager='yarn')), ] diff -Nru snapcraft-2.34/integration_tests/plugins/test_plainbox_provider_plugin.py snapcraft-2.35/integration_tests/plugins/test_plainbox_provider_plugin.py --- snapcraft-2.34/integration_tests/plugins/test_plainbox_provider_plugin.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/plugins/test_plainbox_provider_plugin.py 2017-11-01 19:41:33.000000000 +0000 @@ -23,8 +23,8 @@ import integration_tests -class PlainboxProviderPluginTestCase(testscenarios.WithScenarios, - integration_tests.TestCase): +class PlainboxProviderPluginStageTestCase(testscenarios.WithScenarios, + integration_tests.TestCase): scenarios = [ ('basic', dict(project_directory='plainbox-provider')), @@ -41,6 +41,9 @@ 'plainbox-provider-simple.provider'), FileExists()) + +class PlainboxProviderPluginTestCase(integration_tests.TestCase): + def test_snap_provider_with_deps(self): project_dir = 'plainbox-provider-with-deps' self.run_snapcraft('prime', project_dir) diff -Nru snapcraft-2.34/integration_tests/plugins/test_ruby_plugin.py snapcraft-2.35/integration_tests/plugins/test_ruby_plugin.py --- snapcraft-2.34/integration_tests/plugins/test_ruby_plugin.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/plugins/test_ruby_plugin.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,38 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 James Beedy +# +# 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 . + +import os + +import integration_tests + + +class RubyPluginTestCase(integration_tests.TestCase): + + def test_ruby_bins_exist(self): + self.run_snapcraft('stage', 'ruby-bins-exist') + for exe in ['erb', 'gem', 'irb', 'rake', 'rdoc', 'ri', 'ruby']: + exe_path = os.path.join(self.stage_dir, 'bin', exe) + self.assertTrue(os.path.exists(exe_path)) + + def test_ruby_gem_install(self): + self.run_snapcraft('stage', 'ruby-gem-install') + rack_path = os.path.join(self.stage_dir, 'bin', 'rackup') + self.assertTrue(os.path.exists(rack_path)) + + def test_ruby_bundle_install(self): + self.run_snapcraft('stage', 'ruby-bundle-install') + rack_path = os.path.join(self.stage_dir, 'bin', 'rackup') + self.assertTrue(os.path.exists(rack_path)) diff -Nru snapcraft-2.34/integration_tests/snapd/test_autotools_snap.py snapcraft-2.35/integration_tests/snapd/test_autotools_snap.py --- snapcraft-2.34/integration_tests/snapd/test_autotools_snap.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snapd/test_autotools_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,34 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import subprocess + +from testtools.matchers import Equals + +import integration_tests +from snapcraft.tests import fixture_setup + + +class AutotoolsTestCase(integration_tests.SnapdIntegrationTestCase): + + def test_install_and_execution(self): + with fixture_setup.WithoutSnapInstalled('autotools-hello'): + self.run_snapcraft(project_dir='autotools-hello') + self.install_snap() + self.assertThat( + subprocess.check_output( + ['autotools-hello'], universal_newlines=True), + Equals('Hello, world!\n')) diff -Nru snapcraft-2.34/integration_tests/snapd/test_catkin_snap.py snapcraft-2.35/integration_tests/snapd/test_catkin_snap.py --- snapcraft-2.34/integration_tests/snapd/test_catkin_snap.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snapd/test_catkin_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,92 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os +import re +import subprocess + +from testtools.matchers import ( + Contains, + Equals, + MatchesRegex, +) + +import integration_tests +from snapcraft.tests import ( + fixture_setup, + skip, +) + + +class CatkinTestCase(integration_tests.SnapdIntegrationTestCase): + + slow_test = True + + @skip.skip_unless_codename('xenial', 'ROS Kinetic only targets Xenial') + def test_install_and_execution(self): + self.useFixture(fixture_setup.WithoutSnapInstalled('ros-example')) + try: + failed = True + self.run_snapcraft(project_dir='ros-talker-listener') + failed = False + except subprocess.CalledProcessError: + if self.deb_arch == 'arm64': + # https://bugs.launchpad.net/snapcraft/+bug/1662915 + self.expectFailure( + 'There are no arm64 Indigo packages in the ROS archive', + self.assertFalse, failed) + else: + raise + + self.install_snap() + # check that the hardcoded /usr/bin/python in rosversion + # is changed to using /usr/bin/env python + expected = b'#!/usr/bin/env python\n' + output = subprocess.check_output( + "sed -n '/env/p;1q' prime/usr/bin/rosversion", shell=True) + self.assertThat(output, Equals(expected)) + + # This test fails if the binary is executed from /tmp. + os.chdir(os.path.expanduser('~')) + # Regression test for LP: #1660852. Make sure --help actually gets + # passed to roslaunch instead of being eaten by setup.sh. + output = subprocess.check_output( + ['ros-example.launch-project', '--help']).decode() + self.assertThat(output, MatchesRegex(r'.*Usage: roslaunch.*')) + + # Run the ROS system. By default this will never exit, but the demo + # supports an `exit-after-receive` parameter that, if true, will cause + # the system to shutdown after the listener has successfully received + # a message. + output = subprocess.check_output( + ['ros-example.launch-project', + 'exit-after-receive:=true']).decode() + self.assertThat( + output, + MatchesRegex(r'.*I heard Hello world.*', flags=re.DOTALL)) + + @skip.skip_unless_codename('xenial', 'ROS Kinetic only targets Xenial') + def test_catkin_pip_support(self): + with fixture_setup.WithoutSnapInstalled('ros-pip-example'): + self.run_snapcraft(project_dir='ros-pip') + self.install_snap() + + # If pip support didn't work properly, the import should fail. + self.assertThat( + subprocess.check_output( + ['ros-pip-example.launch-project'], + universal_newlines=True, stderr=subprocess.STDOUT), + Contains("Local timezone:")) diff -Nru snapcraft-2.34/integration_tests/snapd/test_cmake_snap.py snapcraft-2.35/integration_tests/snapd/test_cmake_snap.py --- snapcraft-2.34/integration_tests/snapd/test_cmake_snap.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snapd/test_cmake_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,34 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import subprocess + +from testtools.matchers import Equals + +import integration_tests +from snapcraft.tests import fixture_setup + + +class CmakeTestCase(integration_tests.SnapdIntegrationTestCase): + + def test_install_and_execution(self): + with fixture_setup.WithoutSnapInstalled('cmake-hello'): + self.run_snapcraft(project_dir='cmake-hello') + self.install_snap() + self.assertThat( + subprocess.check_output( + ['cmake-hello'], universal_newlines=True), + Equals("It's a CMake world\n")) diff -Nru snapcraft-2.34/integration_tests/snapd/test_dotnet_snap.py snapcraft-2.35/integration_tests/snapd/test_dotnet_snap.py --- snapcraft-2.34/integration_tests/snapd/test_dotnet_snap.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snapd/test_dotnet_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,38 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import subprocess + +from testtools.matchers import Equals + +import integration_tests +from snapcraft.tests import fixture_setup, skip + + +class DotnetTestCase(integration_tests.SnapdIntegrationTestCase): + + @skip.skip_unless_codename('xenial', 'the dotnet plugin targets Xenial') + def test_install_and_execution(self): + if self.deb_arch != 'amd64': + self.skipTest('The dotnet plugin only supports amd64, for now') + + with fixture_setup.WithoutSnapInstalled('dotnet-hello'): + self.run_snapcraft(project_dir='dotnet-hello') + self.install_snap() + self.assertThat( + subprocess.check_output( + ['dotnet-hello'], universal_newlines=True), + Equals('Hello World!\n')) diff -Nru snapcraft-2.34/integration_tests/snapd/test_go_snap.py snapcraft-2.35/integration_tests/snapd/test_go_snap.py --- snapcraft-2.34/integration_tests/snapd/test_go_snap.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snapd/test_go_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,44 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os +import subprocess +from distutils import dir_util + +from testtools.matchers import Equals + +import integration_tests +from snapcraft.tests import fixture_setup + + +class GoTestCase(integration_tests.SnapdIntegrationTestCase): + + def test_install_and_execution(self): + with fixture_setup.WithoutSnapInstalled('go-hello'): + # If we just put the source in the temp dir, go will generate + # the binary with the name of the temp dir, instead of go-hello. + dir_util.copy_tree( + os.path.join(self.snaps_dir, 'go-hello'), + os.path.join(self.path, 'go-hello'), + preserve_symlinks=True) + os.chdir('go-hello') + + self.run_snapcraft() + self.install_snap() + self.assertThat( + subprocess.check_output( + ['go-hello'], universal_newlines=True), + Equals('Hello snapcrafter\n')) diff -Nru snapcraft-2.34/integration_tests/snapd/test_nodejs_snap.py snapcraft-2.35/integration_tests/snapd/test_nodejs_snap.py --- snapcraft-2.34/integration_tests/snapd/test_nodejs_snap.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snapd/test_nodejs_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,34 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import subprocess + +from testtools.matchers import Equals + +import integration_tests +from snapcraft.tests import fixture_setup + + +class NodeJSTestCase(integration_tests.SnapdIntegrationTestCase): + + def test_install_and_execution(self): + with fixture_setup.WithoutSnapInstalled('nodejs-hello'): + self.run_snapcraft(project_dir='nodejs-hello') + self.install_snap() + self.assertThat( + subprocess.check_output( + ['nodejs-hello'], universal_newlines=True), + Equals('Hello world!\n')) diff -Nru snapcraft-2.34/integration_tests/snapd/test_plainbox.py snapcraft-2.35/integration_tests/snapd/test_plainbox.py --- snapcraft-2.34/integration_tests/snapd/test_plainbox.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snapd/test_plainbox.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,51 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2017 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 . + +import os +import subprocess +import textwrap + +from testtools.matchers import Equals + +import integration_tests +from snapcraft.tests import fixture_setup + + +class PlainboxTestCase(integration_tests.SnapdIntegrationTestCase): + + def test_install_and_execution(self): + with fixture_setup.WithoutSnapInstalled('plainbox-simple'): + self.run_snapcraft(project_dir='plainbox-simple') + self.install_snap() + + # This test fails if the binary is executed from /tmp. + os.chdir(os.path.expanduser('~')) + + expected = textwrap.dedent("""\ + com.canonical.plainbox::collect-manifest + com.canonical.plainbox::manifest + com.example::always-fail + com.example::always-pass + """) + self.assertThat( + subprocess.check_output( + ['plainbox-simple.plainbox', 'dev', 'special', '-j'], + universal_newlines=True), + Equals(expected)) + + subprocess.check_call( + ['plainbox-simple.plainbox', 'run', + '-i com.example::.*']) diff -Nru snapcraft-2.34/integration_tests/snapd/test_python_snap.py snapcraft-2.35/integration_tests/snapd/test_python_snap.py --- snapcraft-2.34/integration_tests/snapd/test_python_snap.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snapd/test_python_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,34 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import subprocess + +from testtools.matchers import Equals + +import integration_tests +from snapcraft.tests import fixture_setup + + +class PythonTestCase(integration_tests.SnapdIntegrationTestCase): + + def test_install_and_execution(self): + with fixture_setup.WithoutSnapInstalled('python-hello'): + self.run_snapcraft(project_dir='python-hello') + self.install_snap() + self.assertThat( + subprocess.check_output( + ['python-hello'], universal_newlines=True), + Equals('Hello world!\n')) diff -Nru snapcraft-2.34/integration_tests/snapd/test_ruby_snap.py snapcraft-2.35/integration_tests/snapd/test_ruby_snap.py --- snapcraft-2.34/integration_tests/snapd/test_ruby_snap.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snapd/test_ruby_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,34 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import subprocess + +from testtools.matchers import Equals + +import integration_tests +from snapcraft.tests import fixture_setup + + +class RubyTestCase(integration_tests.SnapdIntegrationTestCase): + + def test_install_and_execution(self): + with fixture_setup.WithoutSnapInstalled('ruby-hello'): + self.run_snapcraft(project_dir='ruby-hello') + self.install_snap() + self.assertThat( + subprocess.check_output( + ['ruby-hello'], universal_newlines=True), + Equals('Hello world!\n')) diff -Nru snapcraft-2.34/integration_tests/snapd/test_scons_snap.py snapcraft-2.35/integration_tests/snapd/test_scons_snap.py --- snapcraft-2.34/integration_tests/snapd/test_scons_snap.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snapd/test_scons_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,34 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import subprocess + +from testtools.matchers import Equals + +import integration_tests +from snapcraft.tests import fixture_setup + + +class SconsTestCase(integration_tests.SnapdIntegrationTestCase): + + def test_install_and_execution(self): + with fixture_setup.WithoutSnapInstalled('scons-hello'): + self.run_snapcraft(project_dir='scons-hello') + self.install_snap() + self.assertThat( + subprocess.check_output( + ['scons-hello'], universal_newlines=True), + Equals('Hello world\n')) diff -Nru snapcraft-2.34/integration_tests/snaps/assemble/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/assemble/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/assemble/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/assemble/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -10,6 +10,9 @@ command: binary1 binary2: command: subdir/binary2 + binary-wrapper-none: + command: subdir/binary3 + adapter: none assemble-service: command: service-start stop-command: service-stop with args diff -Nru snapcraft-2.34/integration_tests/snaps/assets-with-gui-in-snap/snap/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/assets-with-gui-in-snap/snap/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/assets-with-gui-in-snap/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/assets-with-gui-in-snap/snap/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -11,6 +11,9 @@ apps: my-app: command: bash + # ensure we still have a valid desktop file even when no adapter + # (wrapper) is generated + adapter: none parts: my-app: diff -Nru snapcraft-2.34/integration_tests/snaps/autotools-hello/configure.ac snapcraft-2.35/integration_tests/snaps/autotools-hello/configure.ac --- snapcraft-2.34/integration_tests/snaps/autotools-hello/configure.ac 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/autotools-hello/configure.ac 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -AC_INIT([test], [0.1], [snapcraft@lists.snapcraft.io]) -AM_INIT_AUTOMAKE([-Wall -Werror foreign]) -AC_PROG_CC -AC_CONFIG_HEADERS([config.h]) -AC_CONFIG_FILES([ - Makefile -]) -AC_OUTPUT diff -Nru snapcraft-2.34/integration_tests/snaps/autotools-hello/Makefile.am snapcraft-2.35/integration_tests/snaps/autotools-hello/Makefile.am --- snapcraft-2.34/integration_tests/snaps/autotools-hello/Makefile.am 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/autotools-hello/Makefile.am 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -bin_PROGRAMS = test -test_SOURCES = test.c diff -Nru snapcraft-2.34/integration_tests/snaps/autotools-hello/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/autotools-hello/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/autotools-hello/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/autotools-hello/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -8,12 +8,12 @@ confinement: strict apps: - autottools-hello: - command: test + autotools-hello: + command: hello build-packages: [gcc] parts: make-project: plugin: autotools - source: . + source: http://ftp.gnu.org/gnu/hello/hello-2.10.tar.gz diff -Nru snapcraft-2.34/integration_tests/snaps/autotools-hello/test.c snapcraft-2.35/integration_tests/snaps/autotools-hello/test.c --- snapcraft-2.34/integration_tests/snaps/autotools-hello/test.c 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/autotools-hello/test.c 1970-01-01 00:00:00.000000000 +0000 @@ -1,6 +0,0 @@ -#include - -int main (void) -{ - printf("Hello world\n"); -} diff -Nru snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/consumer/snap/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/consumer/snap/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/consumer/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/consumer/snap/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,27 @@ +name: catkin-shared-ros-consumer +version: '0.1' +summary: Test building a consumer for a shared part. +description: | + The shared part doesn't contain Catkin, so this makes sure the Catkin + used within the Catkin plugin is properly isolated. + +grade: devel +confinement: strict + +parts: + underlay: + plugin: dump + source: underlay.tar.bz2 + prime: [-*] + + # Create the overlay, the consumer that uses the stuff shared from the + # producer. + overlay: + plugin: catkin + rosdistro: kinetic + include-roscore: false + catkin-packages: [overlay_package] + underlay: + build-path: $SNAPCRAFT_STAGE/opt/ros/kinetic + run-path: $SNAP/opt/ros/kinetic + after: [underlay] diff -Nru snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/consumer/src/overlay_package/CMakeLists.txt snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/consumer/src/overlay_package/CMakeLists.txt --- snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/consumer/src/overlay_package/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/consumer/src/overlay_package/CMakeLists.txt 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 2.8.3) +project(overlay_package) + +find_package(catkin REQUIRED) + +catkin_package() diff -Nru snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/consumer/src/overlay_package/package.xml snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/consumer/src/overlay_package/package.xml --- snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/consumer/src/overlay_package/package.xml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/consumer/src/overlay_package/package.xml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,12 @@ + + + overlay_package + 0.0.0 + A description + me + GPLv3 + catkin + + underlay_package + underlay_package + diff -Nru snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/producer/snap/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/producer/snap/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/producer/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/producer/snap/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,27 @@ +name: catkin-shared-ros-producer +version: '0.1' +summary: Producer for sharing ROS +description: | + Of particular note, the shared bits do not include Catkin + +grade: devel +confinement: strict + +parts: + # Create the underlay, the stuff shared from the producer. + underlay: + plugin: catkin + rosdistro: kinetic + include-roscore: false + catkin-packages: [underlay_package] + + # Strip out everything except that single dependency. Of particular + # importance: the underlay does not include Catkin. + stage: + - opt/ros/kinetic/.catkin + - opt/ros/kinetic/env.sh + - opt/ros/kinetic/setup.bash + - opt/ros/kinetic/setup.sh + - opt/ros/kinetic/_setup_util.py + - opt/ros/kinetic/lib/pkgconfig/underlay_package.pc + - opt/ros/kinetic/share/underlay_package/ diff -Nru snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/producer/src/underlay_package/CMakeLists.txt snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/producer/src/underlay_package/CMakeLists.txt --- snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/producer/src/underlay_package/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/producer/src/underlay_package/CMakeLists.txt 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 2.8.3) +project(underlay_package) + +find_package(catkin REQUIRED) + +catkin_package() diff -Nru snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/producer/src/underlay_package/package.xml snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/producer/src/underlay_package/package.xml --- snapcraft-2.34/integration_tests/snaps/catkin-shared-ros/producer/src/underlay_package/package.xml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/catkin-shared-ros/producer/src/underlay_package/package.xml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,9 @@ + + + underlay_package + 0.0.0 + A description + me + GPLv3 + catkin + diff -Nru snapcraft-2.34/integration_tests/snaps/catkin-with-python-part/ros_tutorials.rosinstall snapcraft-2.35/integration_tests/snaps/catkin-with-python-part/ros_tutorials.rosinstall --- snapcraft-2.34/integration_tests/snaps/catkin-with-python-part/ros_tutorials.rosinstall 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/catkin-with-python-part/ros_tutorials.rosinstall 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,4 @@ +- git: + local-name: ros_tutorials + uri: https://github.com/ros/ros_tutorials.git + version: kinetic-devel diff -Nru snapcraft-2.34/integration_tests/snaps/catkin-with-python-part/snap/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/catkin-with-python-part/snap/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/catkin-with-python-part/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/catkin-with-python-part/snap/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,31 @@ +name: catkin-with-python-part +version: '1.0' +summary: Snap with a Catkin and Python part +description: | + This snap ensures that the Catkin plugin plays nicely with the Python plugin. + It also ensures that Catkin's handling of rosinstall files continues to work. + +grade: devel +confinement: strict + +parts: + python-part: + plugin: python + python-version: python2 + python-packages: + - transitions + + catkin-part: + plugin: catkin + rosdistro: kinetic + rosinstall-files: [ros_tutorials.rosinstall] + catkin-packages: + - roscpp_tutorials + + # Make sure this runs after the python part has been staged, as that poses + # the most problems. + after: [python-part] + + stage: + # The python part includes a sitecustomize that we don't want to clobber + - -usr/lib/python2.7/sitecustomize.py diff -Nru snapcraft-2.34/integration_tests/snaps/cmake-hello/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/cmake-hello/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/cmake-hello/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/cmake-hello/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -10,6 +10,10 @@ build-packages: [gcc, libc6-dev] +apps: + cmake-hello: + command: cmake-hello + parts: cmake-project: plugin: cmake diff -Nru snapcraft-2.34/integration_tests/snaps/dotnet-hello/dotnet-hello.csproj snapcraft-2.35/integration_tests/snaps/dotnet-hello/dotnet-hello.csproj --- snapcraft-2.34/integration_tests/snaps/dotnet-hello/dotnet-hello.csproj 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/dotnet-hello/dotnet-hello.csproj 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,8 @@ + + + + Exe + netcoreapp2.0 + + + diff -Nru snapcraft-2.34/integration_tests/snaps/dotnet-hello/Program.cs snapcraft-2.35/integration_tests/snaps/dotnet-hello/Program.cs --- snapcraft-2.34/integration_tests/snaps/dotnet-hello/Program.cs 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/dotnet-hello/Program.cs 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,12 @@ +using System; + +namespace dotnet.hello +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello World!"); + } + } +} diff -Nru snapcraft-2.34/integration_tests/snaps/dotnet-hello/snap/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/dotnet-hello/snap/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/dotnet-hello/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/dotnet-hello/snap/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,16 @@ +name: dotnet-hello +version: '0.1' +summary: This is a sample dotnet 2.0.0 application +description: | + This is a sample dotnet 2.0.0 application + +grade: devel +confinement: strict + +apps: + dotnet-hello: + command: dotnet-hello +parts: + dotnet-hello: + plugin: dotnet + source: . diff -Nru snapcraft-2.34/integration_tests/snaps/make-hello/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/make-hello/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/make-hello/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/make-hello/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -10,6 +10,10 @@ build-packages: [gcc, libc6-dev] +apps: + make-hello: + command: test + parts: make-project: plugin: make diff -Nru snapcraft-2.34/integration_tests/snaps/nodejs-hello/index.js snapcraft-2.35/integration_tests/snaps/nodejs-hello/index.js --- snapcraft-2.34/integration_tests/snaps/nodejs-hello/index.js 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/nodejs-hello/index.js 2017-11-01 19:41:33.000000000 +0000 @@ -1,9 +1,3 @@ -var http = require("http"); +#!/usr/bin/env node -http.createServer(function (request, response) { - - response.writeHead(200, {'Content-Type': 'text/plain'}); - response.end('Hello World\n'); -}).listen(8081); - -console.log('Server running at http://127.0.0.1:8081/'); +console.log('Hello world!'); diff -Nru snapcraft-2.34/integration_tests/snaps/nodejs-hello/package.json snapcraft-2.35/integration_tests/snaps/nodejs-hello/package.json --- snapcraft-2.34/integration_tests/snaps/nodejs-hello/package.json 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/nodejs-hello/package.json 2017-11-01 19:41:33.000000000 +0000 @@ -2,9 +2,11 @@ "name": "nodejs-hello", "version": "1.0.0", "description": "Testing grounds for snapcraft integration tests", - "main": "index.js", + "bin": { + "nodejs-hello": "index.js" + }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "nodejs-hello": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "GPL-3.0" diff -Nru snapcraft-2.34/integration_tests/snaps/nodejs-hello/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/nodejs-hello/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/nodejs-hello/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/nodejs-hello/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -7,6 +7,11 @@ Make a new one. confinement: strict +apps: + nodejs-hello: + command: nodejs-hello + plugs: [network] + parts: nodejs-part: source: . diff -Nru snapcraft-2.34/integration_tests/snaps/plainbox-simple/com.example_simple/manage.py snapcraft-2.35/integration_tests/snaps/plainbox-simple/com.example_simple/manage.py --- snapcraft-2.34/integration_tests/snaps/plainbox-simple/com.example_simple/manage.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/plainbox-simple/com.example_simple/manage.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +from plainbox.provider_manager import setup, N_ + + +setup( + name='plainbox-provider-simple', + namespace='com.example', + version="1.0", + description=N_("A really simple Plainbox provider"), + gettext_domain="com_example_simple", +) diff -Nru snapcraft-2.34/integration_tests/snaps/plainbox-simple/com.example_simple/units/simple.pxu snapcraft-2.35/integration_tests/snaps/plainbox-simple/com.example_simple/units/simple.pxu --- snapcraft-2.34/integration_tests/snaps/plainbox-simple/com.example_simple/units/simple.pxu 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/plainbox-simple/com.example_simple/units/simple.pxu 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,32 @@ + +unit: category +id: simple +_name: Simple + +unit: job +id: always-pass +category_id: simple +_summary: A test that always passes +_description: + A test that always passes + . + This simple test will always succeed, assuming your + platform has a 'true' command that returns 0. +plugin: shell +estimated_duration: 0.01 +command: true +flags: preserve-locale + +unit: job +id: always-fail +category_id: simple +_summary: A test that always fails +_description: + A test that always fails + . + This simple test will always fail, assuming your + platform has a 'false' command that returns 1. +plugin: shell +estimated_duration: 0.01 +command: false +flags: preserve-locale diff -Nru snapcraft-2.34/integration_tests/snaps/plainbox-simple/launchers/plainbox-wrapper snapcraft-2.35/integration_tests/snaps/plainbox-simple/launchers/plainbox-wrapper --- snapcraft-2.34/integration_tests/snaps/plainbox-simple/launchers/plainbox-wrapper 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/plainbox-simple/launchers/plainbox-wrapper 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/sh +export PATH="$PATH:$SNAP/usr/sbin" +exec python3 $(which plainbox) "$@" diff -Nru snapcraft-2.34/integration_tests/snaps/plainbox-simple/snap/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/plainbox-simple/snap/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/plainbox-simple/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/plainbox-simple/snap/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,42 @@ +name: plainbox-simple +summary: A Plainbox Test Tool +description: > + The Plainbox tool itself coupled with a Provider which contains the test + definitions +version: 0.1 +confinement: strict + +apps: + plainbox: + command: bin/plainbox-wrapper + # Required because of https://bugs.launchpad.net/snapcraft/+bug/1732076 + plugs: [home] + +parts: + # The testing framework + plainbox-local: + plugin: python3 + python-packages: + - plainbox + - requests-oauthlib + - xlsxwriter + build-packages: + - libxml2-dev + - libxslt1-dev + - zlib1g-dev + - build-essential + # The test definitions + simple-plainbox-provider: + plugin: plainbox-provider + source: ./com.example_simple + after: [plainbox-local] + # A wrapper script + launchers: + plugin: dump + source: . + organize: + launchers/plainbox-wrapper: bin/plainbox-wrapper + filesets: + wrapper: [bin/plainbox-wrapper] + stage: [$wrapper] + snap: [$wrapper] diff -Nru snapcraft-2.34/integration_tests/snaps/python-hello/hello snapcraft-2.35/integration_tests/snaps/python-hello/hello --- snapcraft-2.34/integration_tests/snaps/python-hello/hello 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/python-hello/hello 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,3 @@ +#!/usr/bin/env python + +print('Hello world!') diff -Nru snapcraft-2.34/integration_tests/snaps/python-hello/setup.py snapcraft-2.35/integration_tests/snaps/python-hello/setup.py --- snapcraft-2.34/integration_tests/snaps/python-hello/setup.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/python-hello/setup.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,11 @@ +import setuptools + + +setuptools.setup( + name='hello-world', + version='0.0.1', + author='Canonical LTD', + author_email='snapcraft@lists.snapcraft.io', + description='A simple hello world in python', + scripts=['hello'] +) diff -Nru snapcraft-2.34/integration_tests/snaps/python-hello/snap/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/python-hello/snap/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/python-hello/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/python-hello/snap/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,17 @@ +name: python-hello +version: '0.1' +summary: A simple hello world in python +description: | + This is a basic python snap. It just hosts a hello world. + If you want to add other functionalities to this snap, please don't. + Make a new one. +confinement: strict + +apps: + python-hello: + command: hello + +parts: + python-part: + source: . + plugin: python diff -Nru snapcraft-2.34/integration_tests/snaps/ros-pip/snap/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/ros-pip/snap/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/ros-pip/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-pip/snap/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,20 @@ +name: ros-pip-example +version: 1.0 +summary: ROS Example using pip dependencies +description: Contains a simple pip test and a .launch file. +confinement: strict +grade: devel + +apps: + launch-project: + command: roslaunch timezone_test test.launch + plugs: [network-bind] + +parts: + ros-project: + plugin: catkin + source: . + rosdistro: kinetic + catkin-packages: + - timezone_test + include-roscore: true diff -Nru snapcraft-2.34/integration_tests/snaps/ros-pip/src/CMakeLists.txt snapcraft-2.35/integration_tests/snaps/ros-pip/src/CMakeLists.txt --- snapcraft-2.34/integration_tests/snaps/ros-pip/src/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-pip/src/CMakeLists.txt 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,47 @@ +# toplevel CMakeLists.txt for a catkin workspace +# catkin/cmake/toplevel.cmake + +cmake_minimum_required(VERSION 2.8.3) + +# optionally provide a cmake file in the workspace to override arbitrary stuff +include(workspace.cmake OPTIONAL) + +set(CATKIN_TOPLEVEL TRUE) + +# include catkin directly or via find_package() +if(EXISTS "${CMAKE_SOURCE_DIR}/catkin/cmake/all.cmake" AND EXISTS "${CMAKE_SOURCE_DIR}/catkin/CMakeLists.txt") + set(catkin_EXTRAS_DIR "${CMAKE_SOURCE_DIR}/catkin/cmake") + # include all.cmake without add_subdirectory to let it operate in same scope + include(catkin/cmake/all.cmake NO_POLICY_SCOPE) + add_subdirectory(catkin) + +else() + # use either CMAKE_PREFIX_PATH explicitly passed to CMake as a command line argument + # or CMAKE_PREFIX_PATH from the environment + if(NOT DEFINED CMAKE_PREFIX_PATH) + if(NOT "$ENV{CMAKE_PREFIX_PATH}" STREQUAL "") + string(REPLACE ":" ";" CMAKE_PREFIX_PATH $ENV{CMAKE_PREFIX_PATH}) + endif() + endif() + + # list of catkin workspaces + set(catkin_search_path "") + foreach(path ${CMAKE_PREFIX_PATH}) + if(EXISTS "${path}/.CATKIN_WORKSPACE") + list(FIND catkin_search_path ${path} _index) + if(_index EQUAL -1) + list(APPEND catkin_search_path ${path}) + endif() + endif() + endforeach() + + # search for catkin in all workspaces + set(CATKIN_TOPLEVEL_FIND_PACKAGE TRUE) + find_package(catkin REQUIRED + NO_POLICY_SCOPE + PATHS ${catkin_search_path} + NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) + unset(CATKIN_TOPLEVEL_FIND_PACKAGE) +endif() + +catkin_workspace() diff -Nru snapcraft-2.34/integration_tests/snaps/ros-pip/src/timezone_test/CMakeLists.txt snapcraft-2.35/integration_tests/snaps/ros-pip/src/timezone_test/CMakeLists.txt --- snapcraft-2.34/integration_tests/snaps/ros-pip/src/timezone_test/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-pip/src/timezone_test/CMakeLists.txt 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,16 @@ +cmake_minimum_required(VERSION 2.8.3) +project(timezone_test) + +find_package(catkin REQUIRED COMPONENTS rospy) + +catkin_package() + +install(PROGRAMS + scripts/timezone_test_node + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) + +install(FILES + test.launch + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} +) diff -Nru snapcraft-2.34/integration_tests/snaps/ros-pip/src/timezone_test/package.xml snapcraft-2.35/integration_tests/snaps/ros-pip/src/timezone_test/package.xml --- snapcraft-2.34/integration_tests/snaps/ros-pip/src/timezone_test/package.xml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-pip/src/timezone_test/package.xml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,13 @@ + + + timezone_test + 0.0.0 + The timezone_test package + me + GPLv3 + catkin + rospy + python-tzlocal-pip + rospy + python-tzlocal-pip + diff -Nru snapcraft-2.34/integration_tests/snaps/ros-pip/src/timezone_test/scripts/timezone_test_node snapcraft-2.35/integration_tests/snaps/ros-pip/src/timezone_test/scripts/timezone_test_node --- snapcraft-2.34/integration_tests/snaps/ros-pip/src/timezone_test/scripts/timezone_test_node 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-pip/src/timezone_test/scripts/timezone_test_node 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +import rospy +import tzlocal + + +def main(): + # This line verifies that we can connect to ROS master + rospy.init_node('timezone_test') + + # This line verifies that we can use tzlocal, which is a pip dependency + print('Local timezone: {}'.format(tzlocal.get_localzone())) + +if __name__ == '__main__': + main() diff -Nru snapcraft-2.34/integration_tests/snaps/ros-pip/src/timezone_test/test.launch snapcraft-2.35/integration_tests/snaps/ros-pip/src/timezone_test/test.launch --- snapcraft-2.34/integration_tests/snaps/ros-pip/src/timezone_test/test.launch 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-pip/src/timezone_test/test.launch 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,4 @@ + + + diff -Nru snapcraft-2.34/integration_tests/snaps/ros-talker-listener/snap/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/ros-talker-listener/snap/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/ros-talker-listener/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-talker-listener/snap/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,19 @@ +name: ros-example +version: 1.0 +summary: ROS Example +description: Contains talker/listener ROS packages and a .launch file. +confinement: strict + +apps: + launch-project: + command: roslaunch listener talk_and_listen.launch + plugs: [network-bind] + +parts: + ros-project: + plugin: catkin + source: . + catkin-packages: + - talker + - listener + include-roscore: true diff -Nru snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/CMakeLists.txt snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/CMakeLists.txt --- snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/CMakeLists.txt 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,47 @@ +# toplevel CMakeLists.txt for a catkin workspace +# catkin/cmake/toplevel.cmake + +cmake_minimum_required(VERSION 2.8.3) + +# optionally provide a cmake file in the workspace to override arbitrary stuff +include(workspace.cmake OPTIONAL) + +set(CATKIN_TOPLEVEL TRUE) + +# include catkin directly or via find_package() +if(EXISTS "${CMAKE_SOURCE_DIR}/catkin/cmake/all.cmake" AND EXISTS "${CMAKE_SOURCE_DIR}/catkin/CMakeLists.txt") + set(catkin_EXTRAS_DIR "${CMAKE_SOURCE_DIR}/catkin/cmake") + # include all.cmake without add_subdirectory to let it operate in same scope + include(catkin/cmake/all.cmake NO_POLICY_SCOPE) + add_subdirectory(catkin) + +else() + # use either CMAKE_PREFIX_PATH explicitly passed to CMake as a command line argument + # or CMAKE_PREFIX_PATH from the environment + if(NOT DEFINED CMAKE_PREFIX_PATH) + if(NOT "$ENV{CMAKE_PREFIX_PATH}" STREQUAL "") + string(REPLACE ":" ";" CMAKE_PREFIX_PATH $ENV{CMAKE_PREFIX_PATH}) + endif() + endif() + + # list of catkin workspaces + set(catkin_search_path "") + foreach(path ${CMAKE_PREFIX_PATH}) + if(EXISTS "${path}/.CATKIN_WORKSPACE") + list(FIND catkin_search_path ${path} _index) + if(_index EQUAL -1) + list(APPEND catkin_search_path ${path}) + endif() + endif() + endforeach() + + # search for catkin in all workspaces + set(CATKIN_TOPLEVEL_FIND_PACKAGE TRUE) + find_package(catkin REQUIRED + NO_POLICY_SCOPE + PATHS ${catkin_search_path} + NO_DEFAULT_PATH NO_CMAKE_FIND_ROOT_PATH) + unset(CATKIN_TOPLEVEL_FIND_PACKAGE) +endif() + +catkin_workspace() diff -Nru snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/listener/CMakeLists.txt snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/listener/CMakeLists.txt --- snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/listener/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/listener/CMakeLists.txt 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,19 @@ +cmake_minimum_required(VERSION 2.8.3) +project(listener) + +find_package(catkin REQUIRED COMPONENTS + rospy + std_msgs +) + +catkin_package() + +install(PROGRAMS + scripts/listener_node + DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) + +install(FILES + talk_and_listen.launch + DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION} +) diff -Nru snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/listener/package.xml snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/listener/package.xml --- snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/listener/package.xml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/listener/package.xml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,13 @@ + + + listener + 0.0.0 + The listener package + me + GPLv3 + catkin + rospy + std_msgs + rospy + std_msgs + diff -Nru snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/listener/scripts/listener_node snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/listener/scripts/listener_node --- snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/listener/scripts/listener_node 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/listener/scripts/listener_node 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +import rospy +from std_msgs.msg import String + + +def callback(data): + rospy.loginfo('I heard %s', data.data) + + if rospy.get_param('~exit-after-receive', False): + rospy.signal_shutdown( + 'Requested to exit after message received. Exiting now.') + + +def listener(): + rospy.init_node('listener') + + rospy.Subscriber('babble', String, callback) + + rospy.spin() + +if __name__ == '__main__': + listener() diff -Nru snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/listener/talk_and_listen.launch snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/listener/talk_and_listen.launch --- snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/listener/talk_and_listen.launch 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/listener/talk_and_listen.launch 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,13 @@ + + + + + + + + + + diff -Nru snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/talker/CMakeLists.txt snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/talker/CMakeLists.txt --- snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/talker/CMakeLists.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/talker/CMakeLists.txt 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 2.8.3) +project(talker) + +find_package(catkin REQUIRED COMPONENTS + roscpp + std_msgs +) + +catkin_package() + +include_directories(${catkin_INCLUDE_DIRS}) + +add_executable(talker_node src/talker_node.cpp) + +target_link_libraries(talker_node ${catkin_LIBRARIES}) + +install(TARGETS talker_node + ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} + LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION} + RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION} +) diff -Nru snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/talker/package.xml snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/talker/package.xml --- snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/talker/package.xml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/talker/package.xml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,13 @@ + + + talker + 0.0.0 + The talker package + me + GPLv3 + catkin + roscpp + std_msgs + roscpp + std_msgs + diff -Nru snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/talker/src/talker_node.cpp snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/talker/src/talker_node.cpp --- snapcraft-2.34/integration_tests/snaps/ros-talker-listener/src/talker/src/talker_node.cpp 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ros-talker-listener/src/talker/src/talker_node.cpp 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,35 @@ +#include + +#include +#include + +int main(int argc, char **argv) +{ + ros::init(argc, argv, "talker"); + + ros::NodeHandle nodeHandle; + + ros::Publisher publisher = nodeHandle.advertise("chatter", 1); + + ros::Rate loopRate(10); + + int count = 0; + while (ros::ok()) + { + std_msgs::String message; + + std::stringstream stream; + stream << "Hello world " << count++; + message.data = stream.str(); + + ROS_INFO("%s", message.data.c_str()); + + publisher.publish(message); + + ros::spinOnce(); + + loopRate.sleep(); + } + + return 0; +} diff -Nru snapcraft-2.34/integration_tests/snaps/ruby-bins-exist/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/ruby-bins-exist/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/ruby-bins-exist/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ruby-bins-exist/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,13 @@ +name: ruby-bins-exist +version: 0.1 +summary: Test ruby binary exists in stage +description: | + Snap to run ruby-exists test against +confinement: strict + +parts: + ruby-exists: + plugin: ruby + build-packages: [lsb-release] + stage: + - bin diff -Nru snapcraft-2.34/integration_tests/snaps/ruby-bundle-install/Gemfile snapcraft-2.35/integration_tests/snaps/ruby-bundle-install/Gemfile --- snapcraft-2.34/integration_tests/snaps/ruby-bundle-install/Gemfile 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ruby-bundle-install/Gemfile 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'rack' diff -Nru snapcraft-2.34/integration_tests/snaps/ruby-bundle-install/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/ruby-bundle-install/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/ruby-bundle-install/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ruby-bundle-install/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,13 @@ +name: ruby-bundle-install +version: 0.1 +summary: Test bundle install +description: | + Snap to test 'bundle install' command +confinement: strict + +parts: + ruby-bundle-install: + plugin: ruby + use-bundler: true + stage: + - bin diff -Nru snapcraft-2.34/integration_tests/snaps/ruby-gem-install/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/ruby-gem-install/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/ruby-gem-install/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ruby-gem-install/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,13 @@ +name: ruby-gem-install +version: 0.1 +summary: Test gem install +description: | + Snap to test 'gem install ' command +confinement: strict + +parts: + ruby-gem-install: + plugin: ruby + gems: ['rack'] + stage: + - bin diff -Nru snapcraft-2.34/integration_tests/snaps/ruby-hello/ruby-hello.rb snapcraft-2.35/integration_tests/snaps/ruby-hello/ruby-hello.rb --- snapcraft-2.34/integration_tests/snaps/ruby-hello/ruby-hello.rb 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ruby-hello/ruby-hello.rb 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby + +puts "Hello world!"; diff -Nru snapcraft-2.34/integration_tests/snaps/ruby-hello/snap/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/ruby-hello/snap/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/ruby-hello/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/ruby-hello/snap/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,25 @@ +name: "ruby-hello" +version: 0.1 +summary: Test ruby +description: | + Snap to test ruby compiled correctly, exists, and works + +grade: stable +confinement: strict + +apps: + ruby-hello: + command: ruby-hello.rb + +parts: + ruby-hello: + plugin: ruby + source: . + stage: + - bin + - lib + prime: + - bin + - lib + install: | + cp ./ruby-hello.rb $SNAPCRAFT_PART_INSTALL/bin/ruby-hello.rb diff -Nru snapcraft-2.34/integration_tests/snaps/scons-hello/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/scons-hello/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/scons-hello/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/scons-hello/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -10,6 +10,10 @@ build-packages: [gcc, libc6-dev] +apps: + scons-hello: + command: opt/bin/main + parts: scons-project: scons-options: Binary files /tmp/tmpvSnI8A/Zd5Qdu2F8S/snapcraft-2.34/integration_tests/snaps/zip/simple.zip and /tmp/tmpvSnI8A/XSe0pZshOt/snapcraft-2.35/integration_tests/snaps/zip/simple.zip differ diff -Nru snapcraft-2.34/integration_tests/snaps/zip/snapcraft.yaml snapcraft-2.35/integration_tests/snaps/zip/snapcraft.yaml --- snapcraft-2.34/integration_tests/snaps/zip/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/snaps/zip/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -11,4 +11,4 @@ zip-source-checksum: plugin: dump source: simple.zip - source-checksum: sha256/035ae7da4bd0ff39960466353e0810f51d17193a13e8b75e767391820aed484c + source-checksum: sha256/e8eae6f110b62687f21eddfca149d638f80d48e2bf4654771af46c7218f0d27e diff -Nru snapcraft-2.34/integration_tests/store/test_store_download.py snapcraft-2.35/integration_tests/store/test_store_download.py --- snapcraft-2.34/integration_tests/store/test_store_download.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/store/test_store_download.py 2017-11-01 19:41:33.000000000 +0000 @@ -26,7 +26,7 @@ def setUp(self): if os.getenv('TEST_STORE') == 'staging': # TODO add the snap to the staging server. - self.skipTest('There is no ubuntu-core snap in the staging server') + self.skipTest('There is no core snap in the staging server') super().setUp() def test_download_os_snap(self): diff -Nru snapcraft-2.34/integration_tests/store/test_store_gated.py snapcraft-2.35/integration_tests/store/test_store_gated.py --- snapcraft-2.34/integration_tests/store/test_store_gated.py 2017-09-10 17:25:47.000000000 +0000 +++ snapcraft-2.35/integration_tests/store/test_store_gated.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,8 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os - from testtools.matchers import Equals import integration_tests @@ -24,21 +22,21 @@ class GatedTestCase(integration_tests.StoreTestCase): def setUp(self): - if os.getenv('TEST_STORE', 'fake') != 'fake': + super().setUp() + if not self.is_store_fake(): self.skipTest('Right combination of snaps and IDs is not ' 'available in real stores.') - super().setUp() def test_gated_success(self): self.addCleanup(self.logout) self.login() validations = [('snap-1', '3'), ('snap-2', '5')] - self.assertThat(self.gated('ubuntu-core', validations), Equals(0)) + self.assertThat(self.gated('core', validations), Equals(0)) def test_gated_no_login_failure(self): self.assertThat( self.gated( - 'ubuntu-core', + 'core', expected_output='Have you run "snapcraft login'), Equals(2)) diff -Nru snapcraft-2.34/integration_tests/store/test_store_register_key.py snapcraft-2.35/integration_tests/store/test_store_register_key.py --- snapcraft-2.34/integration_tests/store/test_store_register_key.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/store/test_store_register_key.py 2017-11-01 19:41:33.000000000 +0000 @@ -34,7 +34,7 @@ 'SNAP_GNUPG_HOME', temp_keys_dir)) def test_successful_key_registration(self): - if os.getenv('TEST_STORE', 'fake') != 'fake': + if not self.is_store_fake(): # https://bugs.launchpad.net/bugs/1621441 self.skipTest( 'Cannot register test keys against staging/production until ' diff -Nru snapcraft-2.34/integration_tests/store/test_store_register.py snapcraft-2.35/integration_tests/store/test_store_register.py --- snapcraft-2.34/integration_tests/store/test_store_register.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/store/test_store_register.py 2017-11-01 19:41:33.000000000 +0000 @@ -13,7 +13,7 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os + import re from testtools.matchers import Contains, MatchesRegex @@ -50,7 +50,7 @@ def test_registration_of_already_owned_name(self): self.login() self.addCleanup(self.logout) - if os.getenv('TEST_STORE', 'fake') != 'fake': + if not self.is_store_fake(): snap_name = self.get_unique_name() self.register(snap_name) else: diff -Nru snapcraft-2.34/integration_tests/store/test_store_revisions.py snapcraft-2.35/integration_tests/store/test_store_revisions.py --- snapcraft-2.34/integration_tests/store/test_store_revisions.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/store/test_store_revisions.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,10 +14,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os import re import subprocess -import unittest from testtools.matchers import Contains, Equals, FileExists, MatchesRegex @@ -64,9 +62,10 @@ "Snap 'mysnap' for 'i386' was not found in '16' series.", str(error.output)) - @unittest.skipUnless( - os.getenv('TEST_STORE', 'fake') == 'fake', 'Skip fake store.') def test_revisions_fake_store(self): + if not self.is_store_fake(): + self.skipTest('This test only works in the fake store') + self.addCleanup(self.logout) self.login() @@ -78,9 +77,10 @@ )) self.assertThat(output, Contains(expected)) - @unittest.skipUnless( - os.getenv('TEST_STORE', 'fake') == 'fake', 'Skip fake store.') def test_list_revisions_fake_store(self): + if not self.is_store_fake(): + self.skipTest('This test only works in the fake store') + self.addCleanup(self.logout) self.login() @@ -92,9 +92,10 @@ )) self.assertThat(output, Contains(expected)) - @unittest.skipUnless( - os.getenv('TEST_STORE', 'fake') == 'fake', 'Skip fake store.') def test_history_fake_store(self): + if not self.is_store_fake(): + self.skipTest('This test only works in the fake store') + self.addCleanup(self.logout) self.login() @@ -110,9 +111,10 @@ "DEPRECATED: The 'history' command has " "been replaced by 'list-revisions'.")) - @unittest.skipUnless( - os.getenv('TEST_STORE', 'fake') == 'staging', 'Skip staging store.') def test_revisions_staging_store(self): + if not self.is_store_staging(): + self.skipTest('This test only works in the staging store') + self.addCleanup(self.logout) self.login() diff -Nru snapcraft-2.34/integration_tests/store/test_store_sign_build.py snapcraft-2.35/integration_tests/store/test_store_sign_build.py --- snapcraft-2.34/integration_tests/store/test_store_sign_build.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/store/test_store_sign_build.py 2017-11-01 19:41:33.000000000 +0000 @@ -82,7 +82,7 @@ self.assertThat(self.snap_build_path, Not(FileExists())) def test_successful_sign_build_push(self): - if os.getenv('TEST_STORE', 'fake') != 'fake': + if not self.is_store_fake(): # https://bugs.launchpad.net/bugs/1621441 self.skipTest( 'Cannot push signed assertion against staging/production ' diff -Nru snapcraft-2.34/integration_tests/store/test_store_status.py snapcraft-2.35/integration_tests/store/test_store_status.py --- snapcraft-2.34/integration_tests/store/test_store_status.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/store/test_store_status.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,9 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os import subprocess -import unittest from testtools.matchers import Equals, Contains, FileExists @@ -63,9 +61,10 @@ "Snap 'mysnap' for 'i386' was not found in '16' series.", str(error.output)) - @unittest.skipUnless( - os.getenv('TEST_STORE', 'fake') == 'fake', 'Skip fake store.') def test_status_fake_store(self): + if not self.is_store_fake(): + self.skipTest('This test only works in the fake store') + self.addCleanup(self.logout) self.login() @@ -80,9 +79,10 @@ ' edge 1.0-i386 3')) self.assertThat(output, Contains(expected)) - @unittest.skipUnless( - os.getenv('TEST_STORE', 'fake') == 'staging', 'Skip staging store.') def test_status_staging_store(self): + if not self.is_store_staging(): + self.skipTest('This test only works in the staging store') + self.addCleanup(self.logout) self.login() diff -Nru snapcraft-2.34/integration_tests/store/test_store_validate.py snapcraft-2.35/integration_tests/store/test_store_validate.py --- snapcraft-2.34/integration_tests/store/test_store_validate.py 2017-09-10 17:25:47.000000000 +0000 +++ snapcraft-2.35/integration_tests/store/test_store_validate.py 2017-11-01 19:41:33.000000000 +0000 @@ -26,10 +26,11 @@ class ValidateTestCase(integration_tests.StoreTestCase): def setUp(self): - if os.getenv('TEST_STORE', 'fake') != 'fake': + super().setUp() + if not self.is_store_fake(): self.skipTest('Right combination of snaps and IDs is not ' 'available in real stores.') - super().setUp() + keys_dir = os.path.join(os.path.dirname(__file__), 'keys') temp_keys_dir = os.path.join(self.path, '.snap', 'gnupg') shutil.copytree(keys_dir, temp_keys_dir) @@ -40,15 +41,14 @@ self.addCleanup(self.logout) self.login() self.assertThat( - self.validate('ubuntu-core', [ - "ubuntu-core=3", "test-snap=4"]), + self.validate('core', ["core=3", "test-snap=4"]), Equals(0)) def test_validate_unknown_snap_failure(self): self.addCleanup(self.logout) self.login() self.assertThat( - self.validate('unknown', ["ubuntu-core=3", "test-snap=4"], + self.validate('unknown', ["core=3", "test-snap=4"], expected_error="Snap 'unknown' was not found."), Equals(2)) @@ -56,12 +56,12 @@ self.addCleanup(self.logout) self.login() self.assertThat( - self.validate('ubuntu-core', ["ubuntu-core=foo"], + self.validate('core', ["core=foo"], expected_error='format must be name=revision'), Equals(2)) def test_validate_no_login_failure(self): self.assertThat( - self.validate('ubuntu-core', ["ubuntu-core=3", "test-snap=4"], + self.validate('core', ["core=3", "test-snap=4"], expected_error='Have you run "snapcraft login"'), Equals(2)) diff -Nru snapcraft-2.34/integration_tests/test_asset_recording.py snapcraft-2.35/integration_tests/test_asset_recording.py --- snapcraft-2.34/integration_tests/test_asset_recording.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/test_asset_recording.py 2017-11-01 19:41:33.000000000 +0000 @@ -17,14 +17,18 @@ import filecmp import os import subprocess +import sys import yaml +import apt import fixtures import testscenarios -from testtools.matchers import Equals +from testtools.matchers import Contains, Equals import snapcraft import integration_tests +from snapcraft.internal.repo import snaps +from snapcraft.tests import fixture_setup class AssetRecordingBaseTestCase(integration_tests.TestCase): @@ -55,6 +59,54 @@ class ManifestRecordingTestCase(AssetRecordingBaseTestCase): + def test_prime_records_uname(self): + self.run_snapcraft('prime', project_dir='basic') + + recorded_yaml_path = os.path.join( + self.prime_dir, 'snap', 'manifest.yaml') + with open(recorded_yaml_path) as recorded_yaml_file: + recorded_yaml = yaml.load(recorded_yaml_file) + + expected_uname = subprocess.check_output( + ['uname', '-srvmpio']).decode(sys.getfilesystemencoding()).strip() + self.assertThat( + recorded_yaml['parts']['dummy-part']['uname'], + Equals(expected_uname)) + + def test_prime_records_installed_packages(self): + self.run_snapcraft('prime', project_dir='basic') + + recorded_yaml_path = os.path.join( + self.prime_dir, 'snap', 'manifest.yaml') + with open(recorded_yaml_path) as recorded_yaml_file: + recorded_yaml = yaml.load(recorded_yaml_file) + + with apt.Cache() as apt_cache: + expected_package = 'python3={}'.format( + apt_cache['python3'].installed.version) + self.assertThat( + recorded_yaml['parts']['dummy-part']['installed-packages'], + Contains(expected_package)) + + def test_prime_records_installed_snaps(self): + if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': + self.skipTest("The autopkgtest armhf runners can't install snaps") + + subprocess.check_call(['sudo', 'snap', 'install', 'core']) + self.run_snapcraft('prime', project_dir='basic') + + recorded_yaml_path = os.path.join( + self.prime_dir, 'snap', 'manifest.yaml') + with open(recorded_yaml_path) as recorded_yaml_file: + recorded_yaml = yaml.load(recorded_yaml_file) + + expected_package = 'core={}'.format( + snaps.SnapPackage( + 'core').get_local_snap_info()['revision']) + self.assertThat( + recorded_yaml['parts']['dummy-part']['installed-snaps'], + Contains(expected_package)) + def test_prime_with_architectures(self): """Test the recorded manifest for a basic snap @@ -88,6 +140,31 @@ recorded_yaml['architectures'], Equals([snapcraft.ProjectOptions().deb_arch])) + def test_prime_records_build_snaps(self): + if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': + self.skipTest("The autopkgtest armhf runners can't install snaps") + + self.useFixture(fixture_setup.WithoutSnapInstalled('hello')) + snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) + snapcraft_yaml.update_part('test-part', { + 'plugin': 'nil', + 'build-snaps': ['hello'] + }) + self.useFixture(snapcraft_yaml) + + self.run_snapcraft('prime') + + expected_revision = snaps.SnapPackage( + 'hello').get_local_snap_info()['revision'] + recorded_yaml_path = os.path.join( + self.prime_dir, 'snap', 'manifest.yaml') + with open(recorded_yaml_path) as recorded_yaml_file: + recorded_yaml = yaml.load(recorded_yaml_file) + + self.assertThat( + recorded_yaml['build-snaps'], + Equals(['hello={}'.format(expected_revision)])) + class ManifestRecordingBuildPackagesTestCase( testscenarios.WithScenarios, AssetRecordingBaseTestCase): diff -Nru snapcraft-2.34/integration_tests/test_build_snap_grammar.py snapcraft-2.35/integration_tests/test_build_snap_grammar.py --- snapcraft-2.34/integration_tests/test_build_snap_grammar.py 2017-09-10 17:25:47.000000000 +0000 +++ snapcraft-2.35/integration_tests/test_build_snap_grammar.py 2017-11-01 19:41:33.000000000 +0000 @@ -22,6 +22,7 @@ import integration_tests import snapcraft +from snapcraft.tests import fixture_setup def _construct_scenarios(): @@ -62,26 +63,7 @@ if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': self.skipTest('snap installation not working well with ' 'adt on armhf') - if self._hello_is_installed(): - self.fail( - 'This integration test cannot run if you already have the ' - "'hello' snap installed. Please uninstall it " - "by running 'sudo snap remove hello'.") - - def tearDown(self): - super().tearDown() - - # Remove hello. This is safe since the test fails if hello was already - # installed. - try: - subprocess.check_output( - ['sudo', 'snap', 'remove', 'hello'], - stderr=subprocess.STDOUT) - except subprocess.CalledProcessError as e: - self.fail("unable to remove 'hello': {}".format(e.output)) - - def _hello_is_installed(self): - return snapcraft.repo.snaps.SnapPackage.is_snap_installed('hello') + self.useFixture(fixture_setup.WithoutSnapInstalled('hello')) def _add_channel_information_to_hello(self): replacement = '- hello{}'.format(self.channel) @@ -95,8 +77,9 @@ self.run_snapcraft('pull') - self.assertThat(self._hello_is_installed(), - Equals(self.hello_installed)) + self.assertThat( + snapcraft.repo.snaps.SnapPackage.is_snap_installed('hello'), + Equals(self.hello_installed)) class BuildSnapGrammarErrorsTestCase(integration_tests.TestCase): diff -Nru snapcraft-2.34/integration_tests/test_build_snaps.py snapcraft-2.35/integration_tests/test_build_snaps.py --- snapcraft-2.34/integration_tests/test_build_snaps.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/test_build_snaps.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,47 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os + +import testscenarios + +import integration_tests +import snapcraft +from snapcraft.tests import fixture_setup + + +class BuildSnapsTestCase( + testscenarios.WithScenarios, integration_tests.TestCase): + + scenarios = ( + ('snap name', {'snap': 'u1test-snap-with-tracks'}), + ('snap name with track and risk', + {'snap': 'u1test-snap-with-tracks/test-track-1/beta'})) + + def test_build_snap(self): + if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': + self.skipTest("The autopkgtest armhf runners can't install snaps") + self.useFixture(fixture_setup.WithoutSnapInstalled(self.snap)) + snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) + snapcraft_yaml.update_part( + 'test-part-with-build-snap', { + 'plugin': 'nil', + 'build-snaps': [self.snap] + }) + self.useFixture(snapcraft_yaml) + self.run_snapcraft('build') + self.assertTrue( + snapcraft.repo.snaps.SnapPackage.is_snap_installed(self.snap)) diff -Nru snapcraft-2.34/integration_tests/test_catkin.py snapcraft-2.35/integration_tests/test_catkin.py --- snapcraft-2.34/integration_tests/test_catkin.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/integration_tests/test_catkin.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,56 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os +import subprocess +import tempfile + +import integration_tests + +from snapcraft.tests import skip + + +class CatkinTestCase(integration_tests.TestCase): + + @skip.skip_unless_codename('xenial', 'ROS Kinetic only targets Xenial') + def test_shared_ros_builds_without_catkin_in_underlay(self): + # Build the producer until we have a good staging area + self.copy_project_to_cwd(os.path.join('catkin-shared-ros', 'producer')) + self.run_snapcraft('stage') + + with tempfile.TemporaryDirectory() as tmpdir: + underlay_tarball = os.path.join(tmpdir, 'underlay.tar.bz2') + + # Now tar up the producer's staging area to be used in the consumer + subprocess.check_call(['tar', 'czf', underlay_tarball, 'stage/']) + + # Blow away the entire producer project + self.run_snapcraft('clean') + subprocess.check_call(['rm', '-rf', '*']) + + # Copy the tarball back into cwd + os.rename(underlay_tarball, 'underlay.tar.bz2') + + # Now copy in and build the consumer. This should not throw exceptions. + self.copy_project_to_cwd(os.path.join('catkin-shared-ros', 'consumer')) + self.run_snapcraft('build') + + @skip.skip_unless_codename('xenial', 'ROS Kinetic only targets Xenial') + def test_catkin_part_builds_after_python_part(self): + self.copy_project_to_cwd('catkin-with-python-part') + + # This snap should be staged with no errors + self.run_snapcraft('stage') diff -Nru snapcraft-2.34/integration_tests/test_snap.py snapcraft-2.35/integration_tests/test_snap.py --- snapcraft-2.34/integration_tests/test_snap.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/test_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -16,6 +16,7 @@ import os import subprocess +from textwrap import dedent import fixtures import testtools @@ -74,6 +75,11 @@ os.path.join(self.prime_dir, 'bin', 'not-wrapped.wrapper'), Not(FileExists())) + self.assertThat( + os.path.join(self.prime_dir, 'bin', + 'command-binary-wrapper-none.wrapper.wrapper'), + Not(FileExists())) + def test_snap_default(self): self.copy_project_to_cwd('assemble') self.run_snapcraft([]) @@ -81,16 +87,6 @@ snap_file_path = 'assemble_1.0_{}.snap'.format(self.deb_arch) self.assertThat(snap_file_path, FileExists()) - def test_cleanbuild(self): - self.skipTest("Fails to run correctly on travis.") - self.run_snapcraft('cleanbuild', 'assemble') - - snap_source_path = 'assemble_1.0_source.tar.bz2' - self.assertThat(snap_source_path, FileExists()) - - snap_file_path = 'assemble_1.0_{}.snap'.format(self.deb_arch) - self.assertThat(snap_file_path, FileExists()) - def test_snap_directory(self): self.copy_project_to_cwd('assemble') self.run_snapcraft('snap') @@ -104,6 +100,19 @@ self.run_snapcraft(['snap', 'prime']) self.assertThat(snap_file_path, FileExists()) + def test_pack_directory(self): + self.copy_project_to_cwd('assemble') + self.run_snapcraft('snap') + + snap_file_path = 'assemble_1.0_{}.snap'.format(self.deb_arch) + os.remove(snap_file_path) + + # Verify that Snapcraft can snap its own snap directory (this will make + # sure `snapcraft snap` and `snapcraft pack ` are always + # in sync). + self.run_snapcraft(['pack', 'prime']) + self.assertThat(snap_file_path, FileExists()) + def test_snap_long_output_option(self): self.run_snapcraft(['snap', '--output', 'mysnap.snap'], 'assemble') self.assertThat('mysnap.snap', FileExists()) @@ -184,3 +193,47 @@ os.path.join(self.stage_dir, 'test.txt'), FileExists() ) + + def test_ordered_snap_yaml(self): + with open('snapcraft.yaml', 'w') as s: + s.write(dedent("""\ + apps: + stub-app: + command: sh + grade: stable + version: "2" + assumes: [snapd_227] + architectures: [all] + description: stub description + summary: stub summary + confinement: strict + name: stub-snap + environment: + stub_key: stub-value + epoch: 1 + parts: + nothing: + plugin: nil + """)) + self.run_snapcraft('prime') + + expected_snap_yaml = dedent("""\ + name: stub-snap + version: '2' + summary: stub summary + description: stub description + architectures: + - all + confinement: strict + grade: stable + assumes: + - snapd_227 + epoch: 1 + environment: + stub_key: stub-value + apps: + stub-app: + command: command-stub-app.wrapper + """) + self.assertThat(os.path.join('prime', 'meta', 'snap.yaml'), + FileContains(expected_snap_yaml)) diff -Nru snapcraft-2.34/integration_tests/test_zip_source.py snapcraft-2.35/integration_tests/test_zip_source.py --- snapcraft-2.34/integration_tests/test_zip_source.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/integration_tests/test_zip_source.py 2017-11-01 19:41:33.000000000 +0000 @@ -17,6 +17,7 @@ import os from testtools.matchers import ( + Equals, DirExists, FileExists ) @@ -31,6 +32,7 @@ self.run_snapcraft('stage') expected_files = [ + 'exec', 'top-simple', os.path.join('dir-simple', 'sub') ] @@ -38,6 +40,9 @@ self.assertThat( os.path.join(self.stage_dir, expected_file), FileExists()) + self.assertThat(os.access( + os.path.join(self.stage_dir, 'exec'), os.X_OK), + Equals(True)) expected_dirs = [ 'dir-simple', ] diff -Nru snapcraft-2.34/libraries/generate_lib_list.py snapcraft-2.35/libraries/generate_lib_list.py --- snapcraft-2.34/libraries/generate_lib_list.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/libraries/generate_lib_list.py 2017-11-01 19:41:33.000000000 +0000 @@ -30,7 +30,7 @@ config = load_config() with tempfile.NamedTemporaryFile() as temp: print('Downloading') - download('ubuntu-core', 'stable', temp.name, config, 'amd64') + download('core', 'stable', temp.name, config, 'amd64') lib_list = generate_list(temp.name) lib_list = ('{}\n'.format(l) for l in lib_list) diff -Nru snapcraft-2.34/runtests.sh snapcraft-2.35/runtests.sh --- snapcraft-2.34/runtests.sh 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/runtests.sh 2017-11-01 19:41:33.000000000 +0000 @@ -18,7 +18,7 @@ set -e export PATH=$(pwd)/bin:$PATH -export PYTHONPATH=$(pwd):$PYTHONPATH +export PYTHONPATH=$(pwd)${PYTHONPATH:+:$PYTHONPATH} parseargs(){ if [[ "$#" -eq 0 ]] || [[ "$1" == "all" ]]; then @@ -27,6 +27,8 @@ export RUN_INTEGRATION="true" export RUN_STORE="true" export RUN_PLUGINS="true" + export RUN_CONTAINERS="true" + export RUN_SNAPD="true" export RUN_SNAPS="true" export RUN_SPREAD="true" else @@ -40,6 +42,10 @@ export RUN_STORE="true" elif [ "$1" == "plugins" ] ; then export RUN_PLUGINS="true" + elif [ "$1" == "containers" ] ; then + export RUN_CONTAINERS="true" + elif [ "$1" == "snapd" ] ; then + export RUN_SNAPD="true" elif [ "$1" == "snaps" ] ; then export RUN_SNAPS="true" # Temporary: backward compatibility until CI run the "snaps" target @@ -48,7 +54,7 @@ elif [ "$1" == "spread" ] ; then export RUN_SPREAD="true" else - echo "Not recognized option, should be one of all, static, unit, integration, store, snaps or spread" + echo "Not recognized option, should be one of all, static, unit, integration, store, plugins, containers, snapd, snaps or spread" exit 1 fi fi @@ -57,7 +63,7 @@ python3 -m coverage 1>/dev/null 2>&1 && coverage="true" run_static_tests(){ - SRC_PATHS="bin snapcraft integration_tests snaps_tests external_snaps_tests" + SRC_PATHS="bin snapcraft integration_tests snaps_tests external_snaps_tests setup.py" python3 -m flake8 --max-complexity=10 $SRC_PATHS } @@ -103,6 +109,24 @@ python3 -m unittest discover -b -v -s integration_tests/plugins -p $pattern } +run_containers(){ + if [[ "$#" -lt 2 ]]; then + pattern="test_*.py" + else + pattern=$2 + fi + python3 -m unittest discover -b -v -s integration_tests/containers -p $pattern +} + +run_snapd(){ + if [[ "$#" -lt 2 ]]; then + pattern="test_*.py" + else + pattern=$2 + fi + python3 -m unittest discover -b -v -s integration_tests/snapd -p $pattern +} + run_snaps(){ python3 -m snaps_tests "$@" } @@ -138,6 +162,14 @@ run_plugins "$@" fi +if [ ! -z "$RUN_CONTAINERS" ]; then + run_containers "$@" +fi + +if [ ! -z "$RUN_SNAPD" ]; then + run_snapd "$@" +fi + if [ ! -z "$RUN_SNAPS" ]; then if [ "$1" == "snaps" ] ; then # shift to remove the test suite name and be able to pass the rest diff -Nru snapcraft-2.34/schema/snapcraft.yaml snapcraft-2.35/schema/snapcraft.yaml --- snapcraft-2.34/schema/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/schema/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -37,7 +37,11 @@ name: type: string description: name of the snap package - pattern: "^[a-z0-9][a-z0-9+-]*$" + validation-failure: + "{.instance!r} is not a valid snap name. Snap names consist of lower-case + alphanumeric characters and hyphens. They cannot be all numbers. They + also cannot start or end with a hyphen." + pattern: "^(?:[a-z0-9]+-?)*[a-z](?:-?[a-z0-9])*$" architectures: type: array description: architectures to override with @@ -49,6 +53,10 @@ # python's defaul yaml loading code loads 1.0 as an int # type: string description: package version + validation-failure: + "{.instance!r} is not a valid snap version. Snap versions consist of + upper- and lower-case alphanumeric characters, as well as periods, plus + signs, tildes, and hyphens." pattern: "^[a-zA-Z0-9.+~-]+$" maxLength: 32 version-script: @@ -120,6 +128,10 @@ apps: type: object additionalProperties: false + validation-failure: + "{!r} is not a valid app name. App names consist of upper- and + lower-case alphanumeric characters and hyphens. They cannot start + or end with a hyphen." patternProperties: "^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$": type: object @@ -194,11 +206,20 @@ - type: string minLength: 1 - type: number + adapter: + type: string + description: What kind of wrapper to generate for the given command + enum: + - none hooks: type: object additionalProperties: false + validation-failure: + "{!r} is not a valid hook name. Hook names consist of lower-case + alphanumeric characters and hyphens. They cannot start or end with a + hyphen." patternProperties: - "^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$": + "^[a-z](?:-?[a-z0-9])*$": type: object additionalProperties: false properties: @@ -212,6 +233,10 @@ type: object minProperties: 1 additionalProperties: false + validation-failure: + "{!r} is not a valid part name. Part names consist of lower-case + alphanumeric characters, hyphens, plus signs, and forward slashes. As a + special case, 'plugins' is also not a valid part name." patternProperties: ^(?!plugins$)[a-z0-9][a-z0-9+-\/]*$: # Make sure snap/prime are mutually exclusive @@ -219,7 +244,8 @@ - not: type: object required: [snap, prime] - validation-failure: "{.instance} cannot contain both 'snap' and 'prime' keywords." + validation-failure: + "{.instance} cannot contain both 'snap' and 'prime' keywords." type: object minProperties: 1 properties: diff -Nru snapcraft-2.34/setup.py snapcraft-2.35/setup.py --- snapcraft-2.34/setup.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/setup.py 2017-11-01 19:41:33.000000000 +0000 @@ -22,43 +22,46 @@ # Common distribution data -name='snapcraft' +name = 'snapcraft' version = 'devel' description = 'Easily craft snaps from multiple sources' author_email = 'snapcraft@lists.snapcraft.io' url = 'https://github.com/snapcore/snapcraft' packages = [ - 'snapcraft', - 'snapcraft.cli', - 'snapcraft.integrations', - 'snapcraft.internal', - 'snapcraft.internal.cache', - 'snapcraft.internal.deltas', - 'snapcraft.internal.pluginhandler', - 'snapcraft.internal.project_loader', - 'snapcraft.internal.project_loader.grammar', - 'snapcraft.internal.project_loader.grammar_processing', - 'snapcraft.internal.repo', - 'snapcraft.internal.sources', - 'snapcraft.internal.states', - 'snapcraft.plugins', - 'snapcraft.plugins._ros', - 'snapcraft.storeapi' - ] + 'snapcraft', + 'snapcraft.cli', + 'snapcraft.integrations', + 'snapcraft.internal', + 'snapcraft.internal.cache', + 'snapcraft.internal.deltas', + 'snapcraft.internal.lifecycle', + 'snapcraft.internal.lxd', + 'snapcraft.internal.pluginhandler', + 'snapcraft.internal.project_loader', + 'snapcraft.internal.project_loader.grammar', + 'snapcraft.internal.project_loader.grammar_processing', + 'snapcraft.internal.repo', + 'snapcraft.internal.sources', + 'snapcraft.internal.states', + 'snapcraft.plugins', + 'snapcraft.plugins._ros', + 'snapcraft.plugins._python', + 'snapcraft.storeapi' +] package_data = {'snapcraft.internal.repo': ['manifest.txt']} license = 'GPL v3' classifiers = ( - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Developers', - 'Intended Audience :: System Administrators', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Natural Language :: English', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Topic :: Software Development :: Build Tools', - 'Topic :: System :: Software Distribution', + 'Development Status :: 4 - Beta', + 'Environment :: Console', + 'Intended Audience :: Developers', + 'Intended Audience :: System Administrators', + 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Natural Language :: English', + 'Programming Language :: Python', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Topic :: Software Development :: Build Tools', + 'Topic :: System :: Software Distribution', ) # look/set what version we have @@ -72,80 +75,79 @@ # If on Windows, construct an exe distribution if sys.platform == 'win32': - from cx_Freeze import setup, Executable - - # cx_Freeze relevant options - build_exe_options = { - # Explicitly add any missed packages that are not found at runtime - 'packages': [ - 'pkg_resources', - 'pymacaroons', - 'click', - 'responses', - 'configparser', - 'docopt', - 'cffi', - ], - # Explicit inclusion data, which is then clobbered. - 'include_files' : [ - ('libraries', os.path.join('share', 'snapcraft', 'libraries')), - ('schema', os.path.join('share', 'snapcraft', 'schema')), - ], - } - - exe = Executable( - script='bin/snapcraft', - base=None # console subsystem - ) - - setup( - name=name, - version=version, - description=description, - author_email=author_email, - url=url, - packages=packages, - package_data=package_data, - license=license, - classifiers=classifiers, - # cx_Freeze-specific arguments - options={'build_exe': build_exe_options}, - executables=[exe], - ) + from cx_Freeze import setup, Executable + + # cx_Freeze relevant options + build_exe_options = { + # Explicitly add any missed packages that are not found at runtime + 'packages': [ + 'pkg_resources', + 'pymacaroons', + 'click', + 'responses', + 'configparser', + 'docopt', + 'cffi', + ], + # Explicit inclusion data, which is then clobbered. + 'include_files': [ + ('libraries', os.path.join('share', 'snapcraft', 'libraries')), + ('schema', os.path.join('share', 'snapcraft', 'schema')), + ], + } + + exe = Executable( + script='bin/snapcraft', + base=None # console subsystem + ) + + setup( + name=name, + version=version, + description=description, + author_email=author_email, + url=url, + packages=packages, + package_data=package_data, + license=license, + classifiers=classifiers, + # cx_Freeze-specific arguments + options={'build_exe': build_exe_options}, + executables=[exe], + ) # On other platforms, continue as normal else: - from setuptools import setup - - setup( - name=name, - version=version, - description=description, - author_email=author_email, - url=url, - packages=packages, - package_data=package_data, - license=license, - classifiers=classifiers, - # non-cx_Freeze arguments - entry_points={ - 'console_scripts': [ - 'snapcraft = snapcraft.cli.__main__:run', - 'snapcraft-parser = snapcraft.internal.parser:main', - ], - }, - data_files=[ - ('share/snapcraft/schema', - ['schema/' + x for x in os.listdir('schema')]), - ('share/snapcraft/libraries', - ['libraries/' + x for x in os.listdir('libraries')]), - ], - install_requires=[ - 'pysha3', - 'pyxdg', - 'requests', - 'libarchive-c', - ], - test_suite='snapcraft.tests', - ) + from setuptools import setup + setup( + name=name, + version=version, + description=description, + author_email=author_email, + url=url, + packages=packages, + package_data=package_data, + license=license, + classifiers=classifiers, + # non-cx_Freeze arguments + entry_points={ + 'console_scripts': [ + 'snapcraft = snapcraft.cli.__main__:run', + 'snapcraft-parser = snapcraft.internal.parser:main', + ], + }, + data_files=[ + ('share/snapcraft/schema', + ['schema/' + x for x in os.listdir('schema')]), + ('share/snapcraft/libraries', + ['libraries/' + x for x in os.listdir('libraries')]), + ], + install_requires=[ + 'pysha3', + 'pyxdg', + 'requests', + 'libarchive-c', + ], + test_suite='snapcraft.tests', + ) diff -Nru snapcraft-2.34/snap/snapcraft.yaml snapcraft-2.35/snap/snapcraft.yaml --- snapcraft-2.34/snap/snapcraft.yaml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snap/snapcraft.yaml 2017-11-01 19:41:33.000000000 +0000 @@ -68,12 +68,12 @@ prepare: | make startup build: | - mkdir build - cd build + mkdir apt-build + cd apt-build ../configure make install: | - cd build + cd apt-build install -d $SNAPCRAFT_PART_INSTALL/apt cp -r bin/methods/* $SNAPCRAFT_PART_INSTALL/apt/ cp -r bin/methods/* $SNAPCRAFT_PART_INSTALL/apt/ diff -Nru snapcraft-2.34/snapcraft/cli/assertions.py snapcraft-2.35/snapcraft/cli/assertions.py --- snapcraft-2.34/snapcraft/cli/assertions.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/cli/assertions.py 2017-11-01 19:41:33.000000000 +0000 @@ -13,10 +13,34 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os +import subprocess +import tempfile +from datetime import datetime +from textwrap import dedent import click +import yaml import snapcraft +from . import echo +from snapcraft import storeapi +from snapcraft.storeapi import assertions + + +_COLLABORATION_HEADER = dedent("""\ + # Change which developers may push or release snaps on the publisher's behalf. + # + # Sample entry: + # + # developers: + # - developer-id: "dev-one" # Which developer + # since: "2017-02-10 08:35:00" # When contributions started + # until: "2018-02-10 08:35:00" # When contributions ceased (optional) + # + # All timestamps are UTC, and the "now" special string will be replaced by + # the current time. Do not remove entries or use an until time in the past + # unless you want existing snaps provided by the developer to stop working.""") # noqa @click.group() @@ -77,3 +101,90 @@ def gated(snap_name): """Get the list of snaps and revisions gating a snap.""" snapcraft.gated(snap_name) + + +@assertionscli.command('edit-collaborators') +@click.argument('snap-name', metavar='') +@click.option('--key-name', metavar='') +def edit_collaborators(snap_name, key_name): + """Edit the list of collaborators for . + + This command has an alias of `collaborators`. + """ + dev_assertion = assertions.DeveloperAssertion( + snap_name=snap_name, signing_key=key_name) + developers = dev_assertion.get_developers() + updated_developers = _update_developers(developers) + new_dev_assertion = dev_assertion.new_assertion( + developers=updated_developers) + if new_dev_assertion.is_equal(dev_assertion): + echo.warning('Aborting due to unchanged collaborators list.') + return + + try: + new_dev_assertion.push() + except storeapi.errors.StoreValidationError as store_error: + if store_error.error_list[0]['code'] != 'revoked-uploads': + raise store_error + click.echo('This will revoke the following collaborators: {!r}'.format( + ' '.join(store_error.error_list[0]['extra']))) + if click.confirm('Are you sure you want to continue?'): + new_dev_assertion.push(force=True) + else: + echo.warning('The collaborators for this snap have not been ' + 'altered.') + + +def _update_developers(developers): + edit_friendly_developers = _reformat_time_for_editing(developers) + updated_developers = _edit_developers(edit_friendly_developers) + return _reformat_time_for_assertion(updated_developers) + + +def _edit_developers(developers): + """Spawn an editor to modify the snap-developer assertion for a snap.""" + editor_cmd = os.getenv('EDITOR', 'vi') + + developer_wrapper = {'developers': developers} + + with tempfile.NamedTemporaryFile() as ft: + ft.close() + with open(ft.name, 'w') as fw: + print(_COLLABORATION_HEADER, file=fw) + yaml.dump(developer_wrapper, stream=fw, default_flow_style=False) + subprocess.check_call([editor_cmd, ft.name]) + with open(ft.name, 'r') as fr: + developers = yaml.load(fr).get('developers') + return developers + + +def _reformat_time_for_editing(developers, time_format='%Y-%m-%d %H:%M:%S'): + reformatted_developers = [] + for developer in developers: + developer_it = {'developer-id': developer['developer-id']} + for range in ['since', 'until']: + if range in developer: + date = datetime.strptime(developer[range], + '%Y-%m-%dT%H:%M:%S.%fZ') + developer_it[range] = datetime.strftime(date, time_format) + reformatted_developers.append(developer_it) + return reformatted_developers + + +def _reformat_time_for_assertion(developers): + reformatted_developers = [] + for developer in developers: + developer_it = {'developer-id': developer['developer-id']} + for range_ in ['since', 'until']: + if range_ in developer: + if developer[range_] == 'now': + date = datetime.now() + else: + date = datetime.strptime(developer[range_], + '%Y-%m-%d %H:%M:%S') + # We don't care about microseconds because we cannot edit + # later so we set that to 0. + developer_it[range_] = date.strftime( + '%Y-%m-%dT%H:%M:%S.000000Z') + reformatted_developers.append(developer_it) + return reformatted_developers diff -Nru snapcraft-2.34/snapcraft/cli/containers.py snapcraft-2.35/snapcraft/cli/containers.py --- snapcraft-2.34/snapcraft/cli/containers.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/cli/containers.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,59 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import click +import os + +import snapcraft +from snapcraft.internal import errors, lxd +from ._options import get_project_options +from . import env + + +@click.group() +def containerscli(): + pass + + +@containerscli.command() +@click.option('--debug', is_flag=True, + help='Shells into the environment if the build fails.') +def refresh(debug, **kwargs): + """Refresh an existing LXD container. + + \b + Examples: + snapcraft refresh + + This will take care of updating the apt package cache, upgrading packages + as needed as well as refreshing snaps. + """ + + container_config = env.get_container_config() + if not container_config.use_container: + raise errors.SnapcraftEnvironmentError( + "The 'refresh' command only applies to LXD containers but " + "SNAPCRAFT_CONTAINER_BUILDS is not set or 0.\n" + "Maybe you meant to update the parts cache instead? " + "You can do that with the following command:\n\n" + "snapcraft update") + + project_options = get_project_options(**kwargs, debug=debug) + config = snapcraft.internal.load_config(project_options) + lxd.Project(project_options=project_options, + remote=container_config.remote, + output=None, source=os.path.curdir, + metadata=config.get_metadata()).refresh() diff -Nru snapcraft-2.34/snapcraft/cli/env.py snapcraft-2.35/snapcraft/cli/env.py --- snapcraft-2.34/snapcraft/cli/env.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/cli/env.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,7 +14,42 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os +from distutils import util +from snapcraft.internal import errors, lxd -def is_containerbuild(): - return os.environ.get('SNAPCRAFT_CONTAINER_BUILDS') +class ContainerConfig: + + def __init__(self): + """ + Determines if a container should be used and which remote to choose + + Checks the environment variable SNAPCRAFT_CONTAINER_BUILDS: + 1. SNAPCRAFT_CONTAINER_BUILDS=1 enables local containers + 2. SNAPCRAFT_CONTAINER_BUILDS=foobar uses the value as a remote + 3. SNAPCRAFT_CONTAINER_BUILDS=0 or unset, no container is used + """ + + container_builds = os.environ.get('SNAPCRAFT_CONTAINER_BUILDS', '0') + # Default remote if it's a truthy value - otherwise it's a remote name + try: + self._use_container = util.strtobool(container_builds) + self._remote = None + except ValueError: + self._use_container = True + # Verbatim name of a remote + if not lxd._remote_is_valid(container_builds): + raise errors.InvalidContainerRemoteError(container_builds) + self._remote = container_builds + + @property + def use_container(self): + return self._use_container + + @property + def remote(self): + return self._remote + + +def get_container_config(): + return ContainerConfig() diff -Nru snapcraft-2.34/snapcraft/cli/_errors.py snapcraft-2.35/snapcraft/cli/_errors.py --- snapcraft-2.34/snapcraft/cli/_errors.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/cli/_errors.py 2017-11-01 19:41:33.000000000 +0000 @@ -18,7 +18,7 @@ import traceback from . import echo -import snapcraft.internal.errors +from snapcraft.internal import errors def exception_handler(exception_type, exception, exception_traceback, *, @@ -40,16 +40,18 @@ """ exit_code = 1 - is_snapcraft_error = issubclass( - exception_type, snapcraft.internal.errors.SnapcraftError) + is_snapcraft_error = issubclass(exception_type, errors.SnapcraftError) if debug or not is_snapcraft_error: traceback.print_exception( exception_type, exception, exception_traceback) + should_print_error = not debug and ( + exception_type != errors.ContainerSnapcraftCmdError) + if is_snapcraft_error: exit_code = exception.get_exit_code() - if not debug: + if should_print_error: echo.error(str(exception)) sys.exit(exit_code) diff -Nru snapcraft-2.34/snapcraft/cli/__init__.py snapcraft-2.35/snapcraft/cli/__init__.py --- snapcraft-2.34/snapcraft/cli/__init__.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/cli/__init__.py 2017-11-01 19:41:33.000000000 +0000 @@ -25,7 +25,9 @@ from snapcraft.internal import deprecations from snapcraft.internal import log from . import echo +from . import env from .assertions import assertionscli +from .containers import containerscli from .discovery import discoverycli from .lifecycle import lifecyclecli from .store import storecli @@ -40,6 +42,7 @@ storecli, cicli, assertionscli, + containerscli, discoverycli, helpcli, lifecyclecli, @@ -57,6 +60,7 @@ 'keys': 'list-keys', 'revisions': 'list-revisions', 'plugins': 'list-plugins', + 'collaborators': 'edit-collaborators', } _CMD_DEPRECATION_NOTICES = { @@ -84,6 +88,16 @@ cmd = click.Group.get_command(self, ctx, cmd_name) return cmd + def list_commands(self, ctx): + commands = super().list_commands(ctx) + # Let's keep edit-collaborators hidden until we get the green light + # from the store. + commands.pop(commands.index('edit-collaborators')) + container_config = env.get_container_config() + if not container_config.use_container: + commands.pop(commands.index('refresh')) + return commands + @click.group(cls=SnapcraftGroup, invoke_without_command=True) @click.version_option(version=snapcraft.__version__) diff -Nru snapcraft-2.34/snapcraft/cli/lifecycle.py snapcraft-2.35/snapcraft/cli/lifecycle.py --- snapcraft-2.34/snapcraft/cli/lifecycle.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/cli/lifecycle.py 2017-11-01 19:41:33.000000000 +0000 @@ -15,8 +15,10 @@ # along with this program. If not, see . import click +import os -from snapcraft.internal import lifecycle +import snapcraft +from snapcraft.internal import deprecations, lifecycle, lxd from ._options import add_build_options, get_project_options from . import echo from . import env @@ -24,9 +26,10 @@ def _execute(command, parts, **kwargs): project_options = get_project_options(**kwargs) - - if env.is_containerbuild(): - lifecycle.containerbuild(command, project_options, parts) + container_config = env.get_container_config() + if container_config.use_container: + lifecycle.containerbuild(command, project_options, + container_config, parts) else: lifecycle.execute(command, project_options, parts) return project_options @@ -121,12 +124,17 @@ snapcraft snap snapcraft snap --output renamed-snap.snap - If you want to snap a directory, you should use the snap-dir command + If you want to snap a directory, you should use the pack command instead. """ + if directory: + deprecations.handle_deprecation_notice('dn6') + project_options = get_project_options(**kwargs) - if env.is_containerbuild(): - lifecycle.containerbuild('snap', project_options, output, directory) + container_config = env.get_container_config() + if container_config.use_container: + lifecycle.containerbuild('snap', project_options, + container_config, output, directory) else: snap_name = lifecycle.snap( project_options, directory=directory, output=output) @@ -134,6 +142,25 @@ @lifecyclecli.command() +@click.argument('directory') +@click.option('--output', '-o', help='path to the resulting snap.') +def pack(directory, output, **kwargs): + """Create a snap from a directory holding a valid snap. + + The layout of should contain a valid meta/snap.yaml in + order to be a valid snap. + + \b + Examples: + snapcraft pack my-snap-directory + snapcraft pack my-snap-directory --output renamed-snap.snap + + """ + snap_name = lifecycle.pack(directory, output) + echo.info('Snapped {}'.format(snap_name)) + + +@lifecyclecli.command() @add_build_options() @click.argument('parts', nargs=-1, metavar='...', required=False) @click.option('--step', '-s', @@ -149,11 +176,15 @@ snapcraft clean my-part --step build """ project_options = get_project_options(**kwargs) - if env.is_containerbuild(): - step = step or 'pull' - lifecycle.containerbuild('clean', project_options, - args=['--step', step, *parts]) + container_config = env.get_container_config() + if container_config.use_container: + config = snapcraft.internal.load_config(project_options) + lxd.Project(project_options=project_options, + remote=container_config.remote, + output=None, source=os.path.curdir, + metadata=config.get_metadata()).clean(parts, step) else: + step = step or 'pull' if step == 'strip': echo.warning('DEPRECATED: Use `prime` instead of `strip` ' 'as the step to clean') diff -Nru snapcraft-2.34/snapcraft/cli/parts.py snapcraft-2.35/snapcraft/cli/parts.py --- snapcraft-2.34/snapcraft/cli/parts.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/cli/parts.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,7 +14,10 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . import click -from snapcraft.internal import remote_parts + +from snapcraft.internal import remote_parts, lifecycle +from ._options import get_project_options +from . import env @click.group(context_settings={}) @@ -25,8 +28,16 @@ @partscli.command() @click.pass_context -def update(ctx): +def update(ctx, **kwargs): """Updates the parts listing from the cloud.""" + # Update in the container so that it will use the parts at build time + container_config = env.get_container_config() + if container_config.use_container: + project_options = get_project_options(**kwargs) + lifecycle.containerbuild('update', project_options, container_config) + + # Parts can be defined and searched from any folder on the host, so + # regardless of using containers we always update these as well remote_parts.update() diff -Nru snapcraft-2.34/snapcraft/cli/store.py snapcraft-2.35/snapcraft/cli/store.py --- snapcraft-2.34/snapcraft/cli/store.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/cli/store.py 2017-11-01 19:41:33.000000000 +0000 @@ -230,7 +230,11 @@ @storecli.command() def login(): - """Authenticate session against Ubuntu One SSO.""" + """Login with your Ubuntu One e-mail address and password. + + If you do not have an Ubuntu One account, you can create one at + https://dashboard.snapcraft.io/openid/login + """ if not snapcraft.login(): sys.exit(1) @@ -238,7 +242,6 @@ @storecli.command() def logout(): """Clear session credentials.""" - echo.info('Clearing credentials for Ubuntu One SSO.') store = storeapi.StoreClient() store.logout() echo.info('Credentials cleared.') diff -Nru snapcraft-2.34/snapcraft/__init__.py snapcraft-2.35/snapcraft/__init__.py --- snapcraft-2.34/snapcraft/__init__.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/__init__.py 2017-11-01 19:41:33.000000000 +0000 @@ -420,7 +420,6 @@ sign_build, status, validate, - collaborate, ) from snapcraft import common # noqa from snapcraft import plugins # noqa @@ -456,8 +455,3 @@ yaml.add_representer(str, str_presenter) yaml.add_representer(OrderedDict, dict_representer) yaml.add_constructor(_mapping_tag, dict_constructor) - -from snapcraft.internal import common as _common # noqa -if _common.is_snap(): - snap = _os.environ.get('SNAP') - _common.set_schemadir(_os.path.join(snap, 'share', 'snapcraft', 'schema')) diff -Nru snapcraft-2.34/snapcraft/internal/common.py snapcraft-2.35/snapcraft/internal/common.py --- snapcraft-2.34/snapcraft/internal/common.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/common.py 2017-11-01 19:41:33.000000000 +0000 @@ -263,12 +263,3 @@ ] return [p for p in paths if os.path.exists(p)] - - -def get_os_release_info(): - with open("/etc/os-release") as os_release_file: - os_release_dict = {} - for line in os_release_file: - key, value = line.rstrip().split('=') - os_release_dict[key] = value.strip('"') - return os_release_dict diff -Nru snapcraft-2.34/snapcraft/internal/deprecations.py snapcraft-2.35/snapcraft/internal/deprecations.py --- snapcraft-2.34/snapcraft/internal/deprecations.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/deprecations.py 2017-11-01 19:41:33.000000000 +0000 @@ -29,6 +29,10 @@ 'dn2': "Custom plugins should now be placed in 'snap/plugins'.", 'dn3': "Assets in 'setup/gui' should now be placed in 'snap/gui'.", 'dn4': "The 'history' command has been replaced by 'list-revisions'.", + 'dn5': "Aliases are now handled by the store, and shouldn't be declared " + "in the snap.", + 'dn6': "Use of the 'snap' command with a directory has been deprecated " + "in favour of the 'pack' command.", } _DEPRECATION_URL_FMT = 'http://snapcraft.io/docs/deprecation-notices/{id}' diff -Nru snapcraft-2.34/snapcraft/internal/dirs.py snapcraft-2.35/snapcraft/internal/dirs.py --- snapcraft-2.34/snapcraft/internal/dirs.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/dirs.py 2017-11-01 19:41:33.000000000 +0000 @@ -24,8 +24,20 @@ """ from snapcraft.internal import common topdir = os.path.abspath(os.path.join(__file__, '..', '..', '..')) - # only change the default if we are running from a checkout + # Only change the default if we are running from a checkout or from the + # snap. if os.path.exists(os.path.join(topdir, 'setup.py')): common.set_plugindir(os.path.join(topdir, 'snapcraft', 'plugins')) common.set_schemadir(os.path.join(topdir, 'schema')) common.set_librariesdir(os.path.join(topdir, 'libraries')) + + # The default paths are relative to sys.prefix, which works well for + # Snapcraft as a deb or in a venv. However, the Python plugin installs + # packages into $SNAP/ as a prefix, while Python itself is contained in + # $SNAP/usr/. As a result, using sys.prefix (which is '/usr') to find these + # files won't work in the snap. + elif common.is_snap(): + parent_dir = os.path.join(os.environ.get('SNAP'), 'share', 'snapcraft') + common.set_plugindir(os.path.join(parent_dir, 'plugins')) + common.set_schemadir(os.path.join(parent_dir, 'schema')) + common.set_librariesdir(os.path.join(parent_dir, 'libraries')) diff -Nru snapcraft-2.34/snapcraft/internal/errors.py snapcraft-2.35/snapcraft/internal/errors.py --- snapcraft-2.34/snapcraft/internal/errors.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/errors.py 2017-11-01 19:41:33.000000000 +0000 @@ -118,6 +118,28 @@ 'https://linuxcontainers.org/lxd/getting-started-cli.') +class ContainerRunError(SnapcraftError): + + fmt = ( + 'The following command failed to run: ' + '{command!r} exited with {exit_code}\n' + ) + + def __init__(self, *, command, exit_code): + super().__init__(command=' '.join(command), exit_code=exit_code) + + +class ContainerSnapcraftCmdError(ContainerRunError): + + fmt = ( + 'Snapcraft command failed in the container: ' + '{command!r} exited with {exit_code}\n' + ) + + def __init__(self, *, command, exit_code): + super().__init__(command=' '.join(command), exit_code=exit_code) + + class SnapdError(SnapcraftError): fmt = '{message}' @@ -144,6 +166,17 @@ super().__init__(command=command, app=app) +class InvalidContainerRemoteError(SnapcraftError): + + fmt = ( + '{remote!r} is not a valid LXD remote name.\n' + "Colons, spaces and slashes can't be used.\n" + ) + + def __init__(self, remote): + super().__init__(remote=remote) + + class InvalidDesktopFileError(SnapcraftError): fmt = ( @@ -294,3 +327,31 @@ def __init__(self, part_name, message): super().__init__(part_name=part_name, message=message) + + +class OsReleaseIdError(SnapcraftError): + + fmt = 'Unable to determine host OS ID' + + +class OsReleaseNameError(SnapcraftError): + + fmt = 'Unable to determine host OS name' + + +class OsReleaseVersionIdError(SnapcraftError): + + fmt = 'Unable to determine host OS version ID' + + +class OsReleaseCodenameError(SnapcraftError): + + fmt = 'Unable to determine host OS version codename' + + +class InvalidContainerImageInfoError(SnapcraftError): + + fmt = 'Error parsing the container image info: {image_info}' + + def __init__(self, image_info): + super().__init__(image_info=image_info) diff -Nru snapcraft-2.34/snapcraft/internal/libraries.py snapcraft-2.35/snapcraft/internal/libraries.py --- snapcraft-2.34/snapcraft/internal/libraries.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/libraries.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,13 +14,19 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import contextlib import re import glob import logging import os import subprocess -from snapcraft.internal import common +from snapcraft.internal import ( + common, + errors, + os_release, + repo, +) logger = logging.getLogger(__name__) @@ -66,13 +72,18 @@ if _libraries: return _libraries - release = common.get_os_release_info()['VERSION_ID'] - lib_path = os.path.join(common.get_librariesdir(), release) + lib_path = None - if not os.path.exists(lib_path): - logger.debug('No libraries to exclude from this release') - # Always exclude libc.so.6 - return frozenset(['libc.so.6']) + release = os_release.OsRelease() + with contextlib.suppress(errors.OsReleaseVersionIdError): + lib_path = os.path.join( + common.get_librariesdir(), release.version_id()) + + if not lib_path or not os.path.exists(lib_path): + logger.debug('Only excluding libc libraries from the release') + libc6_libs = [os.path.basename(l) + for l in repo.Repo.get_package_libraries('libc6')] + return frozenset(libc6_libs) with open(lib_path) as fn: _libraries = frozenset(fn.read().split()) diff -Nru snapcraft-2.34/snapcraft/internal/lifecycle/_clean.py snapcraft-2.35/snapcraft/internal/lifecycle/_clean.py --- snapcraft-2.34/snapcraft/internal/lifecycle/_clean.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lifecycle/_clean.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,197 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2017 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 . +import contextlib +import logging +import os +import shutil + +from snapcraft import formatting_utils +from snapcraft.internal import common, load_config +from . import constants + + +logger = logging.getLogger(__name__) + + +def _reverse_dependency_tree(config, part_name): + dependents = config.parts.get_dependents(part_name) + for dependent in dependents.copy(): + # No need to worry about infinite recursion due to circular + # dependencies since the YAML validation won't allow it. + dependents |= _reverse_dependency_tree(config, dependent) + + return dependents + + +def _clean_part_and_all_dependents(part_name, step, config, staged_state, + primed_state): + # Obtain the reverse dependency tree for this part. Make sure all + # dependents are cleaned. + dependents = _reverse_dependency_tree(config, part_name) + dependent_parts = {p for p in config.all_parts + if p.name in dependents} + for dependent_part in dependent_parts: + dependent_part.clean(staged_state, primed_state, step) + + # Finally, clean the part in question + config.parts.clean_part(part_name, staged_state, primed_state, step) + + +def _verify_dependents_will_be_cleaned(part_name, clean_part_names, step, + config): + # Get the name of the parts that depend upon this one + dependents = config.parts.get_dependents(part_name) + additional_dependents = [] + + # Verify that they're either already clean, or that they will be cleaned. + if not dependents.issubset(clean_part_names): + for part in config.all_parts: + if part.name in dependents and not part.is_clean(step): + humanized_parts = formatting_utils.humanize_list( + dependents, 'and') + additional_dependents.append(part_name) + + logger.warning( + 'Requested clean of {!r} which requires also cleaning ' + 'the part{} {}'.format(part_name, + '' if len(dependents) == 1 else 's', + humanized_parts)) + + +def _clean_parts(part_names, step, config, staged_state, primed_state): + if not step: + step = 'pull' + + # Before doing anything, verify that we weren't asked to clean only the + # root of a dependency tree and hint that more parts would be cleaned + # if not. + for part_name in part_names: + _verify_dependents_will_be_cleaned(part_name, part_names, step, config) + + # Now we can actually clean. + for part_name in part_names: + _clean_part_and_all_dependents( + part_name, step, config, staged_state, primed_state) + + +def _remove_directory_if_empty(directory): + if os.path.isdir(directory) and not os.listdir(directory): + os.rmdir(directory) + + +def _cleanup_common_directories(config, project_options): + max_index = -1 + for part in config.all_parts: + step = part.last_step() + if step: + index = common.COMMAND_ORDER.index(step) + if index > max_index: + max_index = index + + with contextlib.suppress(IndexError): + _cleanup_common_directories_for_step( + common.COMMAND_ORDER[max_index+1], project_options) + + +def _cleanup_common_directories_for_step(step, project_options, parts=None): + if not parts: + parts = [] + + index = common.COMMAND_ORDER.index(step) + + if index <= common.COMMAND_ORDER.index('prime'): + # Remove the priming area. + _cleanup_common( + project_options.prime_dir, 'prime', 'Cleaning up priming area', + parts) + + if index <= common.COMMAND_ORDER.index('stage'): + # Remove the staging area. + _cleanup_common( + project_options.stage_dir, 'stage', 'Cleaning up staging area', + parts) + + if index <= common.COMMAND_ORDER.index('pull'): + # Remove the parts directory (but leave local plugins alone). + _cleanup_parts_dir( + project_options.parts_dir, project_options.local_plugins_dir, + parts) + _cleanup_internal_snapcraft_dir() + + _remove_directory_if_empty(project_options.prime_dir) + _remove_directory_if_empty(project_options.stage_dir) + _remove_directory_if_empty(project_options.parts_dir) + + +def _cleanup_common(directory, step, message, parts): + if os.path.isdir(directory): + logger.info(message) + shutil.rmtree(directory) + for part in parts: + part.mark_cleaned(step) + + +def _cleanup_parts_dir(parts_dir, local_plugins_dir, parts): + if os.path.exists(parts_dir): + logger.info('Cleaning up parts directory') + for subdirectory in os.listdir(parts_dir): + path = os.path.join(parts_dir, subdirectory) + if path != local_plugins_dir: + try: + shutil.rmtree(path) + except NotADirectoryError: + os.remove(path) + for part in parts: + part.mark_cleaned('build') + part.mark_cleaned('pull') + + +def _cleanup_internal_snapcraft_dir(): + if os.path.exists(constants.SNAPCRAFT_INTERNAL_DIR): + shutil.rmtree(constants.SNAPCRAFT_INTERNAL_DIR) + + +def clean(project_options, parts, step=None): + # step defaults to None because that's how it comes from docopt when it's + # not set. + if not step: + step = 'pull' + + if not parts and step == 'pull': + _cleanup_common_directories_for_step(step, project_options) + return + + config = load_config() + + if not parts and (step == 'stage' or step == 'prime'): + # If we've been asked to clean stage or prime without being given + # specific parts, just blow away those directories instead of + # doing it per part. + _cleanup_common_directories_for_step( + step, project_options, parts=config.all_parts) + return + + if parts: + config.parts.validate(parts) + else: + parts = [part.name for part in config.all_parts] + + staged_state = config.get_project_state('stage') + primed_state = config.get_project_state('prime') + + _clean_parts(parts, step, config, staged_state, primed_state) + + _cleanup_common_directories(config, project_options) diff -Nru snapcraft-2.34/snapcraft/internal/lifecycle/constants.py snapcraft-2.35/snapcraft/internal/lifecycle/constants.py --- snapcraft-2.34/snapcraft/internal/lifecycle/constants.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lifecycle/constants.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,19 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2017 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 . +import os + +SNAPCRAFT_INTERNAL_DIR = os.path.join('snap', '.snapcraft') +STEPS_TO_AUTOMATICALLY_CLEAN_IF_DIRTY = {'stage', 'prime'} diff -Nru snapcraft-2.34/snapcraft/internal/lifecycle/_containers.py snapcraft-2.35/snapcraft/internal/lifecycle/_containers.py --- snapcraft-2.34/snapcraft/internal/lifecycle/_containers.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lifecycle/_containers.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,66 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2017 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 . +import logging +import os +import tarfile + +from snapcraft.internal import errors, load_config, lxd + + +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', './prime', tar_filename): + return None + elif fn.endswith('.snap'): + return None + return tarinfo + return _tar_filter + + +def containerbuild(step, project_options, container_config, + output=None, args=[]): + config = load_config(project_options) + if container_config.remote: + logger.info('Using LXD remote {!r} from SNAPCRAFT_CONTAINER_BUILDS' + .format(container_config.remote)) + else: + logger.info('Using default LXD remote because ' + 'SNAPCRAFT_CONTAINER_BUILDS is set to 1') + lxd.Project(output=output, source=os.path.curdir, + project_options=project_options, + remote=container_config.remote, + metadata=config.get_metadata()).execute(step, args) + + +def cleanbuild(project_options, remote=''): + if remote and not lxd._remote_is_valid(remote): + raise errors.InvalidContainerRemoteError(remote) + + config = load_config(project_options) + 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)) + lxd.Cleanbuilder(source=tar_filename, + project_options=project_options, + metadata=config.get_metadata(), remote=remote).execute() diff -Nru snapcraft-2.34/snapcraft/internal/lifecycle/__init__.py snapcraft-2.35/snapcraft/internal/lifecycle/__init__.py --- snapcraft-2.34/snapcraft/internal/lifecycle/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lifecycle/__init__.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,22 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . +from ._clean import clean # noqa +from ._containers import cleanbuild # noqa +from ._containers import containerbuild # noqa +from ._init import init # noqa +from ._packer import pack # noqa +from ._packer import snap # noqa +from ._runner import execute # noqa diff -Nru snapcraft-2.34/snapcraft/internal/lifecycle/_init.py snapcraft-2.35/snapcraft/internal/lifecycle/_init.py --- snapcraft-2.34/snapcraft/internal/lifecycle/_init.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lifecycle/_init.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,62 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2017 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 . +import contextlib +import os +from textwrap import dedent + +from snapcraft.internal import errors + + +_TEMPLATE_YAML = dedent("""\ + name: my-snap-name # you probably want to 'snapcraft register ' + version: '0.1' # just for humans, typically '1.2+git' or '1.3.2' + summary: Single-line elevator pitch for your amazing snap # 79 char long summary + description: | + This is my-snap's description. You have a paragraph or two to tell the + most important story about your snap. Keep it under 100 words though, + we live in tweetspace and your description wants to look good in the snap + store. + + grade: devel # must be 'stable' to release into candidate/stable channels + confinement: devmode # use 'strict' once you have the right plugs and slots + + parts: + my-part: + # See 'snapcraft plugins' + plugin: nil + """) # noqa, lines too long. + + +def init(): + """Initialize a snapcraft project.""" + snapcraft_yaml_path = os.path.join('snap', 'snapcraft.yaml') + + if os.path.exists(snapcraft_yaml_path): + raise errors.SnapcraftEnvironmentError( + '{} already exists!'.format(snapcraft_yaml_path)) + elif os.path.exists('snapcraft.yaml'): + raise errors.SnapcraftEnvironmentError( + 'snapcraft.yaml already exists!') + elif os.path.exists('.snapcraft.yaml'): + raise errors.SnapcraftEnvironmentError( + '.snapcraft.yaml already exists!') + yaml = _TEMPLATE_YAML + with contextlib.suppress(FileExistsError): + os.mkdir(os.path.dirname(snapcraft_yaml_path)) + with open(snapcraft_yaml_path, mode='w') as f: + f.write(yaml) + + return snapcraft_yaml_path diff -Nru snapcraft-2.34/snapcraft/internal/lifecycle/_packer.py snapcraft-2.35/snapcraft/internal/lifecycle/_packer.py --- snapcraft-2.34/snapcraft/internal/lifecycle/_packer.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lifecycle/_packer.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,103 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2017 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 . +import logging +import os +import time +from subprocess import Popen, PIPE, STDOUT + +import yaml +from progressbar import AnimatedMarker, ProgressBar + +from snapcraft.internal import common, repo +from snapcraft.internal.indicators import is_dumb_terminal +from ._runner import execute + + +logger = logging.getLogger(__name__) + + +def _snap_data_from_dir(directory): + with open(os.path.join(directory, 'meta', 'snap.yaml')) as f: + snap = yaml.load(f) + + return {'name': snap['name'], + 'version': snap['version'], + 'arch': snap.get('architectures', []), + 'type': snap.get('type', '')} + + +def snap(project_options, directory=None, output=None): + if not directory: + directory = project_options.prime_dir + execute('prime', project_options) + + return pack(directory, output) + + +def pack(directory, output=None): + # Check for our prerequesite external command early + repo.check_for_command('mksquashfs') + + snap = _snap_data_from_dir(directory) + snap_name = output or common.format_snap_name(snap) + + # If a .snap-build exists at this point, when we are about to override + # the snap blob, it is stale. We rename it so user have a chance to + # recover accidentally lost assertions. + snap_build = snap_name + '-build' + if os.path.isfile(snap_build): + _new = '{}.{}'.format(snap_build, int(time.time())) + logger.warning('Renaming stale build assertion to {}'.format(_new)) + os.rename(snap_build, _new) + + # These options need to match the review tools: + # http://bazaar.launchpad.net/~click-reviewers/click-reviewers-tools/trunk/view/head:/clickreviews/common.py#L38 + mksquashfs_args = ['-noappend', '-comp', 'xz', '-no-xattrs'] + if snap['type'] != 'os': + mksquashfs_args.append('-all-root') + + with Popen(['mksquashfs', directory, snap_name] + mksquashfs_args, + stdout=PIPE, stderr=STDOUT) as proc: + ret = None + if is_dumb_terminal(): + logger.info('Snapping {!r} ...'.format(snap['name'])) + ret = proc.wait() + else: + message = '\033[0;32m\rSnapping {!r}\033[0;32m '.format( + snap['name']) + progress_indicator = ProgressBar( + widgets=[message, AnimatedMarker()], maxval=7) + progress_indicator.start() + + ret = proc.poll() + count = 0 + + while ret is None: + if count >= 7: + progress_indicator.start() + count = 0 + progress_indicator.update(count) + count += 1 + time.sleep(.2) + ret = proc.poll() + print('') + if ret != 0: + logger.error(proc.stdout.read().decode('utf-8')) + raise RuntimeError('Failed to create snap {!r}'.format(snap_name)) + + logger.debug(proc.stdout.read().decode('utf-8')) + + return snap_name diff -Nru snapcraft-2.34/snapcraft/internal/lifecycle/_runner.py snapcraft-2.35/snapcraft/internal/lifecycle/_runner.py --- snapcraft-2.34/snapcraft/internal/lifecycle/_runner.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lifecycle/_runner.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,244 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2017 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 . +import contextlib +import logging +import os +from subprocess import check_call +from tempfile import TemporaryDirectory + +import yaml + +import snapcraft +import snapcraft.internal +from snapcraft.internal import ( + common, + meta, + pluginhandler, + repo, + states +) +from snapcraft.internal import errors +from snapcraft.internal.cache import SnapCache +from snapcraft.internal.project_loader import replace_attr +from . import constants + + +logger = logging.getLogger(__name__) + + +def execute(step, project_options, part_names=None): + """Execute until step in the lifecycle for part_names or all parts. + + Lifecycle execution will happen for each step iterating over all + the available parts, if part_names is specified, only those parts + will run. + + If one of the parts to execute has an after keyword, execution is + forced until the stage step for such part. If part_names was provided + and after is not in this set, an exception will be raised. + + :param str step: A valid step in the lifecycle: pull, build, prime or snap. + :param project_options: Runtime options for the project. + :param list part_names: A list of parts to execute the lifecycle on. + :raises RuntimeError: If a prerequesite of the part needs to be staged + and such part is not in the list of parts to iterate + over. + :returns: A dict with the snap name, version, type and architectures. + """ + config = snapcraft.internal.load_config(project_options) + installed_packages = repo.Repo.install_build_packages( + config.build_tools) + if installed_packages is None: + raise ValueError( + 'The repo backend is not returning the list of installed packages') + + installed_snaps = repo.snaps.install_snaps(config.build_snaps) + + os.makedirs(constants.SNAPCRAFT_INTERNAL_DIR, exist_ok=True) + state_path = os.path.join(constants.SNAPCRAFT_INTERNAL_DIR, 'state') + with open(state_path, 'w') as state_file: + state_file.write(yaml.dump( + states.GlobalState(installed_packages, installed_snaps))) + + if (os.environ.get('SNAPCRAFT_SETUP_CORE') and + config.data['confinement'] == 'classic'): + _setup_core(project_options.deb_arch) + + _Executor(config, project_options).run(step, part_names) + + return {'name': config.data['name'], + 'version': config.data['version'], + 'arch': config.data['architectures'], + 'type': config.data.get('type', '')} + + +def _setup_core(deb_arch): + core_path = common.get_core_path() + if os.path.exists(core_path) and os.listdir(core_path): + logger.debug('{!r} already exists, skipping core setup'.format( + core_path)) + return + snap_cache = SnapCache(project_name='snapcraft-core') + + # Try to get the latest revision. + core_snap = snap_cache.get(deb_arch=deb_arch) + if core_snap: + # The current hash matches the filename + current_hash = os.path.splitext(os.path.basename(core_snap))[0] + else: + current_hash = '' + + with TemporaryDirectory() as d: + download_path = os.path.join(d, 'core.snap') + download_hash = snapcraft.download('core', 'stable', download_path, + deb_arch, except_hash=current_hash) + if download_hash != current_hash: + snap_cache.cache(snap_filename=download_path) + snap_cache.prune(deb_arch=deb_arch, keep_hash=download_hash) + + core_snap = snap_cache.get(deb_arch=deb_arch) + + # Now unpack + logger.info('Setting up {!r} in {!r}'.format(core_snap, core_path)) + if os.path.exists(core_path) and not os.listdir(core_path): + check_call(['sudo', 'rmdir', core_path]) + check_call(['sudo', 'mkdir', '-p', os.path.dirname(core_path)]) + check_call(['sudo', 'unsquashfs', '-d', core_path, core_snap]) + + +def _replace_in_part(part): + for key, value in part.plugin.options.__dict__.items(): + value = replace_attr(value, [ + ('$SNAPCRAFT_PART_INSTALL', part.plugin.installdir), + ]) + setattr(part.plugin.options, key, value) + + return part + + +class _Executor: + + def __init__(self, config, project_options): + self.config = config + self.project_options = project_options + self.parts_config = config.parts + self._steps_run = self._init_run_states() + + def _init_run_states(self): + steps_run = {} + + for part in self.config.all_parts: + steps_run[part.name] = set() + for step in common.COMMAND_ORDER: + dirty_report = part.get_dirty_report(step) + if dirty_report: + self._handle_dirty(part, step, dirty_report) + elif not (part.should_step_run(step)): + steps_run[part.name].add(step) + part.notify_part_progress('Skipping {}'.format(step), + '(already ran)') + + return steps_run + + def run(self, step, part_names=None): + if part_names: + self.parts_config.validate(part_names) + # self.config.all_parts is already ordered, let's not lose that + # and keep using a list. + parts = [p for p in self.config.all_parts if p.name in part_names] + else: + parts = self.config.all_parts + part_names = self.config.part_names + + step_index = common.COMMAND_ORDER.index(step) + 1 + + for step in common.COMMAND_ORDER[0:step_index]: + if step == 'stage': + # XXX check only for collisions on the parts that have already + # been built --elopio - 20170713 + pluginhandler.check_for_collisions(self.config.all_parts) + for part in parts: + if step not in self._steps_run[part.name]: + self._run_step(step, part, part_names) + self._steps_run[part.name].add(step) + + self._create_meta(step, part_names) + + def _run_step(self, step, part, part_names): + common.reset_env() + prereqs = self.parts_config.get_prereqs(part.name) + unstaged_prereqs = {p for p in prereqs + if 'stage' not in self._steps_run[p]} + + if unstaged_prereqs and not unstaged_prereqs.issubset(part_names): + missing_parts = [part_name for part_name in self.config.part_names + if part_name in unstaged_prereqs] + if missing_parts: + raise RuntimeError( + 'Requested {!r} of {!r} but there are unsatisfied ' + 'prerequisites: {!r}'.format( + step, part.name, ' '.join(missing_parts))) + elif unstaged_prereqs: + # prerequisites need to build all the way to the staging + # step to be able to share the common assets that make them + # a dependency. + logger.info( + '{!r} has prerequisites that need to be staged: ' + '{}'.format(part.name, ' '.join(unstaged_prereqs))) + self.run('stage', unstaged_prereqs) + + # Run the preparation function for this step (if implemented) + with contextlib.suppress(AttributeError): + getattr(part, 'prepare_{}'.format(step))() + + common.env = self.parts_config.build_env_for_part(part) + common.env.extend(self.config.project_env()) + + part = _replace_in_part(part) + + getattr(part, step)() + + def _create_meta(self, step, part_names): + if step == 'prime' and part_names == self.config.part_names: + common.env = self.config.snap_env() + meta.create_snap_packaging( + self.config.data, self.project_options, + self.config.snapcraft_yaml_path) + + def _handle_dirty(self, part, step, dirty_report): + if step not in constants.STEPS_TO_AUTOMATICALLY_CLEAN_IF_DIRTY: + raise errors.StepOutdatedError( + step=step, part=part.name, + dirty_properties=dirty_report.dirty_properties, + dirty_project_options=dirty_report.dirty_project_options) + + staged_state = self.config.get_project_state('stage') + primed_state = self.config.get_project_state('prime') + + # We need to clean this step, but if it involves cleaning the stage + # step and it has dependents that have been built, we need to ask for + # them to first be cleaned (at least back to the build step). + index = common.COMMAND_ORDER.index(step) + dependents = self.parts_config.get_dependents(part.name) + if (index <= common.COMMAND_ORDER.index('stage') and + not part.is_clean('stage') and dependents): + for dependent in self.config.all_parts: + if (dependent.name in dependents and + not dependent.is_clean('build')): + raise errors.StepOutdatedError(step=step, part=part.name, + dependents=dependents) + + part.clean(staged_state, primed_state, step, '(out of date)') diff -Nru snapcraft-2.34/snapcraft/internal/lifecycle.py snapcraft-2.35/snapcraft/internal/lifecycle.py --- snapcraft-2.34/snapcraft/internal/lifecycle.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lifecycle.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,571 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2017 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 . - -import contextlib -import logging -import os -import shutil -import tarfile -import time -from subprocess import check_call, Popen, PIPE, STDOUT -from tempfile import TemporaryDirectory - -import yaml -from progressbar import AnimatedMarker, ProgressBar - -import snapcraft -from snapcraft import formatting_utils -import snapcraft.internal -from snapcraft.internal import ( - common, - lxd, - meta, - pluginhandler, - repo, - states -) -from snapcraft.internal import errors -from snapcraft.internal.cache import SnapCache -from snapcraft.internal.indicators import is_dumb_terminal -from snapcraft.internal.project_loader import replace_attr - - -logger = logging.getLogger(__name__) - - -_SNAPCRAFT_INTERNAL_DIR = os.path.join('snap', '.snapcraft') - -_TEMPLATE_YAML = """name: my-snap-name # you probably want to 'snapcraft register ' -version: '0.1' # just for humans, typically '1.2+git' or '1.3.2' -summary: Single-line elevator pitch for your amazing snap # 79 char long summary -description: | - This is my-snap's description. You have a paragraph or two to tell the - most important story about your snap. Keep it under 100 words though, - we live in tweetspace and your description wants to look good in the snap - store. - -grade: devel # must be 'stable' to release into candidate/stable channels -confinement: devmode # use 'strict' once you have the right plugs and slots - -parts: - my-part: - # See 'snapcraft plugins' - plugin: nil -""" # noqa, lines too long. - -_STEPS_TO_AUTOMATICALLY_CLEAN_IF_DIRTY = {'stage', 'prime'} - - -def init(): - """Initialize a snapcraft project.""" - snapcraft_yaml_path = os.path.join('snap', 'snapcraft.yaml') - - if os.path.exists(snapcraft_yaml_path): - raise errors.SnapcraftEnvironmentError( - '{} already exists!'.format(snapcraft_yaml_path)) - elif os.path.exists('snapcraft.yaml'): - raise errors.SnapcraftEnvironmentError( - 'snapcraft.yaml already exists!') - elif os.path.exists('.snapcraft.yaml'): - raise errors.SnapcraftEnvironmentError( - '.snapcraft.yaml already exists!') - yaml = _TEMPLATE_YAML - with contextlib.suppress(FileExistsError): - os.mkdir(os.path.dirname(snapcraft_yaml_path)) - with open(snapcraft_yaml_path, mode='w') as f: - f.write(yaml) - - return snapcraft_yaml_path - - -def execute(step, project_options, part_names=None): - """Execute until step in the lifecycle for part_names or all parts. - - Lifecycle execution will happen for each step iterating over all - the available parts, if part_names is specified, only those parts - will run. - - If one of the parts to execute has an after keyword, execution is - forced until the stage step for such part. If part_names was provided - and after is not in this set, an exception will be raised. - - :param str step: A valid step in the lifecycle: pull, build, prime or snap. - :param project_options: Runtime options for the project. - :param list part_names: A list of parts to execute the lifecycle on. - :raises RuntimeError: If a prerequesite of the part needs to be staged - and such part is not in the list of parts to iterate - over. - :returns: A dict with the snap name, version, type and architectures. - """ - config = snapcraft.internal.load_config(project_options) - installed_packages = repo.Repo.install_build_packages( - config.build_tools) - if installed_packages is None: - raise ValueError( - 'The repo backend is not returning the list of installed packages') - - repo.snaps.install_snaps(config.build_snaps) - - os.makedirs(_SNAPCRAFT_INTERNAL_DIR, exist_ok=True) - with open(os.path.join(_SNAPCRAFT_INTERNAL_DIR, 'state'), 'w') as f: - f.write(yaml.dump(states.GlobalState(installed_packages))) - - if (os.environ.get('SNAPCRAFT_SETUP_CORE') and - config.data['confinement'] == 'classic'): - _setup_core(project_options.deb_arch) - - _Executor(config, project_options).run(step, part_names) - - return {'name': config.data['name'], - 'version': config.data['version'], - 'arch': config.data['architectures'], - 'type': config.data.get('type', '')} - - -def _setup_core(deb_arch): - core_path = common.get_core_path() - if os.path.exists(core_path) and os.listdir(core_path): - logger.debug('{!r} already exists, skipping core setup'.format( - core_path)) - return - snap_cache = SnapCache(project_name='snapcraft-core') - - # Try to get the latest revision. - core_snap = snap_cache.get(deb_arch=deb_arch) - if core_snap: - # The current hash matches the filename - current_hash = os.path.splitext(os.path.basename(core_snap))[0] - else: - current_hash = '' - - with TemporaryDirectory() as d: - download_path = os.path.join(d, 'core.snap') - download_hash = snapcraft.download('core', 'stable', download_path, - deb_arch, except_hash=current_hash) - if download_hash != current_hash: - snap_cache.cache(snap_filename=download_path) - snap_cache.prune(deb_arch=deb_arch, keep_hash=download_hash) - - core_snap = snap_cache.get(deb_arch=deb_arch) - - # Now unpack - logger.info('Setting up {!r} in {!r}'.format(core_snap, core_path)) - if os.path.exists(core_path) and not os.listdir(core_path): - check_call(['sudo', 'rmdir', core_path]) - check_call(['sudo', 'mkdir', '-p', os.path.dirname(core_path)]) - check_call(['sudo', 'unsquashfs', '-d', core_path, core_snap]) - - -def _replace_in_part(part): - for key, value in part.plugin.options.__dict__.items(): - value = replace_attr(value, [ - ('$SNAPCRAFT_PART_INSTALL', part.plugin.installdir), - ]) - setattr(part.plugin.options, key, value) - - return part - - -class _Executor: - - def __init__(self, config, project_options): - self.config = config - self.project_options = project_options - self.parts_config = config.parts - self._steps_run = self._init_run_states() - - def _init_run_states(self): - steps_run = {} - - for part in self.config.all_parts: - steps_run[part.name] = set() - for step in common.COMMAND_ORDER: - dirty_report = part.get_dirty_report(step) - if dirty_report: - self._handle_dirty(part, step, dirty_report) - elif not (part.should_step_run(step)): - steps_run[part.name].add(step) - part.notify_part_progress('Skipping {}'.format(step), - '(already ran)') - - return steps_run - - def run(self, step, part_names=None): - if part_names: - self.parts_config.validate(part_names) - # self.config.all_parts is already ordered, let's not lose that - # and keep using a list. - parts = [p for p in self.config.all_parts if p.name in part_names] - else: - parts = self.config.all_parts - part_names = self.config.part_names - - step_index = common.COMMAND_ORDER.index(step) + 1 - - for step in common.COMMAND_ORDER[0:step_index]: - if step == 'stage': - # XXX check only for collisions on the parts that have already - # been built --elopio - 20170713 - pluginhandler.check_for_collisions(self.config.all_parts) - for part in parts: - if step not in self._steps_run[part.name]: - self._run_step(step, part, part_names) - self._steps_run[part.name].add(step) - - self._create_meta(step, part_names) - - def _run_step(self, step, part, part_names): - common.reset_env() - prereqs = self.parts_config.get_prereqs(part.name) - unstaged_prereqs = {p for p in prereqs - if 'stage' not in self._steps_run[p]} - - if unstaged_prereqs and not unstaged_prereqs.issubset(part_names): - missing_parts = [part_name for part_name in self.config.part_names - if part_name in unstaged_prereqs] - if missing_parts: - raise RuntimeError( - 'Requested {!r} of {!r} but there are unsatisfied ' - 'prerequisites: {!r}'.format( - step, part.name, ' '.join(missing_parts))) - elif unstaged_prereqs: - # prerequisites need to build all the way to the staging - # step to be able to share the common assets that make them - # a dependency. - logger.info( - '{!r} has prerequisites that need to be staged: ' - '{}'.format(part.name, ' '.join(unstaged_prereqs))) - self.run('stage', unstaged_prereqs) - - # Run the preparation function for this step (if implemented) - with contextlib.suppress(AttributeError): - getattr(part, 'prepare_{}'.format(step))() - - common.env = self.parts_config.build_env_for_part(part) - common.env.extend(self.config.project_env()) - - part = _replace_in_part(part) - - getattr(part, step)() - - def _create_meta(self, step, part_names): - if step == 'prime' and part_names == self.config.part_names: - common.env = self.config.snap_env() - meta.create_snap_packaging( - self.config.data, self.project_options, - self.config.snapcraft_yaml_path) - - def _handle_dirty(self, part, step, dirty_report): - if step not in _STEPS_TO_AUTOMATICALLY_CLEAN_IF_DIRTY: - raise errors.StepOutdatedError( - step=step, part=part.name, - dirty_properties=dirty_report.dirty_properties, - dirty_project_options=dirty_report.dirty_project_options) - - staged_state = self.config.get_project_state('stage') - primed_state = self.config.get_project_state('prime') - - # We need to clean this step, but if it involves cleaning the stage - # step and it has dependents that have been built, we need to ask for - # them to first be cleaned (at least back to the build step). - index = common.COMMAND_ORDER.index(step) - dependents = self.parts_config.get_dependents(part.name) - if (index <= common.COMMAND_ORDER.index('stage') and - not part.is_clean('stage') and dependents): - for dependent in self.config.all_parts: - if (dependent.name in dependents and - not dependent.is_clean('build')): - raise errors.StepOutdatedError(step=step, part=part.name, - dependents=dependents) - - part.clean(staged_state, primed_state, step, '(out of date)') - - -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', './prime', tar_filename): - return None - elif fn.endswith('.snap'): - return None - return tarinfo - return _tar_filter - - -def containerbuild(step, project_options, output=None, args=[]): - config = snapcraft.internal.load_config(project_options) - lxd.Project(output=output, source=os.path.curdir, - project_options=project_options, - metadata=config.get_metadata()).execute(step, args) - - -def cleanbuild(project_options, remote=''): - config = snapcraft.internal.load_config(project_options) - 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)) - lxd.Cleanbuilder(source=tar_filename, - project_options=project_options, - metadata=config.get_metadata(), remote=remote).execute() - - -def _snap_data_from_dir(directory): - with open(os.path.join(directory, 'meta', 'snap.yaml')) as f: - snap = yaml.load(f) - - return {'name': snap['name'], - 'version': snap['version'], - 'arch': snap.get('architectures', []), - 'type': snap.get('type', '')} - - -def snap(project_options, directory=None, output=None): - # Check for our prerequesite external command early - repo.check_for_command('mksquashfs') - - if directory: - prime_dir = os.path.abspath(directory) - snap = _snap_data_from_dir(prime_dir) - else: - # make sure the full lifecycle is executed - prime_dir = project_options.prime_dir - execute('prime', project_options) - snap = _snap_data_from_dir(prime_dir) - - snap_name = output or common.format_snap_name(snap) - - # If a .snap-build exists at this point, when we are about to override - # the snap blob, it is stale. We rename it so user have a chance to - # recover accidentally lost assertions. - snap_build = snap_name + '-build' - if os.path.isfile(snap_build): - _new = '{}.{}'.format(snap_build, int(time.time())) - logger.warning('Renaming stale build assertion to {}'.format(_new)) - os.rename(snap_build, _new) - - # These options need to match the review tools: - # http://bazaar.launchpad.net/~click-reviewers/click-reviewers-tools/trunk/view/head:/clickreviews/common.py#L38 - mksquashfs_args = ['-noappend', '-comp', 'xz', '-no-xattrs'] - if snap['type'] != 'os': - mksquashfs_args.append('-all-root') - - with Popen(['mksquashfs', prime_dir, snap_name] + mksquashfs_args, - stdout=PIPE, stderr=STDOUT) as proc: - ret = None - if is_dumb_terminal(): - logger.info('Snapping {!r} ...'.format(snap['name'])) - ret = proc.wait() - else: - message = '\033[0;32m\rSnapping {!r}\033[0;32m '.format( - snap['name']) - progress_indicator = ProgressBar( - widgets=[message, AnimatedMarker()], maxval=7) - progress_indicator.start() - - ret = proc.poll() - count = 0 - - while ret is None: - if count >= 7: - progress_indicator.start() - count = 0 - progress_indicator.update(count) - count += 1 - time.sleep(.2) - ret = proc.poll() - print('') - if ret != 0: - logger.error(proc.stdout.read().decode('utf-8')) - raise RuntimeError('Failed to create snap {!r}'.format(snap_name)) - - logger.debug(proc.stdout.read().decode('utf-8')) - - return snap_name - - -def _reverse_dependency_tree(config, part_name): - dependents = config.parts.get_dependents(part_name) - for dependent in dependents.copy(): - # No need to worry about infinite recursion due to circular - # dependencies since the YAML validation won't allow it. - dependents |= _reverse_dependency_tree(config, dependent) - - return dependents - - -def _clean_part_and_all_dependents(part_name, step, config, staged_state, - primed_state): - # Obtain the reverse dependency tree for this part. Make sure all - # dependents are cleaned. - dependents = _reverse_dependency_tree(config, part_name) - dependent_parts = {p for p in config.all_parts - if p.name in dependents} - for dependent_part in dependent_parts: - dependent_part.clean(staged_state, primed_state, step) - - # Finally, clean the part in question - config.parts.clean_part(part_name, staged_state, primed_state, step) - - -def _verify_dependents_will_be_cleaned(part_name, clean_part_names, step, - config): - # Get the name of the parts that depend upon this one - dependents = config.parts.get_dependents(part_name) - additional_dependents = [] - - # Verify that they're either already clean, or that they will be cleaned. - if not dependents.issubset(clean_part_names): - for part in config.all_parts: - if part.name in dependents and not part.is_clean(step): - humanized_parts = formatting_utils.humanize_list( - dependents, 'and') - additional_dependents.append(part_name) - - logger.warning( - 'Requested clean of {!r} which requires also cleaning ' - 'the part{} {}'.format(part_name, - '' if len(dependents) == 1 else 's', - humanized_parts)) - - -def _clean_parts(part_names, step, config, staged_state, primed_state): - if not step: - step = 'pull' - - # Before doing anything, verify that we weren't asked to clean only the - # root of a dependency tree and hint that more parts would be cleaned - # if not. - for part_name in part_names: - _verify_dependents_will_be_cleaned(part_name, part_names, step, config) - - # Now we can actually clean. - for part_name in part_names: - _clean_part_and_all_dependents( - part_name, step, config, staged_state, primed_state) - - -def _remove_directory_if_empty(directory): - if os.path.isdir(directory) and not os.listdir(directory): - os.rmdir(directory) - - -def _cleanup_common_directories(config, project_options): - max_index = -1 - for part in config.all_parts: - step = part.last_step() - if step: - index = common.COMMAND_ORDER.index(step) - if index > max_index: - max_index = index - - with contextlib.suppress(IndexError): - _cleanup_common_directories_for_step( - common.COMMAND_ORDER[max_index+1], project_options) - - -def _cleanup_common_directories_for_step(step, project_options, parts=None): - if not parts: - parts = [] - - index = common.COMMAND_ORDER.index(step) - - if index <= common.COMMAND_ORDER.index('prime'): - # Remove the priming area. - _cleanup_common( - project_options.prime_dir, 'prime', 'Cleaning up priming area', - parts) - - if index <= common.COMMAND_ORDER.index('stage'): - # Remove the staging area. - _cleanup_common( - project_options.stage_dir, 'stage', 'Cleaning up staging area', - parts) - - if index <= common.COMMAND_ORDER.index('pull'): - # Remove the parts directory (but leave local plugins alone). - _cleanup_parts_dir( - project_options.parts_dir, project_options.local_plugins_dir, - parts) - _cleanup_internal_snapcraft_dir() - - _remove_directory_if_empty(project_options.prime_dir) - _remove_directory_if_empty(project_options.stage_dir) - _remove_directory_if_empty(project_options.parts_dir) - - -def _cleanup_common(directory, step, message, parts): - if os.path.isdir(directory): - logger.info(message) - shutil.rmtree(directory) - for part in parts: - part.mark_cleaned(step) - - -def _cleanup_parts_dir(parts_dir, local_plugins_dir, parts): - if os.path.exists(parts_dir): - logger.info('Cleaning up parts directory') - for subdirectory in os.listdir(parts_dir): - path = os.path.join(parts_dir, subdirectory) - if path != local_plugins_dir: - try: - shutil.rmtree(path) - except NotADirectoryError: - os.remove(path) - for part in parts: - part.mark_cleaned('build') - part.mark_cleaned('pull') - - -def _cleanup_internal_snapcraft_dir(): - if os.path.exists(_SNAPCRAFT_INTERNAL_DIR): - shutil.rmtree(_SNAPCRAFT_INTERNAL_DIR) - - -def clean(project_options, parts, step=None): - # step defaults to None because that's how it comes from docopt when it's - # not set. - if not step: - step = 'pull' - - if not parts and step == 'pull': - _cleanup_common_directories_for_step(step, project_options) - return - - config = snapcraft.internal.load_config() - - if not parts and (step == 'stage' or step == 'prime'): - # If we've been asked to clean stage or prime without being given - # specific parts, just blow away those directories instead of - # doing it per part. - _cleanup_common_directories_for_step( - step, project_options, parts=config.all_parts) - return - - if parts: - config.parts.validate(parts) - else: - parts = [part.name for part in config.all_parts] - - staged_state = config.get_project_state('stage') - primed_state = config.get_project_state('prime') - - _clean_parts(parts, step, config, staged_state, primed_state) - - _cleanup_common_directories(config, project_options) diff -Nru snapcraft-2.34/snapcraft/internal/lxd/_cleanbuilder.py snapcraft-2.35/snapcraft/internal/lxd/_cleanbuilder.py --- snapcraft-2.34/snapcraft/internal/lxd/_cleanbuilder.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lxd/_cleanbuilder.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,60 @@ +#!/usr/bin/python3 +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2017 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 . + +import logging +import os +import petname +import subprocess + +from . import Containerbuild + +logger = logging.getLogger(__name__) + + +class Cleanbuilder(Containerbuild): + + def __init__(self, *, output=None, source, project_options, + metadata=None, remote=None): + container_name = petname.Generate(3, '-') + super().__init__(output=output, source=source, + project_options=project_options, metadata=metadata, + container_name=container_name, remote=remote) + + def _ensure_container(self): + subprocess.check_call([ + 'lxc', 'launch', '-e', self._image, self._container_name]) + self._configure_container() + self._wait_for_network() + self._container_run(['apt-get', 'update']) + self._inject_snapcraft() + + def _setup_project(self): + logger.info('Setting up container with project assets') + tar_filename = self._source + # os.sep needs to be `/` and on Windows it will be set to `\` + dst = '{}/{}'.format(self._project_folder, + os.path.basename(tar_filename)) + self._container_run(['mkdir', self._project_folder]) + self._push_file(tar_filename, dst) + self._container_run(['tar', 'xvf', os.path.basename(tar_filename)], + cwd=self._project_folder) + + def _finish(self): + # os.sep needs to be `/` and on Windows it will be set to `\` + src = '{}/{}'.format(self._project_folder, self._snap_output) + self._pull_file(src, self._snap_output) + logger.info('Retrieved {}'.format(self._snap_output)) diff -Nru snapcraft-2.34/snapcraft/internal/lxd/_containerbuild.py snapcraft-2.35/snapcraft/internal/lxd/_containerbuild.py --- snapcraft-2.34/snapcraft/internal/lxd/_containerbuild.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lxd/_containerbuild.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,421 @@ +#!/usr/bin/python3 +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2017 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 . + +import collections +import json +import logging +import os +import pipes +import requests +import requests_unixsocket +import shutil +import sys +import tempfile +import yaml +from contextlib import contextmanager +import subprocess +import time +from urllib import parse + +from snapcraft.internal import common +from snapcraft.internal.errors import ( + ContainerConnectionError, + ContainerRunError, + ContainerSnapcraftCmdError, + SnapdError, +) +from snapcraft._options import _get_deb_arch +from snapcraft.internal.repo import snaps + +logger = logging.getLogger(__name__) + +_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'] +# Canonical store account key +_STORE_KEY = ( + 'BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0hqUel3m8ul') + + +class Containerbuild: + + def __init__(self, *, output, source, project_options, + metadata, container_name, remote=None): + if not output: + output = common.format_snap_name(metadata) + self._snap_output = output + self._source = os.path.realpath(source) + self._project_options = project_options + self._metadata = metadata + self._project_folder = '/root/build_{}'.format(metadata['name']) + + if not remote: + remote = _get_default_remote() + _verify_remote(remote) + self._container_name = '{}:snapcraft-{}'.format(remote, container_name) + server_environment = self._get_remote_info()['environment'] + # Use the server architecture to avoid emulation overhead + try: + kernel = server_environment['kernel_architecture'] + except KeyError: + kernel = server_environment['kernelarchitecture'] + self._server_arch = _get_deb_arch(kernel) + if not self._server_arch: + raise ContainerConnectionError( + 'Unrecognized server architecture {}'.format(kernel)) + self._image = 'ubuntu:xenial/{}'.format(self._server_arch) + # Use a temporary folder the 'lxd' snap can access + lxd_common_dir = os.path.expanduser( + os.path.join('~', 'snap', 'lxd', 'common')) + os.makedirs(lxd_common_dir, exist_ok=True) + self.tmp_dir = tempfile.mkdtemp(prefix='snapcraft', dir=lxd_common_dir) + + def _get_remote_info(self): + remote = self._container_name.split(':')[0] + return yaml.load(subprocess.check_output([ + 'lxc', 'info', '{}:'.format(remote)]).decode()) + + @contextmanager + def _container_running(self): + with self._ensure_started(): + try: + yield + except ContainerRunError as e: + if self._project_options.debug: + logger.info('Debug mode enabled, dropping into a shell') + self._container_run(['bash', '-i']) + else: + raise e + else: + # Remove temporary folder if everything went well + shutil.rmtree(self.tmp_dir) + self._finish() + + @contextmanager + def _ensure_started(self): + try: + self._ensure_container() + yield + finally: + status = self._get_container_status() + if status and status['status'] == 'Running': + # Stopping takes a while and lxc doesn't print anything. + print('Stopping {}'.format(self._container_name)) + subprocess.check_call([ + 'lxc', 'stop', '-f', self._container_name]) + + def _get_container_status(self): + containers = json.loads(subprocess.check_output([ + 'lxc', 'list', '--format=json', self._container_name]).decode()) + for container in containers: + if container['name'] == self._container_name.split(':')[-1]: + return container + + def _configure_container(self): + subprocess.check_call([ + 'lxc', 'config', 'set', self._container_name, + 'environment.SNAPCRAFT_SETUP_CORE', '1']) + for snapcraft_env_var in ( + 'SNAPCRAFT_PARTS_URI', 'SNAPCRAFT_BUILD_INFO'): + if os.getenv(snapcraft_env_var): + subprocess.check_call([ + 'lxc', 'config', 'set', self._container_name, + 'environment.{}'.format(snapcraft_env_var), + os.getenv(snapcraft_env_var)]) + # Necessary to read asset files with non-ascii characters. + subprocess.check_call([ + 'lxc', 'config', 'set', self._container_name, + 'environment.LC_ALL', 'C.UTF-8']) + self._set_image_info_env_var() + + def _set_image_info_env_var(self): + FAILURE_WARNING_FORMAT = ( + 'Failed to get container image info: {}\n' + 'It will not be recorded in manifest.') + try: + image_info_command = [ + 'lxc', 'image', 'list', '--format=json', self._image] + image_info = json.loads(subprocess.check_output( + image_info_command).decode()) + except subprocess.CalledProcessError as e: + message = ('`{command}` returned with exit code {returncode}, ' + 'output: {output}'.format( + command=' '.join(image_info_command), + returncode=e.returncode, + output=e.output)) + logger.warning(FAILURE_WARNING_FORMAT.format(message)) + return + except json.decoder.JSONDecodeError as e: + logger.warning(FAILURE_WARNING_FORMAT.format('Not in JSON format')) + return + edited_image_info = collections.OrderedDict() + for field in ('fingerprint', 'architecture', 'created_at'): + if field in image_info[0]: + edited_image_info[field] = image_info[0][field] + # Pass the image info to the container so it can be used when recording + # information about the build environment. + subprocess.check_call([ + 'lxc', 'config', 'set', self._container_name, + 'environment.SNAPCRAFT_IMAGE_INFO', + json.dumps(edited_image_info)]) + + def execute(self, step='snap', args=None): + with self._container_running(): + self._setup_project() + command = ['snapcraft', step] + if step == 'snap': + command += ['--output', self._snap_output] + # Pass on target arch if specified + # If not specified it defaults to the LXD architecture + if self._project_options.target_arch: + command += ['--target-arch', self._project_options.target_arch] + if args: + command += args + self._container_run(command, cwd=self._project_folder) + + def _container_run(self, cmd, cwd=None, **kwargs): + sh = '' + original_cmd = cmd.copy() + # Automatically wait on lock files before running commands + if cmd[0] == 'apt-get': + lock_file = '/var/lib/dpkg/lock' + if cmd[1] == 'update': + lock_file = '/var/lib/apt/lists/lock' + sh += 'while fuser {} >/dev/null 2>&1; do sleep 1; done; '.format( + lock_file) + if cwd: + sh += 'cd {}; '.format(cwd) + if sh: + cmd = ['sh', '-c', '{}{}'.format(sh, + ' '.join(pipes.quote(arg) for arg in cmd))] + try: + subprocess.check_call([ + 'lxc', 'exec', self._container_name, '--'] + cmd, + **kwargs) + except subprocess.CalledProcessError as e: + if original_cmd[0] == 'snapcraft': + raise ContainerSnapcraftCmdError(command=original_cmd, + exit_code=e.returncode) + else: + raise ContainerRunError(command=original_cmd, + exit_code=e.returncode) + + def _wait_for_network(self): + logger.info('Waiting for a network connection...') + not_connected = True + retry_count = 5 + while not_connected: + time.sleep(5) + try: + self._container_run(['python3', '-c', _NETWORK_PROBE_COMMAND]) + not_connected = False + except ContainerRunError as e: + retry_count -= 1 + if retry_count == 0: + raise e + logger.info('Network connection established') + + def _inject_snapcraft(self): + if common.is_snap(): + # Because of https://bugs.launchpad.net/snappy/+bug/1628289 + self._container_run(['apt-get', 'install', 'squashfuse', '-y']) + + # Push core snap into container + self._inject_snap('core') + self._inject_snap('snapcraft') + else: + self._container_run(['apt-get', 'install', 'snapcraft', '-y']) + + def _inject_snap(self, name): + session = requests_unixsocket.Session() + # Cf. https://github.com/snapcore/snapd/wiki/REST-API#get-v2snapsname + # TODO use get_local_snap info from the snaps module. + slug = 'snaps/{}'.format(parse.quote(name, safe='')) + api = snaps.get_snapd_socket_path_template().format(slug) + try: + json = session.request('GET', api).json() + except requests.exceptions.ConnectionError as e: + raise SnapdError( + 'Error connecting to {}'.format(api)) from e + if json['type'] == 'error': + raise SnapdError( + 'Error querying {!r} snap: {}'.format( + name, json['result']['message'])) + id = json['result']['id'] + # Lookup confinement to know if we need to --classic when installing + is_classic = json['result']['confinement'] == 'classic' + + # If the server has a different arch we can't inject local snaps + if (self._project_options.target_arch + and self._project_options.target_arch != self._server_arch): + channel = json['result']['channel'] + return self._install_snap(name, channel, is_classic=is_classic) + + # Revisions are unique, so we don't need to know the channel + rev = json['result']['revision'] + + # https://github.com/snapcore/snapd/blob/master/snap/info.go + # MountFile + filename = '{}_{}.snap'.format(name, rev) + # https://github.com/snapcore/snapd/blob/master/dirs/dirs.go + # CoreLibExecDir + installed = os.path.join(os.path.sep, 'var', 'lib', 'snapd', 'snaps', + filename) + + filepath = os.path.join(self.tmp_dir, filename) + if rev.startswith('x'): + logger.info('Making {} user-accessible'.format(filename)) + subprocess.check_call(['sudo', 'cp', installed, filepath]) + subprocess.check_call([ + 'sudo', 'chown', str(os.getuid()), filepath]) + else: + shutil.copyfile(installed, filepath) + + if self._is_same_snap(filepath, name): + logger.debug('Not re-injecting same version of {!r}'.format(name)) + return + + if not rev.startswith('x'): + self._inject_assertions('{}_{}.assert'.format(name, rev), [ + ['account-key', 'public-key-sha3-384={}'.format(_STORE_KEY)], + ['snap-declaration', 'snap-name={}'.format(name)], + ['snap-revision', 'snap-revision={}'.format(rev), + 'snap-id={}'.format(id)], + ]) + + container_filename = os.path.join(os.sep, 'run', filename) + self._push_file(filepath, container_filename) + self._install_snap(container_filename, + is_dangerous=rev.startswith('x'), + is_classic=is_classic) + + def _pull_file(self, src, dst): + subprocess.check_call(['lxc', 'file', 'pull', + '{}{}'.format(self._container_name, src), dst]) + + def _push_file(self, src, dst): + subprocess.check_call(['lxc', 'file', 'push', + src, '{}{}'.format(self._container_name, dst)]) + + def _install_snap(self, name, channel=None, + is_dangerous=False, + is_classic=False): + logger.info('Installing {}'.format(name)) + # Install: will do nothing if already installed + args = [] + if channel: + args.append('--channel') + args.append(channel) + if is_dangerous: + args.append('--dangerous') + if is_classic: + args.append('--classic') + self._container_run(['snap', 'install', name] + args) + if channel: + # Switch channel if install was a no-op + self._container_run(['snap', 'refresh', name] + args) + + def _is_same_snap(self, filepath, name): + # Compare checksums: user-visible version may still match + checksum = subprocess.check_output(['sha384sum', filepath]).decode( + sys.getfilesystemencoding()).split()[0] + try: + # Find the current version in use in the container + rev = subprocess.check_output([ + 'lxc', 'exec', self._container_name, '--', + 'readlink', '/snap/{}/current'.format(name)] + ).decode(sys.getfilesystemencoding()).strip() + filename = '{}_{}.snap'.format(name, rev) + installed = os.path.join(os.path.sep, + 'var', 'lib', 'snapd', 'snaps', filename) + checksum_container = subprocess.check_output([ + 'lxc', 'exec', self._container_name, '--', + 'sha384sum', installed] + ).decode(sys.getfilesystemencoding()).split()[0] + except subprocess.CalledProcessError: + # Snap not installed + checksum_container = None + return checksum == checksum_container + + def _inject_assertions(self, filename, assertions): + filepath = os.path.join(self.tmp_dir, filename) + with open(filepath, 'wb') as f: + for assertion in assertions: + logger.info('Looking up assertion {}'.format(assertion)) + f.write(subprocess.check_output(['snap', 'known', *assertion])) + f.write(b'\n') + container_filename = os.path.join(os.path.sep, 'run', filename) + self._push_file(filepath, container_filename) + logger.info('Adding assertion {}'.format(filename)) + self._container_run(['snap', 'ack', container_filename]) + + +def _get_default_remote(): + """Query and return the default lxd remote. + + Use the lxc command to query for the default lxd remote. In most + cases this will return the local remote. + + :returns: default lxd remote. + :rtype: string. + :raises snapcraft.internal.errors.ContainerConnectionError: + raised if the lxc call fails. + """ + try: + default_remote = subprocess.check_output( + ['lxc', 'remote', 'get-default']) + except FileNotFoundError: + raise ContainerConnectionError( + 'You must have LXD installed in order to use cleanbuild.') + except subprocess.CalledProcessError: + raise ContainerConnectionError( + 'Something seems to be wrong with your installation of LXD.') + return default_remote.decode(sys.getfilesystemencoding()).strip() + + +def _remote_is_valid(remote): + """Verify that the given string is a valid remote name + + :param str remote: the LXD remote to verify. + """ + # No colon because it separates remote from container name + # No slash because it's used for images + # No spaces + return not (':' in remote or ' ' in remote or '/' in remote) + + +def _verify_remote(remote): + """Verify that the lxd remote exists. + + :param str remote: the lxd remote to verify. + :raises snapcraft.internal.errors.ContainerConnectionError: + raised if the lxc call listing the remote fails. + """ + # There is no easy way to grep the results from `lxc remote list` + # so we try and execute a simple operation against the remote. + try: + subprocess.check_output(['lxc', 'list', '{}:'.format(remote)]) + except FileNotFoundError: + raise ContainerConnectionError( + 'You must have LXD installed in order to use cleanbuild.') + except subprocess.CalledProcessError as e: + raise ContainerConnectionError( + 'There are either no permissions or the remote {!r} ' + 'does not exist.\n' + 'Verify the existing remotes by running `lxc remote list`\n' + .format(remote)) from e diff -Nru snapcraft-2.34/snapcraft/internal/lxd/__init__.py snapcraft-2.35/snapcraft/internal/lxd/__init__.py --- snapcraft-2.34/snapcraft/internal/lxd/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lxd/__init__.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,21 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +from ._containerbuild import Containerbuild # noqa +from ._cleanbuilder import Cleanbuilder # noqa +from ._project import Project # noqa + +from ._containerbuild import _remote_is_valid # noqa diff -Nru snapcraft-2.34/snapcraft/internal/lxd/_project.py snapcraft-2.35/snapcraft/internal/lxd/_project.py --- snapcraft-2.34/snapcraft/internal/lxd/_project.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lxd/_project.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,174 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2017 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 . + + +import logging +import os +import subprocess +import time + +from . import Containerbuild + +from snapcraft.internal.errors import ( + ContainerConnectionError, + SnapcraftEnvironmentError, +) +from snapcraft.internal import lifecycle +from snapcraft.cli import echo + +logger = logging.getLogger(__name__) + + +class Project(Containerbuild): + + def __init__(self, *, output, source, project_options, + metadata, remote=None): + super().__init__(output=output, source=source, + project_options=project_options, + metadata=metadata, container_name=metadata['name'], + remote=remote) + self._processes = [] + + def _ensure_container(self): + new_container = not self._get_container_status() + if new_container: + subprocess.check_call([ + 'lxc', 'init', self._image, self._container_name]) + if self._get_container_status()['status'] == 'Stopped': + self._configure_container() + try: + subprocess.check_call([ + 'lxc', 'start', self._container_name]) + except subprocess.CalledProcessError: + msg = 'The container could not be started.' + if self._container_name.startswith('local:'): + msg += ('\nThe files /etc/subuid and /etc/subgid need to ' + 'contain this line for mounting the local folder:' + '\n root:1000:1' + '\nNote: Add the line to both files, do not ' + 'remove any existing lines.' + '\nRestart lxd after making this change.') + raise ContainerConnectionError(msg) + self._wait_for_network() + if new_container: + self._container_run(['apt-get', 'update']) + self._inject_snapcraft() + + def _configure_container(self): + super()._configure_container() + if self._container_name.startswith('local:'): + # Map host user to root inside container + subprocess.check_call([ + 'lxc', 'config', 'set', self._container_name, + 'raw.idmap', + 'both {} 0'.format(os.getenv('SUDO_UID', os.getuid()))]) + # Remove existing device (to ensure we update old containers) + devices = self._get_container_status()['devices'] + if self._project_folder in devices: + subprocess.check_call([ + 'lxc', 'config', 'device', 'remove', self._container_name, + self._project_folder]) + if 'fuse' not in devices: + subprocess.check_call([ + 'lxc', 'config', 'device', 'add', self._container_name, + 'fuse', 'unix-char', 'path=/dev/fuse' + ]) + + def _setup_project(self): + self._ensure_mount(self._project_folder, self._source) + + def _ensure_mount(self, destination, source): + logger.info('Mounting {} into container'.format(source)) + if not self._container_name.startswith('local:'): + return self._remote_mount(destination, source) + + devices = self._get_container_status()['devices'] + if destination not in devices: + subprocess.check_call([ + 'lxc', 'config', 'device', 'add', self._container_name, + destination, 'disk', 'source={}'.format(source), + 'path={}'.format(destination)]) + + def _remote_mount(self, destination, source): + # Pipes for sshfs and sftp-server to communicate + stdin1, stdout1 = os.pipe() + stdin2, stdout2 = os.pipe() + # XXX: This needs to be extended once we support other distros + try: + self._background_process_run(['/usr/lib/sftp-server'], + stdin=stdin1, stdout=stdout2) + except FileNotFoundError: + raise SnapcraftEnvironmentError( + 'You must have openssh-sftp-server installed to use a LXD ' + 'remote on a different host.\n' + ) + except subprocess.CalledProcessError: + raise SnapcraftEnvironmentError( + 'sftp-server seems to be installed but could not be run.\n' + ) + + # Use sshfs in slave mode to reverse mount the destination + self._container_run(['apt-get', 'install', '-y', 'sshfs']) + self._container_run(['mkdir', '-p', destination]) + self._background_process_run([ + 'lxc', 'exec', self._container_name, '--', + 'sshfs', '-o', 'slave', '-o', 'nonempty', + ':{}'.format(source), destination], + stdin=stdin2, stdout=stdout1) + + # It may take a second or two for sshfs to come up + retry_count = 5 + while retry_count: + time.sleep(1) + if subprocess.check_output([ + 'lxc', 'exec', self._container_name, '--', + 'ls', self._project_folder]): + return + retry_count -= 1 + raise ContainerConnectionError( + 'The project folder could not be mounted.\n' + 'Fuse must be enabled on the LXD host.\n' + 'You can run the following command to enable it:\n' + 'echo Y | sudo tee /sys/module/fuse/parameters/userns_mounts') + + def _background_process_run(self, cmd, **kwargs): + self._processes += [subprocess.Popen(cmd, **kwargs)] + + def _finish(self): + for process in self._processes: + logger.info('Terminating {}'.format(process.args)) + process.terminate() + + def refresh(self): + with self._container_running(): + self._container_run(['apt-get', 'update']) + self._container_run(['apt-get', 'upgrade', '-y']) + self._container_run(['snap', 'refresh']) + + def clean(self, parts, step): + # clean with no parts deletes the container + if not step: + if self._get_container_status(): + print('Deleting {}'.format(self._container_name)) + subprocess.check_call([ + 'lxc', 'delete', '-f', self._container_name]) + step = 'pull' + # clean normally, without involving the container + if step == 'strip': + echo.warning('DEPRECATED: Use `prime` instead of `strip` ' + 'as the step to clean') + step = 'prime' + lifecycle.clean(self._project_options, parts, step) diff -Nru snapcraft-2.34/snapcraft/internal/lxd.py snapcraft-2.35/snapcraft/internal/lxd.py --- snapcraft-2.34/snapcraft/internal/lxd.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/lxd.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,388 +0,0 @@ -#!/usr/bin/python3 -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2017 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 . - -import json -import logging -import os -import pipes -import shutil -import sys -import tempfile -from contextlib import contextmanager -from subprocess import check_call, check_output, CalledProcessError -from time import sleep -import requests -import requests_unixsocket - -import petname -import yaml - -from snapcraft.internal.errors import ( - ContainerConnectionError, - SnapdError, -) -from snapcraft.internal import common -from snapcraft._options import _get_deb_arch - -logger = logging.getLogger(__name__) - -_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'] -# Canonical store account key -_STORE_KEY = ( - 'BWDEoaqyr25nF5SNCvEv2v7QnM9QsfCc0PBMYD_i2NGSQ32EF2d4D0hqUel3m8ul') - - -class Containerbuild: - - def __init__(self, *, output, source, project_options, - metadata, container_name, remote=None): - if not output: - output = common.format_snap_name(metadata) - self._snap_output = output - self._source = os.path.realpath(source) - self._project_options = project_options - self._metadata = metadata - self._project_folder = '/root/build_{}'.format(metadata['name']) - - if not remote: - remote = _get_default_remote() - _verify_remote(remote) - self._container_name = '{}:snapcraft-{}'.format(remote, container_name) - server_environment = self._get_remote_info()['environment'] - # Use the server architecture to avoid emulation overhead - try: - kernel = server_environment['kernel_architecture'] - except KeyError: - kernel = server_environment['kernelarchitecture'] - deb_arch = _get_deb_arch(kernel) - if not deb_arch: - raise ContainerConnectionError( - 'Unrecognized server architecture {}'.format(kernel)) - self._host_arch = deb_arch - self._image = 'ubuntu:xenial/{}'.format(deb_arch) - # Use a temporary folder the 'lxd' snap can access - lxd_common_dir = os.path.expanduser( - os.path.join('~', 'snap', 'lxd', 'common')) - os.makedirs(lxd_common_dir, exist_ok=True) - self.tmp_dir = tempfile.mkdtemp(prefix='snapcraft', dir=lxd_common_dir) - - def _get_remote_info(self): - remote = self._container_name.split(':')[0] - return yaml.load(check_output([ - 'lxc', 'info', '{}:'.format(remote)]).decode()) - - 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, cwd=None): - sh = '' - # Automatically wait on lock files before running commands - if cmd[0] == 'apt-get': - lock_file = '/var/lib/dpkg/lock' - if cmd[1] == 'update': - lock_file = '/var/lib/apt/lists/lock' - sh += 'while fuser {} >/dev/null 2>&1; do sleep 1; done; '.format( - lock_file) - if cwd: - sh += 'cd {}; '.format(cwd) - if sh: - cmd = ['sh', '-c', '{}{}'.format(sh, - ' '.join(pipes.quote(arg) for arg in cmd))] - check_call(['lxc', 'exec', self._container_name, '--'] + cmd) - - def _ensure_container(self): - check_call([ - 'lxc', 'launch', '-e', self._image, self._container_name]) - check_call([ - 'lxc', 'config', 'set', self._container_name, - 'environment.SNAPCRAFT_SETUP_CORE', '1']) - # Necessary to read asset files with non-ascii characters. - check_call([ - 'lxc', 'config', 'set', self._container_name, - 'environment.LC_ALL', 'C.UTF-8']) - - @contextmanager - def _ensure_started(self): - try: - self._ensure_container() - yield - finally: - if self._get_container_status(): - # Stopping takes a while and lxc doesn't print anything. - print('Stopping {}'.format(self._container_name)) - check_call(['lxc', 'stop', '-f', self._container_name]) - - def _get_container_status(self): - containers = json.loads(check_output([ - 'lxc', 'list', '--format=json', self._container_name]).decode()) - for container in containers: - if container['name'] == self._container_name.split(':')[-1]: - return container - - def execute(self, step='snap', args=None): - with self._ensure_started(): - self._setup_project() - self._wait_for_network() - self._container_run(['apt-get', 'update']) - self._inject_snapcraft() - command = ['snapcraft', step] - if step == 'snap': - command += ['--output', self._snap_output] - if self._host_arch != self._project_options.deb_arch: - command += ['--target-arch', self._project_options.deb_arch] - if args: - command += args - try: - self._container_run(command, cwd=self._project_folder) - except CalledProcessError as e: - if self._project_options.debug: - logger.info('Debug mode enabled, dropping into a shell') - self._container_run(['bash', '-i']) - else: - raise e - else: - # Remove temporary folder if everything went well - shutil.rmtree(self.tmp_dir) - self._finish() - - def _setup_project(self): - logger.info('Setting up container with project assets') - tar_filename = self._source - # os.sep needs to be `/` and on Windows it will be set to `\` - dst = '{}/{}'.format(self._project_folder, - os.path.basename(tar_filename)) - self._container_run(['mkdir', self._project_folder]) - self._push_file(tar_filename, dst) - self._container_run(['tar', 'xvf', os.path.basename(tar_filename)], - cwd=self._project_folder) - - def _inject_snapcraft(self): - if common.is_snap(): - # Because of https://bugs.launchpad.net/snappy/+bug/1628289 - self._container_run(['apt-get', 'install', 'squashfuse', '-y']) - - # Push core snap into container - self._inject_snap('core') - self._inject_snap('snapcraft') - else: - self._container_run(['apt-get', 'install', 'snapcraft', '-y']) - - def _inject_snap(self, name): - session = requests_unixsocket.Session() - snapd_socket = '/run/snapd.socket'.replace('/', '%2F') - # Cf. https://github.com/snapcore/snapd/wiki/REST-API#get-v2snapsname - api = 'http+unix://{}/v2/snaps/{}'.format(snapd_socket, name) - try: - json = session.request('GET', api).json() - except requests.exceptions.ConnectionError as e: - raise SnapdError( - 'Error connecting to {}'.format(api)) from e - if json['type'] == 'error': - raise SnapdError( - 'Error querying {!r} snap: {}'.format( - name, json['result']['message'])) - id = json['result']['id'] - # Lookup confinement to know if we need to --classic when installing - is_classic = json['result']['confinement'] == 'classic' - # Revisions are unique, so we don't need to know the channel - rev = json['result']['revision'] - - if not rev.startswith('x'): - self._inject_assertions('{}_{}.assert'.format(name, rev), [ - ['account-key', 'public-key-sha3-384={}'.format(_STORE_KEY)], - ['snap-declaration', 'snap-name={}'.format(name)], - ['snap-revision', 'snap-revision={}'.format(rev), - 'snap-id={}'.format(id)], - ]) - - # https://github.com/snapcore/snapd/blob/master/snap/info.go - # MountFile - filename = '{}_{}.snap'.format(name, rev) - # https://github.com/snapcore/snapd/blob/master/dirs/dirs.go - # CoreLibExecDir - installed = os.path.join(os.path.sep, 'var', 'lib', 'snapd', 'snaps', - filename) - - filepath = os.path.join(self.tmp_dir, filename) - if rev.startswith('x'): - logger.info('Making {} user-accessible'.format(filename)) - check_call(['sudo', 'cp', installed, filepath]) - check_call(['sudo', 'chown', str(os.getuid()), filepath]) - else: - shutil.copyfile(installed, filepath) - container_filename = os.path.join(os.sep, 'run', filename) - self._push_file(filepath, container_filename) - logger.info('Installing {}'.format(container_filename)) - cmd = ['snap', 'install', container_filename] - if rev.startswith('x'): - cmd.append('--dangerous') - if is_classic: - cmd.append('--classic') - self._container_run(cmd) - - def _inject_assertions(self, filename, assertions): - filepath = os.path.join(self.tmp_dir, filename) - with open(filepath, 'wb') as f: - for assertion in assertions: - logger.info('Looking up assertion {}'.format(assertion)) - f.write(check_output(['snap', 'known', *assertion])) - f.write(b'\n') - container_filename = os.path.join(os.path.sep, 'run', filename) - self._push_file(filepath, container_filename) - logger.info('Adding assertion {}'.format(filename)) - self._container_run(['snap', 'ack', container_filename]) - - def _finish(self): - # os.sep needs to be `/` and on Windows it will be set to `\` - src = '{}/{}'.format(self._project_folder, 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') - - -class Cleanbuilder(Containerbuild): - - def __init__(self, *, output=None, source, project_options, - metadata=None, remote=None): - container_name = petname.Generate(3, '-') - super().__init__(output=output, source=source, - project_options=project_options, metadata=metadata, - container_name=container_name, remote=remote) - - -class Project(Containerbuild): - - def __init__(self, *, output, source, project_options, - metadata, remote=None): - super().__init__(output=output, source=source, - project_options=project_options, - metadata=metadata, container_name=metadata['name'], - remote=remote) - - def _ensure_container(self): - if not self._get_container_status(): - check_call([ - 'lxc', 'init', self._image, self._container_name]) - if self._get_container_status()['status'] == 'Stopped': - check_call([ - 'lxc', 'config', 'set', self._container_name, - 'environment.SNAPCRAFT_SETUP_CORE', '1']) - # Map host user to root inside container - check_call([ - 'lxc', 'config', 'set', self._container_name, - 'raw.idmap', 'both {} 0'.format(os.getuid())]) - # Remove existing device (to ensure we update old containers) - devices = self._get_container_status()['devices'] - if self._project_folder in devices: - check_call([ - 'lxc', 'config', 'device', 'remove', self._container_name, - self._project_folder]) - check_call([ - 'lxc', 'start', self._container_name]) - - def _setup_project(self): - self._ensure_mount(self._project_folder, self._source) - - def _ensure_mount(self, destination, source): - logger.info('Mounting {} into container'.format(source)) - devices = self._get_container_status()['devices'] - if destination not in devices: - check_call([ - 'lxc', 'config', 'device', 'add', self._container_name, - destination, 'disk', 'source={}'.format(source), - 'path={}'.format(destination)]) - - def _finish(self): - # Nothing to do - pass - - def execute(self, step='snap', args=None): - # clean with no parts deletes the container - if step == 'clean' and args == ['--step', 'pull']: - if self._get_container_status(): - print('Deleting {}'.format(self._container_name)) - check_call(['lxc', 'delete', '-f', self._container_name]) - else: - super().execute(step, args) - - -def _get_default_remote(): - """Query and return the default lxd remote. - - Use the lxc command to query for the default lxd remote. In most - cases this will return the local remote. - - :returns: default lxd remote. - :rtype: string. - :raises snapcraft.internal.errors.ContainerConnectionError: - raised if the lxc call fails. - """ - try: - default_remote = check_output(['lxc', 'remote', 'get-default']) - except FileNotFoundError: - raise ContainerConnectionError( - 'You must have LXD installed in order to use cleanbuild.') - except CalledProcessError: - raise ContainerConnectionError( - 'Something seems to be wrong with your installation of LXD.') - return default_remote.decode(sys.getfilesystemencoding()).strip() - - -def _verify_remote(remote): - """Verify that the lxd remote exists. - - :param str remote: the lxd remote to verify. - :raises snapcraft.internal.errors.ContainerConnectionError: - raised if the lxc call listing the remote fails. - """ - # There is no easy way to grep the results from `lxc remote list` - # so we try and execute a simple operation against the remote. - try: - check_output(['lxc', 'list', '{}:'.format(remote)]) - except FileNotFoundError: - raise ContainerConnectionError( - 'You must have LXD installed in order to use cleanbuild.\n' - 'Refer to the documentation at ' - 'https://linuxcontainers.org/lxd/getting-started-cli.') - except CalledProcessError as e: - raise ContainerConnectionError( - 'There are either no permissions or the remote {!r} ' - 'does not exist.\n' - 'Verify the existing remotes by running `lxc remote list`\n' - .format(remote)) from e diff -Nru snapcraft-2.34/snapcraft/internal/mangling.py snapcraft-2.35/snapcraft/internal/mangling.py --- snapcraft-2.34/snapcraft/internal/mangling.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/mangling.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,48 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import re + +from snapcraft import file_utils + + +def rewrite_python_shebangs(root_dir): + """Recursively change #!/usr/bin/pythonX shebangs to #!/usr/bin/env pythonX + + :param str root_dir: Directory that will be crawled for shebangs. + """ + + file_pattern = re.compile(r'') + argless_shebang_pattern = re.compile( + r'\A#!.*(python\S*)$', re.MULTILINE) + shebang_pattern_with_args = re.compile( + r'\A#!.*(python\S*)[ \t\f\v]+(\S+)$', re.MULTILINE) + + file_utils.replace_in_file( + root_dir, file_pattern, argless_shebang_pattern, r'#!/usr/bin/env \1') + + # The above rewrite will barf if the shebang includes any args to python. + # For example, if the shebang was `#!/usr/bin/python3 -Es`, just replacing + # that with `#!/usr/bin/env python3 -Es` isn't going to work as `env` + # doesn't support arguments like that. + # + # The solution is to replace the shebang with one pointing to /bin/sh, and + # then exec the original shebang with included arguments. This requires + # some quoting hacks to ensure the file can be interpreted by both sh as + # well as python, but it's better than shipping our own `env`. + file_utils.replace_in_file( + root_dir, file_pattern, shebang_pattern_with_args, + r"""#!/bin/sh\n''''exec \1 \2 -- "$0" "$@" # '''""") diff -Nru snapcraft-2.34/snapcraft/internal/meta.py snapcraft-2.35/snapcraft/internal/meta.py --- snapcraft-2.34/snapcraft/internal/meta.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/meta.py 2017-11-01 19:41:33.000000000 +0000 @@ -15,8 +15,10 @@ # along with this program. If not, see . import copy +import collections import contextlib import configparser +import json import logging import os import re @@ -46,22 +48,23 @@ _MANDATORY_PACKAGE_KEYS = [ 'name', - 'description', + 'version', 'summary', + 'description', ] _OPTIONAL_PACKAGE_KEYS = [ + 'type', + 'base', 'architectures', + 'confinement', + 'grade', 'assumes', - 'base', - 'environment', - 'type', 'plugs', 'slots', - 'confinement', 'epoch', - 'grade', 'hooks', + 'environment', ] @@ -155,11 +158,21 @@ annotated_snapcraft = self._annotate_snapcraft( copy.deepcopy(self._config_data)) with open(manifest_file_path, 'w') as manifest_file: - yaml.dump(annotated_snapcraft, manifest_file) + yaml.dump( + annotated_snapcraft, manifest_file, + default_flow_style=False) def _annotate_snapcraft(self, data): - data['build-packages'] = get_global_state().assets.get( - 'build-packages', []) + image_info = os.environ.get('SNAPCRAFT_IMAGE_INFO') + if image_info: + try: + image_info_dict = json.loads(image_info) + except json.decoder.JSONDecodeError as exception: + raise errors.InvalidContainerImageInfoError( + image_info) from exception + data['image-info'] = image_info_dict + for field in ('build-packages', 'build-snaps'): + data[field] = get_global_state().assets.get(field, []) for part in data['parts']: state_dir = os.path.join(self._parts_dir, part, 'state') pull_state = get_state(state_dir, 'pull') @@ -258,11 +271,12 @@ Keys that are in _OPTIONAL_PACKAGE_KEYS are ignored if not there. """ - snap_yaml = {} + snap_yaml = collections.OrderedDict() for key_name in _MANDATORY_PACKAGE_KEYS: snap_yaml[key_name] = self._config_data[key_name] + # Reparse the version, the order should stick. snap_yaml['version'] = self._get_version( self._config_data['version'], self._config_data.get('version-script')) @@ -303,7 +317,11 @@ def _write_wrap_exe(self, wrapexec, wrappath, shebang=None, args=None, cwd=None): - args = ' '.join(args) + ' "$@"' if args else '"$@"' + if args: + quoted_args = ['"{}"'.format(arg) for arg in args] + else: + quoted_args = [] + args = ' '.join(quoted_args) + ' "$@"' if args else '"$@"' cwd = 'cd {}'.format(cwd) if cwd else '' # If we are dealing with classic confinement it means all our @@ -382,7 +400,10 @@ if os.path.splitext(f)[1] == '.desktop': os.remove(os.path.join(gui_dir, f)) for app in apps: - self._wrap_app(app, apps[app]) + adapter = apps[app].get('adapter', '') + if adapter != 'none': + self._wrap_app(app, apps[app]) + self._generate_desktop_file(app, apps[app]) return apps def _wrap_app(self, name, app): @@ -392,6 +413,8 @@ app[k] = self._wrap_exe(app[k], '{}-{}'.format(k, name)) except CommandError as e: raise errors.InvalidAppCommandError(str(e), name) + + def _generate_desktop_file(self, name, app): desktop_file_name = app.pop('desktop', '') if desktop_file_name: desktop_file = _DesktopFile( diff -Nru snapcraft-2.34/snapcraft/internal/os_release.py snapcraft-2.35/snapcraft/internal/os_release.py --- snapcraft-2.34/snapcraft/internal/os_release.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/os_release.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,89 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import contextlib + +from snapcraft.internal import errors + +_ID_TO_UBUNTU_CODENAME = { + '17.10': 'artful', + '17.04': 'zesty', + '16.04': 'xenial', + '14.04': 'trusty', +} + + +class OsRelease: + """A class to intelligently determine the OS on which we're running""" + + def __init__(self, *, os_release_file='/etc/os-release'): + """Create a new OsRelease instance. + + :param str os_release_file: Path to os-release file to be parsed. + """ + with open(os_release_file) as f: + self._os_release = {} + for line in f: + entry = line.rstrip().split('=') + if len(entry) == 2: + self._os_release[entry[0]] = entry[1].strip('"') + + def id(self): + """Return the OS ID + + :raises OsReleaseIdError: If no ID can be determined. + """ + with contextlib.suppress(KeyError): + return self._os_release['ID'] + + raise errors.OsReleaseIdError() + + def name(self): + """Return the OS name + + :raises OsReleaseNameError: If no name can be determined. + """ + with contextlib.suppress(KeyError): + return self._os_release['NAME'] + + raise errors.OsReleaseNameError() + + def version_id(self): + """Return the OS version ID + + :raises OsReleaseVersionIdError: If no version ID can be determined. + """ + with contextlib.suppress(KeyError): + return self._os_release['VERSION_ID'] + + raise errors.OsReleaseVersionIdError() + + def version_codename(self): + """Return the OS version codename + + This first tries to use the VERSION_CODENAME. If that's missing, it + tries to use the VERSION_ID to figure out the codename on its own. + + :raises OsReleaseCodenameError: If no version codename can be + determined. + """ + with contextlib.suppress(KeyError): + return self._os_release['VERSION_CODENAME'] + + with contextlib.suppress(KeyError): + return _ID_TO_UBUNTU_CODENAME[self._os_release['VERSION_ID']] + + raise errors.OsReleaseCodenameError() diff -Nru snapcraft-2.34/snapcraft/internal/pluginhandler/__init__.py snapcraft-2.35/snapcraft/internal/pluginhandler/__init__.py --- snapcraft-2.34/snapcraft/internal/pluginhandler/__init__.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/pluginhandler/__init__.py 2017-11-01 19:41:33.000000000 +0000 @@ -335,10 +335,18 @@ def mark_build_done(self): build_properties = self.plugin.get_build_properties() plugin_manifest = self.plugin.get_manifest() + machine_manifest = self._get_machine_manifest() self.mark_done('build', states.BuildState( - build_properties, self._part_properties, - self._project_options, plugin_manifest)) + build_properties, self._part_properties, self._project_options, + plugin_manifest, machine_manifest)) + + def _get_machine_manifest(self): + return { + 'uname': common.run_output(['uname', '-srvmpio']), + 'installed-packages': repo.Repo.get_installed_packages(), + 'installed-snaps': repo.snaps.get_installed_snaps() + } def clean_build(self, hint=''): if self.is_clean('build'): diff -Nru snapcraft-2.34/snapcraft/internal/pluginhandler/_scriptlets.py snapcraft-2.35/snapcraft/internal/pluginhandler/_scriptlets.py --- snapcraft-2.34/snapcraft/internal/pluginhandler/_scriptlets.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/pluginhandler/_scriptlets.py 2017-11-01 19:41:33.000000000 +0000 @@ -34,7 +34,7 @@ try: with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: - f.write('#!/bin/sh\n') + f.write('#!/bin/sh -e\n') f.write(scriptlet) f.flush() scriptlet_path = f.name diff -Nru snapcraft-2.34/snapcraft/internal/project_loader/_config.py snapcraft-2.35/snapcraft/internal/project_loader/_config.py --- snapcraft-2.34/snapcraft/internal/project_loader/_config.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/project_loader/_config.py 2017-11-01 19:41:33.000000000 +0000 @@ -25,6 +25,7 @@ import snapcraft from snapcraft.internal import ( + deprecations, remote_parts, states, ) @@ -136,6 +137,10 @@ aliases = [] for app_name, app in self.data.get('apps', {}).items(): aliases.extend(app.get('aliases', [])) + + # The aliases property is actually deprecated: + if aliases: + deprecations.handle_deprecation_notice('dn5') seen = set() duplicates = set() for alias in aliases: @@ -196,8 +201,8 @@ def project_env(self): return [ - 'SNAPCRAFT_STAGE={}'.format(self._project_options.stage_dir), - 'SNAPCRAFT_PROJECT_NAME={}'.format(self.data['name']), + 'SNAPCRAFT_STAGE="{}"'.format(self._project_options.stage_dir), + 'SNAPCRAFT_PROJECT_NAME="{}"'.format(self.data['name']), 'SNAPCRAFT_PROJECT_VERSION={}'.format(self.data['version']), 'SNAPCRAFT_PROJECT_GRADE={}'.format(self.data['grade']), ] diff -Nru snapcraft-2.34/snapcraft/internal/project_loader/_env.py snapcraft-2.35/snapcraft/internal/project_loader/_env.py --- snapcraft-2.34/snapcraft/internal/project_loader/_env.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/project_loader/_env.py 2017-11-01 19:41:33.000000000 +0000 @@ -106,6 +106,6 @@ arch_triplet, core_dynamic_linker=None): env = build_env(stagedir, snap_name, confinement, arch_triplet, core_dynamic_linker) - env.append('PERL5LIB={0}/usr/share/perl5/'.format(stagedir)) + env.append('PERL5LIB="{0}/usr/share/perl5/"'.format(stagedir)) return env diff -Nru snapcraft-2.34/snapcraft/internal/project_loader/errors.py snapcraft-2.35/snapcraft/internal/project_loader/errors.py --- snapcraft-2.34/snapcraft/internal/project_loader/errors.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/project_loader/errors.py 2017-11-01 19:41:33.000000000 +0000 @@ -69,29 +69,20 @@ messages = [] - # error.validator_value may contain a custom validation error message. - # If so, use it instead of the garbage message jsonschema gives us. - with contextlib.suppress(TypeError, KeyError): - messages.append( - error.validator_value['validation-failure'].format(error)) + preamble = _determine_preamble(error) + cause = _determine_cause(error) + supplement = _determine_supplemental_info(error) - if not messages: - messages.append(error.message) + if preamble: + messages.append(preamble) - path = [] - while error.absolute_path: - element = error.absolute_path.popleft() - # assume numbers are indices and use 'xxx[123]' notation. - if isinstance(element, int): - path[-1] = '{}[{}]'.format(path[-1], element) - else: - path.append(str(element)) - if path: - messages.insert(0, "The '{}' property does not match the " - "required schema:".format('/'.join(path))) - cause = error.cause or _determine_cause(error) - if cause: - messages.append('({})'.format(cause)) + if supplement: + messages.append(error.message) + messages.append('({})'.format(supplement)) + elif cause: + messages.append(cause) + else: + messages.append(error.message) return cls(' '.join(messages)) @@ -107,23 +98,65 @@ super().__init__(message=message) +def _determine_preamble(error): + messages = [] + path = _determine_property_path(error) + if path: + messages.append( + "The '{}' property does not match the required schema:".format( + '/'.join(path))) + return ' '.join(messages) + + def _determine_cause(error): - """Attempt to determine a cause from validation error. + messages = [] + + # error.validator_value may contain a custom validation error message. + # If so, use it instead of the garbage message jsonschema gives us. + with contextlib.suppress(TypeError, KeyError): + messages.append( + error.validator_value['validation-failure'].format(error)) + + # The schema itself may have a custom validation error message. If so, + # use it as well. + with contextlib.suppress(AttributeError, TypeError, KeyError): + key = error + if (error.schema.get('type') == 'object' and + error.validator == 'additionalProperties'): + key = list(error.instance.keys())[0] - :return: A string representing the cause of the error (it may be empty if - no cause can be determined). - :rtype: str - """ + messages.append( + error.schema['validation-failure'].format(key)) + return ' '.join(messages) + + +def _determine_supplemental_info(error): message = _VALIDATION_ERROR_CAUSES.get(error.validator, '').format( validator_value=error.validator_value) if not message and error.validator == 'anyOf': message = _interpret_anyOf(error) + if not message and error.cause: + message = error.cause + return message +def _determine_property_path(error): + path = [] + while error.absolute_path: + element = error.absolute_path.popleft() + # assume numbers are indices and use 'xxx[123]' notation. + if isinstance(element, int): + path[-1] = '{}[{}]'.format(path[-1], element) + else: + path.append(str(element)) + + return path + + def _interpret_anyOf(error): """Interpret a validation error caused by the anyOf validator. diff -Nru snapcraft-2.34/snapcraft/internal/project_loader/_parts_config.py snapcraft-2.35/snapcraft/internal/project_loader/_parts_config.py --- snapcraft-2.34/snapcraft/internal/project_loader/_parts_config.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/project_loader/_parts_config.py 2017-11-01 19:41:33.000000000 +0000 @@ -229,7 +229,7 @@ self._confinement, self._project_options.arch_triplet, core_dynamic_linker=core_dynamic_linker) - env.append('SNAPCRAFT_PART_INSTALL={}'.format(part.installdir)) + env.append('SNAPCRAFT_PART_INSTALL="{}"'.format(part.installdir)) env.append('SNAPCRAFT_PARALLEL_BUILD_COUNT={}'.format( self._project_options.parallel_build_count)) else: diff -Nru snapcraft-2.34/snapcraft/internal/repo/_base.py snapcraft-2.35/snapcraft/internal/repo/_base.py --- snapcraft-2.34/snapcraft/internal/repo/_base.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/repo/_base.py 2017-11-01 19:41:33.000000000 +0000 @@ -24,6 +24,8 @@ import stat from snapcraft import file_utils +from snapcraft.internal import mangling +from . import errors _BIN_PATHS = ( 'bin', @@ -64,7 +66,7 @@ directories. :rtype: set. """ - raise NotImplemented() + raise errors.NoNativeBackendError() @classmethod def get_packages_for_source_type(cls, source_type): @@ -87,7 +89,7 @@ :returns: a set of packages that need to be installed on the host. :rtype: set of strings. """ - raise NotImplementedError() + raise errors.NoNativeBackendError() @classmethod def install_build_packages(cls, package_names): @@ -98,7 +100,7 @@ :raises snapcraft.repo.errors.BuildPackageNotFoundError: if one of the package_names cannot be installed. """ - raise NotImplementedError() + raise errors.NoNativeBackendError() @classmethod def build_package_is_valid(cls, package_name): @@ -107,7 +109,7 @@ :param package_name: a package name to check. :type package_name: str """ - raise NotImplementedError() + raise errors.NoNativeBackendError() @classmethod def is_package_installed(cls, package_name): @@ -117,7 +119,15 @@ :returns: True if package_name is installed if not False. :rtype: boolean """ - raise NotImplementedError() + raise errors.NoNativeBackendError() + + @classmethod + def get_installed_packages(cls): + """Return a list of the installed packages and their versions + + :rtype: list of strings with the form package=version. + """ + raise errors.NoNativeBackendError() def __init__(self, rootdir, *args, **kwargs): """Initialize a repository handler. @@ -142,7 +152,7 @@ :raises snapcraft.repo.errors.PackageNotFoundError: when a package in package_names is not found. """ - raise NotImplemented() + raise errors.NoNativeBackendError() def unpack(self, unpackdir): """Unpack obtained packages into unpackdir. @@ -155,7 +165,7 @@ :param str unpackdir: target directory to unpack packages to. """ - raise NotImplemented() + raise errors.NoNativeBackendError() def normalize(self, unpackdir): """Normalize artifacts in unpackdir. @@ -229,15 +239,13 @@ paths = [p for p in _BIN_PATHS if os.path.exists(os.path.join(unpackdir, p))] for p in [os.path.join(unpackdir, p) for p in paths]: - file_utils.replace_in_file(p, re.compile(r''), - re.compile(r'#!.*python\n'), - r'#!/usr/bin/env python\n') + mangling.rewrite_python_shebangs(p) class DummyRepo(BaseRepo): def get_packages_for_source_type(*args, **kwargs): - pass + return set() def _try_copy_local(path, target): diff -Nru snapcraft-2.34/snapcraft/internal/repo/_deb.py snapcraft-2.35/snapcraft/internal/repo/_deb.py --- snapcraft-2.34/snapcraft/internal/repo/_deb.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/repo/_deb.py 2017-11-01 19:41:33.000000000 +0000 @@ -32,7 +32,7 @@ import snapcraft from snapcraft import file_utils -from snapcraft.internal import cache, repo, common +from snapcraft.internal import cache, repo, common, os_release from snapcraft.internal.indicators import is_dumb_terminal from ._base import BaseRepo from . import errors @@ -137,7 +137,7 @@ finally: apt_cache.close() except Exception as e: - logger.debug('Exception occured: {!r}'.format(e)) + logger.debug('Exception occurred: {!r}'.format(e)) raise e def sources_digest(self): @@ -146,10 +146,10 @@ def _collected_sources_list(self): if self._use_geoip or self._sources_list: - release = common.get_os_release_info()['VERSION_CODENAME'] + release = os_release.OsRelease() return _format_sources_list( self._sources_list, deb_arch=self._deb_arch, - use_geoip=self._use_geoip, release=release) + use_geoip=self._use_geoip, release=release.version_codename()) return _get_local_sources_list() @@ -260,6 +260,17 @@ with apt.Cache() as apt_cache: return apt_cache[package_name].installed + @classmethod + def get_installed_packages(cls): + installed_packages = [] + with apt.Cache() as apt_cache: + for package in apt_cache: + if package.installed: + installed_packages.append( + '{}={}'.format( + package.name, package.installed.version)) + return installed_packages + def __init__(self, rootdir, sources=None, project_options=None): super().__init__(rootdir) self._downloaddir = os.path.join(rootdir, 'download') diff -Nru snapcraft-2.34/snapcraft/internal/repo/errors.py snapcraft-2.35/snapcraft/internal/repo/errors.py --- snapcraft-2.34/snapcraft/internal/repo/errors.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/repo/errors.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from snapcraft.internal.common import get_os_release_info +from snapcraft.internal.os_release import OsRelease from ._platform import _is_deb_based from snapcraft.internal import errors @@ -23,6 +23,15 @@ pass +class NoNativeBackendError(RepoError): + + fmt = ("Native builds aren't supported on {distro}. " + "You can however use 'snapcraft cleanbuild' with a container.") + + def __init__(self): + super().__init__(distro=OsRelease().name()) + + class BuildPackageNotFoundError(RepoError): fmt = "Could not find a required package in 'build-packages': {package}" @@ -38,7 +47,7 @@ message = 'The package {!r} was not found.'.format( self.package_name) # If the package was multiarch, try to help. - distro = get_os_release_info()['ID'] + distro = OsRelease().id() if _is_deb_based(distro) and ':' in self.package_name: (name, arch) = self.package_name.split(':', 2) if arch: diff -Nru snapcraft-2.34/snapcraft/internal/repo/_platform.py snapcraft-2.35/snapcraft/internal/repo/_platform.py --- snapcraft-2.34/snapcraft/internal/repo/_platform.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/repo/_platform.py 2017-11-01 19:41:33.000000000 +0000 @@ -16,7 +16,7 @@ import logging -from snapcraft.internal.common import get_os_release_info +from snapcraft.internal.os_release import OsRelease logger = logging.getLogger(__name__) @@ -24,18 +24,19 @@ 'ubuntu', 'debian', 'elementary OS', + 'elementary', 'neon', ] def _is_deb_based(distro=None): if not distro: - distro = get_os_release_info()['ID'] + distro = OsRelease().id() return distro in _DEB_BASED_PLATFORM def _get_repo_for_platform(): - distro = get_os_release_info()['ID'] + distro = OsRelease().id() if _is_deb_based(distro): from ._deb import Ubuntu return Ubuntu diff -Nru snapcraft-2.34/snapcraft/internal/repo/snaps.py snapcraft-2.35/snapcraft/internal/repo/snaps.py --- snapcraft-2.34/snapcraft/internal/repo/snaps.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/repo/snaps.py 2017-11-01 19:41:33.000000000 +0000 @@ -160,13 +160,21 @@ def install_snaps(snaps_list): - """Install snaps of the format /.""" + """Install snaps of the format /. + + :return: a list of "name=revision" for the snaps installed. + """ + snaps_installed = [] for snap in snaps_list: snap_pkg = SnapPackage(snap) if not snap_pkg.installed and snap_pkg.in_store: snap_pkg.install() elif snap_pkg.get_current_channel() != snap_pkg.channel: snap_pkg.refresh() + snap_pkg = SnapPackage(snap) + snaps_installed.append('{}={}'.format( + snap_pkg.name, snap_pkg.get_local_snap_info()['revision'])) + return snaps_installed def _snap_command_requires_sudo(): @@ -185,6 +193,19 @@ return requires_root +def get_installed_snaps(): + """Return all the snaps installed in the system. + + :return: a list of "name=revision" for the snaps installed. + """ + try: + local_snaps = _get_local_snaps() + except exceptions.ConnectionError as e: + local_snaps = [] + return ['{}={}'.format(snap['name'], snap['revision']) for + snap in local_snaps] + + def _get_parsed_snap(snap): if '/' in snap: sep_index = snap.find('/') @@ -218,3 +239,12 @@ snap_info = session.get(url) snap_info.raise_for_status() return snap_info.json()['result'][0] + + +def _get_local_snaps(): + slug = 'snaps' + url = get_snapd_socket_path_template().format(slug) + with requests_unixsocket.Session() as session: + snap_info = session.get(url) + snap_info.raise_for_status() + return snap_info.json()['result'] diff -Nru snapcraft-2.34/snapcraft/internal/sources/_deb.py snapcraft-2.35/snapcraft/internal/sources/_deb.py --- snapcraft-2.34/snapcraft/internal/sources/_deb.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/sources/_deb.py 2017-11-01 19:41:33.000000000 +0000 @@ -13,12 +13,13 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -import debian.debfile import os import shutil +import tarfile import tempfile +import debian.arfile + from . import errors from ._base import FileBase @@ -53,8 +54,17 @@ os.makedirs(dst) shutil.move(tmp_deb, deb_file) - deb = debian.debfile.DebFile(deb_file) - deb.data.tgz().extractall(dst) + # Importing DebFile causes LP: #1731478 when snapcraft is + # run as a snap. + deb_ar = debian.arfile.ArFile(deb_file) + try: + data_member_name = [ + i for i in deb_ar.getnames() if i.startswith('data.tar')][0] + except IndexError: + raise errors.InvalidDebError(deb_file=deb_file) + data_member = deb_ar.getmember(data_member_name) + with tarfile.open(fileobj=data_member) as tar: + tar.extractall(dst) if not keep_deb: os.remove(deb_file) diff -Nru snapcraft-2.34/snapcraft/internal/sources/errors.py snapcraft-2.35/snapcraft/internal/sources/errors.py --- snapcraft-2.34/snapcraft/internal/sources/errors.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/sources/errors.py 2017-11-01 19:41:33.000000000 +0000 @@ -36,3 +36,10 @@ def __init__(self, expected, calculated): super().__init__(expected=expected, calculated=calculated) + + +class InvalidDebError(errors.SnapcraftError): + + fmt = ('The {deb_file} used does not contain valid data. ' + 'Ensure a proper deb file is passed for .deb files ' + 'as sources.') diff -Nru snapcraft-2.34/snapcraft/internal/sources/_subversion.py snapcraft-2.35/snapcraft/internal/sources/_subversion.py --- snapcraft-2.34/snapcraft/internal/sources/_subversion.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/sources/_subversion.py 2017-11-01 19:41:33.000000000 +0000 @@ -80,14 +80,12 @@ source = self.source commit = self.source_commit - if not commit: # retrieve the commit id - lines = subprocess.check_output(['svn', 'info', self.source_dir] - ).decode('utf-8').split('\n') - prefix = 'Last Changed Rev: ' - for line in lines: - if line.startswith(prefix): - commit = line.replace(prefix, '') - break + if not commit: + commit = subprocess.check_output( + ['svn', 'info', + '--show-item', 'last-changed-revision', + '--no-newline', + self.source_dir]).decode('utf-8').strip() return { 'source-commit': commit, diff -Nru snapcraft-2.34/snapcraft/internal/sources/_zip.py snapcraft-2.35/snapcraft/internal/sources/_zip.py --- snapcraft-2.34/snapcraft/internal/sources/_zip.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/sources/_zip.py 2017-11-01 19:41:33.000000000 +0000 @@ -52,7 +52,11 @@ os.makedirs(dst) shutil.move(tmp_zip, zip) - zipfile.ZipFile(zip).extractall(path=dst) + # Workaround for: https://bugs.python.org/issue15795 + with zipfile.ZipFile(zip, 'r') as f: + for info in f.infolist(): + extracted_file = f.extract(info.filename, path=dst) + os.chmod(extracted_file, info.external_attr >> 16) if not keep_zip: os.remove(zip) diff -Nru snapcraft-2.34/snapcraft/internal/states/_build_state.py snapcraft-2.35/snapcraft/internal/states/_build_state.py --- snapcraft-2.34/snapcraft/internal/states/_build_state.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/states/_build_state.py 2017-11-01 19:41:33.000000000 +0000 @@ -32,7 +32,10 @@ 'build-attributes', 'build-packages', 'disable-parallel', - 'organize' + 'organize', + 'prepare', + 'build', + 'install', } @@ -41,7 +44,7 @@ def __init__( self, property_names, part_properties=None, project=None, - plugin_assets=None): + plugin_assets=None, machine_assets=None): # Save this off before calling super() since we'll need it # FIXME: for 3.x the name `schema_properties` is leaking # implementation details from a higher layer. @@ -50,6 +53,8 @@ self.assets = plugin_assets else: self.assets = {} + if machine_assets: + self.assets.update(machine_assets) super().__init__(part_properties, project) diff -Nru snapcraft-2.34/snapcraft/internal/states/_global_state.py snapcraft-2.35/snapcraft/internal/states/_global_state.py --- snapcraft-2.34/snapcraft/internal/states/_global_state.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/internal/states/_global_state.py 2017-11-01 19:41:33.000000000 +0000 @@ -30,8 +30,9 @@ yaml_tag = u'!GlobalState' - def __init__(self, build_packages): + def __init__(self, build_packages, build_snaps): super().__init__() self.assets = { 'build-packages': build_packages, + 'build-snaps': build_snaps, } diff -Nru snapcraft-2.34/snapcraft/_options.py snapcraft-2.35/snapcraft/_options.py --- snapcraft-2.34/snapcraft/_options.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/_options.py 2017-11-01 19:41:33.000000000 +0000 @@ -63,7 +63,7 @@ 'cross-build-packages': ['gcc-powerpc64le-linux-gnu', 'libc6-dev-ppc64el-cross'], 'triplet': 'powerpc64le-linux-gnu', - 'core-dynamic-linker': '/lib64/ld64.so.2', + 'core-dynamic-linker': 'lib64/ld64.so.2', }, 'ppc': { 'kernel': 'powerpc', @@ -89,7 +89,7 @@ 'cross-build-packages': ['gcc-s390x-linux-gnu', 'libc6-dev-s390x-cross'], 'triplet': 's390x-linux-gnu', - 'core-dynamic-linker': '/lib/ld64.so.1', + 'core-dynamic-linker': 'lib/ld64.so.1', } } @@ -150,8 +150,19 @@ return self.__target_machine != self.__platform_arch @property + def target_arch(self): + return self.__target_arch + + @property def cross_compiler_prefix(self): try: + # cross-compilation of x86 32bit binaries on a x86_64 host is + # possible by reusing the native toolchain - let Kbuild figure + # it out by itself and pass down an empy cross-compiler-prefix + # to start the build + if (self.__platform_arch == 'x86_64' and + self.__target_machine == 'i686'): + return '' return self.__machine_info['cross-compiler-prefix'] except KeyError: raise SnapcraftEnvironmentError( @@ -248,6 +259,7 @@ def _set_machine(self, target_deb_arch): self.__platform_arch = _get_platform_architecture() + self.__target_arch = target_deb_arch if not target_deb_arch: self.__target_machine = self.__platform_arch else: diff -Nru snapcraft-2.34/snapcraft/plugins/autotools.py snapcraft-2.35/snapcraft/plugins/autotools.py --- snapcraft-2.34/snapcraft/plugins/autotools.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/autotools.py 2017-11-01 19:41:33.000000000 +0000 @@ -89,15 +89,6 @@ raise RuntimeError('Unsupported installation method: "{}"'.format( options.install_via)) - def env(self, root): - env = super().env(root) - if self.project.is_cross_compiling: - env.extend([ - 'CC={}-gcc'.format(self.project.arch_triplet), - 'CXX={}-g++'.format(self.project.arch_triplet), - ]) - return env - def enable_cross_compilation(self): pass @@ -129,7 +120,8 @@ else: configure_command.append('--prefix=' + self.installdir) if self.project.is_cross_compiling: - configure_command.append('--host={}'.format(self.project.deb_arch)) + configure_command.append('--host={}'.format( + self.project.arch_triplet)) self.run(configure_command + self.options.configflags) self.make() diff -Nru snapcraft-2.34/snapcraft/plugins/catkin.py snapcraft-2.35/snapcraft/plugins/catkin.py --- snapcraft-2.34/snapcraft/plugins/catkin.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/catkin.py 2017-11-01 19:41:33.000000000 +0000 @@ -54,6 +54,10 @@ (string) Run-time path of the underlay workspace (e.g. a subdirectory of the content interface's 'target' attribute.) + - catkin-ros-master-uri: + (string) + The URI to ros master setting the env variable ROS_MASTER_URI. Defaults + to http://localhost:11311. """ import contextlib @@ -68,13 +72,17 @@ import snapcraft from snapcraft.plugins import _ros +from snapcraft.plugins import _python from snapcraft import ( common, file_utils, formatting_utils, repo, ) -from snapcraft.internal import errors +from snapcraft.internal import ( + errors, + mangling, +) logger = logging.getLogger(__name__) @@ -88,6 +96,7 @@ _SUPPORTED_DEPENDENCY_TYPES = { 'apt', + 'pip', } @@ -178,6 +187,11 @@ 'default': [], } + schema['properties']['catkin-ros-master-uri'] = { + 'type': 'string', + 'default': 'http://localhost:11311' + } + schema['required'].append('catkin-packages') return schema @@ -193,7 +207,18 @@ def get_build_properties(cls): # Inform Snapcraft of the properties associated with building. If these # change in the YAML Snapcraft will consider the build step dirty. - return ['build-attributes', 'catkin-cmake-args'] + return ['catkin-cmake-args'] + + @property + def _pip(self): + if not self.__pip: + self.__pip = _python.Pip( + python_major_version='2', # ROS1 only supports python2 + part_dir=self.partdir, + install_dir=self.installdir, + stage_dir=self.project.stage_dir) + + return self.__pip @property def PLUGIN_STAGE_SOURCES(self): @@ -207,7 +232,8 @@ def __init__(self, name, options, project): super().__init__(name, options, project) - self.build_packages.extend(['libc6-dev', 'make']) + self.build_packages.extend(['libc6-dev', 'make', 'python-pip']) + self.__pip = None # roslib is the base requiremet to actually create a workspace with # setup.sh and the necessary hooks. @@ -260,8 +286,8 @@ env = [ # This environment variable tells ROS nodes where to find ROS # master. It does not affect ROS master, however-- this is just the - # default URI. - 'ROS_MASTER_URI=http://localhost:11311', + # URI. + 'ROS_MASTER_URI={}'.format(self.options.catkin_ros_master_uri), # Various ROS tools (e.g. rospack, roscore) keep a cache or a log, # and use $ROS_HOME to determine where to put them. @@ -286,7 +312,12 @@ # The ROS packaging system tools (e.g. rospkg, etc.) don't go # into the ROS install path (/opt/ros/$distro), so we need the # PYTHONPATH to include the dist-packages in /usr/lib as well. - env.append('PYTHONPATH={0}:$PYTHONPATH'.format( + # + # Note: Empty segments in PYTHONPATH are interpreted as `.`, thus + # adding the current working directory to the PYTHONPATH. That is + # not desired in this situation, so take proper precautions when + # expanding PYTHONPATH: only add it if it's not empty. + env.append('PYTHONPATH={}${{PYTHONPATH:+:$PYTHONPATH}}'.format( common.get_python2_path(root))) except errors.SnapcraftEnvironmentError as e: logger.debug(e) @@ -408,27 +439,42 @@ 'Unable to determine system dependency for roscore') # Pull down and install any apt dependencies that were discovered - apt_dependencies = system_dependencies.get('apt') + self._setup_apt_dependencies(system_dependencies.get('apt')) + + # Pull down and install any pip dependencies that were discovered + self._setup_pip_dependencies(system_dependencies.get('pip')) + + def _setup_apt_dependencies(self, apt_dependencies): if apt_dependencies: ubuntudir = os.path.join(self.partdir, 'ubuntu') os.makedirs(ubuntudir, exist_ok=True) - logger.info('Preparing to fetch package dependencies...') + logger.info('Preparing to fetch apt dependencies...') ubuntu = repo.Ubuntu(ubuntudir, sources=self.PLUGIN_STAGE_SOURCES, project_options=self.project) - logger.info('Fetching package dependencies...') + logger.info('Fetching apt dependencies...') try: ubuntu.get(apt_dependencies) except repo.errors.PackageNotFoundError as e: raise RuntimeError( - 'Failed to fetch system dependencies: {}'.format( + 'Failed to fetch apt dependencies: {}'.format( e.message)) - logger.info('Installing package dependencies...') + logger.info('Installing apt dependencies...') ubuntu.unpack(self.installdir) + def _setup_pip_dependencies(self, pip_dependencies): + if pip_dependencies: + self._pip.setup() + + logger.info('Fetching pip dependencies...') + self._pip.download(pip_dependencies) + + logger.info('Installing pip dependencies...') + self._pip.install(pip_dependencies) + def clean_pull(self): super().clean_pull() @@ -444,6 +490,9 @@ with contextlib.suppress(FileNotFoundError): shutil.rmtree(self._catkin_path) + # Clean pip packages, if any + self._pip.clean_packages() + def _source_setup_sh(self, root, underlay_path): rosdir = os.path.join(root, 'opt', 'ros', self.options.rosdistro) if underlay_path: @@ -603,11 +652,16 @@ underlay_run_path = self.options.underlay['run-path'] self._generate_snapcraft_setup_sh('$SNAP', underlay_run_path) + # If pip dependencies were installed, generate a sitecustomize that + # allows access to them. + if self._pip.is_setup() and self._pip.list(user=True): + _python.generate_sitecustomize( + '2', stage_dir=self.project.stage_dir, + install_dir=self.installdir) + def _use_in_snap_python(self): # Fix all shebangs to use the in-snap python. - file_utils.replace_in_file(self.rosdir, re.compile(r''), - re.compile(r'^#!.*python'), - r'#!/usr/bin/env python') + mangling.rewrite_python_shebangs(self.installdir) # Also replace the python usage in 10.ros.sh to use the in-snap python. ros10_file = os.path.join(self.rosdir, @@ -909,9 +963,14 @@ formatting_utils.format_path_variable( 'PATH', bin_paths, prepend='', separator=':'))) - lines.append('export _CATKIN_SETUP_DIR={}'.format(self._workspace)) - lines.append('source {}'.format(os.path.join( - self._workspace, 'setup.sh'))) + # Source our own workspace so we have all of Catkin's dependencies, + # then source the workspace we're actually supposed to be crawling. + lines.append('_CATKIN_SETUP_DIR={} source {}'.format( + ros_path, os.path.join(ros_path, 'setup.sh'))) + lines.append('_CATKIN_SETUP_DIR={} source {} --extend'.format( + self._workspace, + os.path.join(self._workspace, 'setup.sh'))) + lines.append('exec "$@"') f.write('\n'.join(lines)) f.flush() diff -Nru snapcraft-2.34/snapcraft/plugins/dotnet.py snapcraft-2.35/snapcraft/plugins/dotnet.py --- snapcraft-2.34/snapcraft/plugins/dotnet.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/dotnet.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,142 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +"""The dotnet plugin is used to build dotnet core runtime parts.' + +The plugin uses the dotnet SDK to install dependencies from nuget +and follows standard semantics from a dotnet core project. + +This plugin uses the common plugin keywords as well as those for "sources". +For more information check the 'plugins' topic for the former and the +'sources' topic for the latter. + +The plugin will take into account the following build-attributes: + + - debug: builds using a Debug configuration. + +""" + +import os +import shutil +import fnmatch + +import snapcraft +from snapcraft import sources + + +_RUNTIME_DEFAULT = '2.0.0' +_SDK_DEFAULT = '2.0.0' +# TODO extend for more than xenial +_SDKS_AMD64 = { + '2.0.0': dict(url_path='http://dotnetcli.blob.core.windows.net/dotnet/' + 'Sdk/2.0.0/dotnet-sdk-2.0.0-linux-x64.tar.gz', + checksum='sha256/6059a6f72fb7aa6205ef4b52583e9c041f' + 'd128e768870a0fc4a33ed84c98ca6b') + } +# TODO extend for other architectures +_SDK_DICT_FOR_ARCH = { + 'amd64': _SDKS_AMD64, +} + + +class DotNetPlugin(snapcraft.BasePlugin): + + @classmethod + def schema(cls): + schema = super().schema() + + if 'required' in schema: + del schema['required'] + + return schema + + def __init__(self, name, options, project): + super().__init__(name, options, project) + + self._dotnet_dir = os.path.join(self.partdir, 'dotnet') + self._dotnet_sdk_dir = os.path.join(self._dotnet_dir, 'sdk') + + self.stage_packages.extend([ + 'libcurl3', + 'libcurl3-gnutls', + 'libicu55', + 'liblttng-ust0', + 'libunwind8', + 'lldb', + 'libssl1.0.0', + 'libgssapi-krb5-2', + 'libc6', + 'zlib1g', + 'libgcc1' + ]) + + self._sdk = self._get_sdk() + self._dotnet_cmd = os.path.join(self._dotnet_sdk_dir, 'dotnet') + + def _get_sdk(self): + try: + sdk_arch = _SDK_DICT_FOR_ARCH[self.project.deb_arch] + except KeyError as missing_arch: + raise NotImplementedError( + 'This plugin does not support architecture ' + '{}'.format(missing_arch)) + # TODO support more SDK releases + sdk_version = sdk_arch['2.0.0'] + + sdk_url = sdk_version['url_path'] + return sources.Tar(sdk_url, self._dotnet_sdk_dir, + source_checksum=sdk_version['checksum']) + + def pull(self): + super().pull() + + os.makedirs(self._dotnet_sdk_dir, exist_ok=True) + + self._sdk.pull() + + def clean_pull(self): + super().clean_pull() + + # Remove the dotnet directory (if any) + if os.path.exists(self._dotnet_dir): + shutil.rmtree(self._dotnet_dir) + + def build(self): + super().build() + + if 'debug' in self.options.build_attributes: + configuration = 'Debug' + else: + configuration = 'Release' + + self.run([self._dotnet_cmd, 'build', '-c', configuration]) + + publish_cmd = [self._dotnet_cmd, 'publish', '-c', configuration, + '-o', self.installdir] + # Build command for self-contained application + publish_cmd += ['--self-contained', '-r', 'linux-x64'] + self.run(publish_cmd) + + # Workaround to set the right permission for the executable. + appname = os.path.join(self.installdir, self._get_appname()) + if os.path.exists(appname): + os.chmod(appname, 0o755) + + def _get_appname(self): + for file in os.listdir(self.builddir): + if fnmatch.fnmatch(file, '*.??proj'): + return os.path.splitext(file)[0] + break diff -Nru snapcraft-2.34/snapcraft/plugins/kbuild.py snapcraft-2.35/snapcraft/plugins/kbuild.py --- snapcraft-2.34/snapcraft/plugins/kbuild.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/kbuild.py 2017-11-01 19:41:33.000000000 +0000 @@ -109,8 +109,7 @@ def get_build_properties(cls): # Inform Snapcraft of the properties associated with building. If these # change in the YAML Snapcraft will consider the build step dirty. - return ['kdefconfig', 'kconfigfile', 'kconfigs', 'kconfigflavour', - 'build-attributes'] + return ['kdefconfig', 'kconfigfile', 'kconfigs', 'kconfigflavour'] def __init__(self, name, options, project): super().__init__(name, options, project) @@ -182,6 +181,10 @@ return os.path.join(self.builddir, '.config') def do_base_config(self, config_path): + # if the parts build dir already contains a .config file, + # use it + if os.path.isfile(config_path): + return # if kconfigfile is provided use that # elif kconfigflavour is provided, assemble the ubuntu.flavour config # otherwise use defconfig to seed the base config diff -Nru snapcraft-2.34/snapcraft/plugins/kernel.py snapcraft-2.35/snapcraft/plugins/kernel.py --- snapcraft-2.34/snapcraft/plugins/kernel.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/kernel.py 2017-11-01 19:41:33.000000000 +0000 @@ -453,7 +453,7 @@ def pull(self): super().pull() snapcraft.download( - 'ubuntu-core', 'edge', self.os_snap, self.project.deb_arch) + 'core', 'stable', self.os_snap, self.project.deb_arch) def do_configure(self): super().do_configure() diff -Nru snapcraft-2.34/snapcraft/plugins/nodejs.py snapcraft-2.35/snapcraft/plugins/nodejs.py --- snapcraft-2.34/snapcraft/plugins/nodejs.py 2017-09-10 17:25:47.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/nodejs.py 2017-11-01 19:41:33.000000000 +0000 @@ -40,10 +40,15 @@ The language package manager to use to drive installation of node packages. Can be either `npm` (default) or `yarn`. """ + +import collections import contextlib +import json import logging import os import shutil +import subprocess +import sys import snapcraft from snapcraft import sources @@ -53,7 +58,7 @@ logger = logging.getLogger(__name__) _NODEJS_BASE = 'node-v{version}-linux-{arch}' -_NODEJS_VERSION = '6.10.2' +_NODEJS_VERSION = '6.11.4' _NODEJS_TMPL = 'https://nodejs.org/dist/v{version}/{base}.tar.gz' _NODEJS_ARCHES = { 'i386': 'x86', @@ -126,6 +131,7 @@ logger.warning( 'EXPERIMENTAL: use of yarn to manage packages is experimental') self._yarn_tar = sources.Tar(_YARN_URL, self._npm_dir) + self._manifest = collections.OrderedDict() def pull(self): super().pull() @@ -149,13 +155,22 @@ def build(self): super().build() if self.options.node_package_manager == 'npm': - self._npm_install(rootdir=self.builddir) + installed_node_packages = self._npm_install(rootdir=self.builddir) # Copy the content of the symlink to the build directory # LP: #1702661 modules_dir = os.path.join(self.installdir, 'lib', 'node_modules') _copy_symlinked_content(modules_dir) else: - self._yarn_install(rootdir=self.builddir) + installed_node_packages = self._yarn_install(rootdir=self.builddir) + lock_file_path = os.path.join(self.sourcedir, 'yarn.lock') + if os.path.isfile(lock_file_path): + with open(lock_file_path) as lock_file: + self._manifest['yarn-lock-contents'] = lock_file.read() + + self._manifest['node-packages'] = [ + '{}={}'.format(name, installed_node_packages[name]) + for name in installed_node_packages + ] def _npm_install(self, rootdir): self._nodejs_tar.provision( @@ -168,6 +183,7 @@ self.run(npm_install + ['--global'], cwd=rootdir) for target in self.options.npm_run: self.run(['npm', 'run', target], cwd=rootdir) + return self._get_installed_node_packages('npm', self.installdir) def _yarn_install(self, rootdir): self._nodejs_tar.provision( @@ -208,6 +224,33 @@ for target in self.options.npm_run: self.run(yarn_cmd + ['run', target], cwd=rootdir, env=self._build_environment(rootdir)) + return self._get_installed_node_packages('npm', self.installdir) + + def _get_installed_node_packages(self, package_manager, cwd): + try: + output = self.run_output( + [package_manager, 'ls', '--global', '--json'], cwd=cwd) + except subprocess.CalledProcessError as error: + # XXX When dependencies have missing dependencies, an error like + # this is printed to stderr: + # npm ERR! peer dep missing: glob@*, required by glob-promise@3.1.0 + # retcode is not 0, which raises an exception. + output = error.output.decode(sys.getfilesystemencoding()).strip() + packages = collections.OrderedDict() + dependencies = json.loads( + output, object_pairs_hook=collections.OrderedDict)['dependencies'] + while dependencies: + key, value = dependencies.popitem(last=False) + # XXX Just as above, dependencies without version are the ones + # missing. + if 'version' in value: + packages[key] = value['version'] + if 'dependencies' in value: + dependencies.update(value['dependencies']) + return packages + + def get_manifest(self): + return self._manifest def _build_environment(self, rootdir): env = os.environ.copy() diff -Nru snapcraft-2.34/snapcraft/plugins/plainbox_provider.py snapcraft-2.35/snapcraft/plugins/plainbox_provider.py --- snapcraft-2.34/snapcraft/plugins/plainbox_provider.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/plainbox_provider.py 2017-11-01 19:41:33.000000000 +0000 @@ -30,10 +30,9 @@ """ import os -import re import snapcraft -from snapcraft import file_utils +from snapcraft.internal import mangling class PlainboxProviderPlugin(snapcraft.BasePlugin): @@ -46,6 +45,10 @@ super().build() env = os.environ.copy() + # Ensure the first provider does not attempt to validate against + # providers installed on the build host by initialising PROVIDERPATH + # to empty + env['PROVIDERPATH'] = '' provider_stage_dir = os.path.join(self.project.stage_dir, 'providers') if os.path.exists(provider_stage_dir): provider_dirs = [os.path.join(provider_stage_dir, provider) @@ -59,10 +62,7 @@ '--prefix=/providers/{}'.format(self.name), '--root={}'.format(self.installdir)]) - # Fix all shebangs to use the in-snap python. - file_utils.replace_in_file(self.installdir, re.compile(r''), - re.compile(r'^#!.*python'), - r'#!/usr/bin/env python') + mangling.rewrite_python_shebangs(self.installdir) def snap_fileset(self): fileset = super().snap_fileset() diff -Nru snapcraft-2.34/snapcraft/plugins/_python/errors.py snapcraft-2.35/snapcraft/plugins/_python/errors.py --- snapcraft-2.34/snapcraft/plugins/_python/errors.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/_python/errors.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,65 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import snapcraft.internal.errors +import snapcraft.formatting_utils + + +class PythonPluginError(snapcraft.internal.errors.SnapcraftError): + pass + + +class MissingPythonCommandError(PythonPluginError): + + fmt = 'Unable to find {python_version}, searched: {search_paths}' + + def __init__(self, python_version, search_paths): + super().__init__( + python_version=python_version, + search_paths=snapcraft.formatting_utils.combine_paths( + search_paths, '', ':')) + + +class MissingUserSitePackagesError(PythonPluginError): + + fmt = 'Unable to find user site packages: {site_dir_glob}' + + def __init__(self, site_dir_glob): + super().__init__(site_dir_glob=site_dir_glob) + + +class MissingSitePyError(PythonPluginError): + + fmt = 'Unable to find site.py: {site_py_glob}' + + def __init__(self, site_py_glob): + super().__init__(site_py_glob=site_py_glob) + + +class PipListInvalidJsonError(PythonPluginError): + + fmt = "Pip packages output isn't valid json: {json!r}" + + def __init__(self, json): + super().__init__(json=json) + + +class PipListMissingFieldError(PythonPluginError): + + fmt = 'Pip packages json missing {field!r} field: {json!r}' + + def __init__(self, field, json): + super().__init__(field=field, json=json) diff -Nru snapcraft-2.34/snapcraft/plugins/_python/__init__.py snapcraft-2.35/snapcraft/plugins/_python/__init__.py --- snapcraft-2.34/snapcraft/plugins/_python/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/_python/__init__.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,19 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +from ._pip import Pip # noqa +from ._python_finder import get_python_command # noqa +from ._sitecustomize import generate_sitecustomize # noqa diff -Nru snapcraft-2.34/snapcraft/plugins/_python/_pip.py snapcraft-2.35/snapcraft/plugins/_python/_pip.py --- snapcraft-2.34/snapcraft/plugins/_python/_pip.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/_python/_pip.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,386 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2017 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 . + +import collections +import contextlib +import json +import logging +import os +import shutil +import stat +import subprocess +import sys +import tempfile + +import snapcraft +from snapcraft import file_utils +from ._python_finder import ( + get_python_command, + get_python_headers, + get_python_home, +) +from . import errors + +logger = logging.getLogger(__name__) + + +def _process_common_args(*, packages, constraints, + requirements, process_dependency_links): + args = [] + if constraints: + for constraint in constraints: + args.extend(['--constraint', constraint]) + + if requirements: + for requirement in requirements: + args.extend(['--requirement', requirement]) + + if process_dependency_links: + args.append('--process-dependency-links') + + if packages: + args.extend(packages) + + return args + + +def _replicate_owner_mode(path): + # Don't bother with a path that doesn't exist or is a symlink. The target + # of the symlink will either be updated anyway, or we won't have permission + # to change it. + if not os.path.exists(path) or os.path.islink(path): + return + + file_mode = os.stat(path).st_mode + new_mode = file_mode & stat.S_IWUSR + if file_mode & stat.S_IXUSR: + new_mode |= stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + if file_mode & stat.S_IRUSR: + new_mode |= stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH + os.chmod(path, new_mode) + + +def _fix_permissions(path): + for root, dirs, files in os.walk(path): + for filename in files: + _replicate_owner_mode(os.path.join(root, filename)) + for dirname in dirs: + _replicate_owner_mode(os.path.join(root, dirname)) + + +class Pip: + """Wrapper for pip abstracting the args necessary for use in a part. + + This class takes care of fetching pip, setuptools, and wheel, and then + simply shells out to pip with the magical arguments necessary to install + packages into a part. + + Of particular importance: packages must be downloaded (via download()) + before they can be installed or have wheels built. + """ + + def __init__(self, *, python_major_version, part_dir, install_dir, + stage_dir): + """Initialize pip. + + You must call setup() before you can actually use pip. + + :param str python_major_version: The python major version to find (2 or + 3) + :param str part_dir: Path to the part's working area + :param str install_dir: Path to the part's install area + :param str stage_dir: Path to the staging area + + :raises MissingPythonCommandError: If no python could be found in the + staging or part's install area. + """ + self._python_major_version = python_major_version + self._part_dir = part_dir + self._install_dir = install_dir + self._stage_dir = stage_dir + + self._python_package_dir = os.path.join( + self._part_dir, 'python-packages') + os.makedirs(self._python_package_dir, exist_ok=True) + + self._python_command = get_python_command( + self._python_major_version, stage_dir=self._stage_dir, + install_dir=self._install_dir) + self._python_home = get_python_home( + self._python_major_version, stage_dir=self._stage_dir, + install_dir=self._install_dir) + + def setup(self): + """Install pip and dependencies. + + Check to see if pip has already been installed. If not, fetch pip, + setuptools, and wheel, and install them so they can be used. + """ + # Check to see if we have our own pip, yet. If not, we need to use the + # pip on the host (installed via build-packages) to grab our own. + if not self.is_setup(): + logger.info('Fetching pip, setuptools, and wheel...') + + real_python_home = self._python_home + + # Make sure we're using pip from the host. Wrapping this operation + # in a try/finally to make sure we revert away from the host's + # python at the end. + try: + self._python_home = os.path.join(os.path.sep, 'usr') + + # Using the host's pip, install our own pip and other tools we + # need. + self.download({'pip', 'setuptools', 'wheel'}) + self.install( + {'pip', 'setuptools', 'wheel'}, ignore_installed=True) + finally: + # Now that we have our own pip, reset the python home + self._python_home = real_python_home + + def is_setup(self): + """Return true if this class has already been setup.""" + + try: + # We're expecting an error here at least once complaining about + # pip not being installed. In order to verify that the error is the + # one we think it is, we need to process the stderr. So we'll + # redirect it to stdout. If it's not the error we expect, something + # is wrong, so re-raise it. + self._run([], stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + output = e.output.decode(sys.getfilesystemencoding()).strip() + if 'no module named pip' in output.lower(): + return False + else: + raise e + + return True + + def download(self, packages, *, setup_py_dir=None, constraints=None, + requirements=None, process_dependency_links=False): + """Download packages into cache, but do not install them. + + :param iterable packages: Packages to download from index. + :param str setup_py_dir: Directory containing setup.py. + :param iterable constraints: Collection of paths to constraints files. + :param iterable requirements: Collection of paths to requirements + files. + :param boolean process_dependency_links: Enable the processing of + dependency links. + """ + args = _process_common_args( + process_dependency_links=process_dependency_links, + packages=packages, constraints=constraints, + requirements=requirements) + + cwd = None + if setup_py_dir: + args.append('.') + cwd = setup_py_dir + + if not args: + return # No operation was requested + + # Using pip with a few special parameters: + # + # --disable-pip-version-check: Don't whine if pip is out-of-date with + # the version on pypi. + # --dest: Download packages into the directory we've set aside for it. + self._run(['download', '--disable-pip-version-check', '--dest', + self._python_package_dir] + args, cwd=cwd) + + def install(self, packages, *, setup_py_dir=None, constraints=None, + requirements=None, process_dependency_links=False, + upgrade=False, install_deps=True, ignore_installed=False): + """Install packages from cache. + + The packages should have already been downloaded via `download()`. + + :param iterable packages: Packages to install from cache. + :param str setup_py_dir: Directory containing setup.py. + :param iterable constraints: Collection of paths to constraints files. + :param iterable requirements: Collection of paths to requirements + files. + :param boolean process_dependency_links: Enable the processing of + dependency links. + :param boolean upgrade: Recursively upgrade packages. + :param boolean install_deps: Install package dependencies. + :param boolean ignore_installed: Reinstall packages if they're already + installed + """ + args = _process_common_args( + process_dependency_links=process_dependency_links, + packages=packages, constraints=constraints, + requirements=requirements) + + if upgrade: + args.append('--upgrade') + + if not install_deps: + args.append('--no-deps') + + if ignore_installed: + args.append('--ignore-installed') + + cwd = None + if setup_py_dir: + args.append('.') + cwd = setup_py_dir + + if not args: + return # No operation was requested + + # Using pip with a few special parameters: + # + # --user: Install packages to PYTHONUSERBASE, which we've pointed to + # the installdir. + # --no-compile: Don't compile .pyc files. FIXME: This is legacy, and + # should be removed once this refactor has been + # validated. + # --no-index: Don't hit pypi, assume the packages are already + # downloaded (i.e. by using `self.download()`) + # --find-links: Provide the directory into which the packages should + # have already been fetched + self._run(['install', '--user', '--no-compile', '--no-index', + '--find-links', self._python_package_dir] + args, cwd=cwd) + + # Installing with --user results in a directory with 700 permissions. + # We need it a bit more open than that, so open it up. + _fix_permissions(self._install_dir) + + def wheel(self, packages, *, setup_py_dir=None, constraints=None, + requirements=None, process_dependency_links=False): + """Build wheels of packages in the cache. + + The packages should have already been downloaded via `download()`. + + :param iterable packages: Packages in cache for which to build wheels. + :param str setup_py_dir: Directory containing setup.py. + :param iterable constraints: Collection of paths to constraints files. + :param iterable requirements: Collection of paths to requirements + files. + :param boolean process_dependency_links: Enable the processing of + dependency links. + + :return: List of paths to each wheel that was built. + :rtype: list + """ + args = _process_common_args( + process_dependency_links=process_dependency_links, + packages=packages, constraints=constraints, + requirements=requirements) + + cwd = None + if setup_py_dir: + args.append('.') + cwd = setup_py_dir + + if not args: + return # No operation was requested + + wheels = [] + with tempfile.TemporaryDirectory() as temp_dir: + + # Using pip with a few special parameters: + # + # --no-index: Don't hit pypi, assume the packages are already + # downloaded (i.e. by using `self.download()`) + # --find-links: Provide the directory into which the packages + # should have already been fetched + # --wheel-dir: Build wheels into a temporary working area rather + # rather than cwd. We'll copy them over. FIXME: We can + # probably get away just building them in the package + # dir. Try that once this refactor has been validated. + self._run(['wheel', '--no-index', '--find-links', + self._python_package_dir, '--wheel-dir', + temp_dir] + args, cwd=cwd) + wheels = os.listdir(temp_dir) + for wheel in wheels: + file_utils.link_or_copy( + os.path.join(temp_dir, wheel), + os.path.join(self._python_package_dir, wheel)) + + return [os.path.join(self._python_package_dir, wheel) + for wheel in wheels] + + def list(self, *, user=False): + """Determine which packages have been installed. + + :return: Dict of installed python packages and their versions + :rtype: dict + """ + command = ['list', '--format=json'] + if user: + command.append('--user') + + output = self._run(command) + packages = collections.OrderedDict() + try: + json_output = json.loads( + output, object_pairs_hook=collections.OrderedDict) + except json.decoder.JSONDecodeError as e: + raise errors.PipListInvalidJsonError(output) from e + + for package in json_output: + if 'name' not in package: + raise errors.PipListMissingFieldError('name', output) + if 'version' not in package: + raise errors.PipListMissingFieldError('version', output) + packages[package['name']] = package['version'] + return packages + + def clean_packages(self): + """Remove the package cache.""" + with contextlib.suppress(FileNotFoundError): + shutil.rmtree(self._python_package_dir) + + def env(self): + """The environment used by pip. + + This function is only useful if you happen to need to call into pip's + environment without using the API otherwise made available here (e.g. + calling the setup.py directly instead of with pip). + + :return: Dict of the environment necessary to use the pip contained + here. + :rtype: dict + """ + env = os.environ.copy() + env['PYTHONUSERBASE'] = self._install_dir + env['PYTHONHOME'] = self._python_home + + env['PATH'] = '{}:{}'.format( + os.path.join(self._install_dir, 'usr', 'bin'), + os.path.expandvars('$PATH')) + + headers = get_python_headers( + self._python_major_version, stage_dir=self._stage_dir) + if headers: + current_cppflags = env.get('CPPFLAGS', '') + env['CPPFLAGS'] = '-I{}'.format(headers) + if current_cppflags: + env['CPPFLAGS'] = '{} {}'.format( + env['CPPFLAGS'], current_cppflags) + + return env + + def _run(self, args, **kwargs): + env = self.env() + + return snapcraft.internal.common.run_output( + [self._python_command, '-m', 'pip'] + list(args), env=env, + **kwargs) diff -Nru snapcraft-2.34/snapcraft/plugins/_python/_python_finder.py snapcraft-2.35/snapcraft/plugins/_python/_python_finder.py --- snapcraft-2.34/snapcraft/plugins/_python/_python_finder.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/_python/_python_finder.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,103 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os +import glob + +from . import errors + + +def get_python_command(python_major_version, *, stage_dir, install_dir): + """Find the python command to use, preferring staged over the part. + + We prefer the staged python as opposed to the in-part python in order to + support one part that supplies python, with another part built `after` it + wanting to use its python. + + :param str python_major_version: The python major version to find (2 or 3) + :param str stage_dir: Path to the staging area + :param str install_dir: Path to the part's install area + + :return: Path to the python command that was found + :rtype: str + + :raises MissingPythonCommandError: If no python could be found in the + staging or part's install area. + """ + python_command_name = 'python{}'.format(python_major_version) + python_command = os.path.join('usr', 'bin', python_command_name) + staged_python = os.path.join(stage_dir, python_command) + part_python = os.path.join(install_dir, python_command) + + if os.path.exists(staged_python): + return staged_python + elif os.path.exists(part_python): + return part_python + else: + raise errors.MissingPythonCommandError( + python_command_name, [stage_dir, install_dir]) + + +def get_python_headers(python_major_version, *, stage_dir): + """Find the python headers to use, if any, preferring staged over the host. + + We want to make sure we use the headers from the staging area if available, + or we may end up building for an older python version than the one we + actually want to use. + + :param str python_major_version: The python version to find (2 or 3) + :param str stage_dir: Path to the staging area + + :return: Path to the python headers that were found ('' if none) + :rtype: str + """ + python_command_name = 'python{}'.format(python_major_version) + base_match = os.path.join('usr', 'include', '{}*'.format( + python_command_name)) + staged_python = glob.glob(os.path.join(stage_dir, base_match)) + host_python = glob.glob(os.path.join(os.path.sep, base_match)) + + if staged_python: + return staged_python[0] + elif host_python: + return host_python[0] + else: + return '' + + +def get_python_home(python_major_version, *, stage_dir, install_dir): + """Find the correct PYTHONHOME, preferring staged over the part. + + We prefer the staged python as opposed to the in-part python in order to + support one part that supplies python, with another part built `after` it + wanting to use its python. + + :param str python_major_version: The python major version to find (2 or 3) + :param str stage_dir: Path to the staging area + :param str install_dir: Path to the part's install area + + :return: Path to the PYTHONHOME that was found + :rtype: str + + :raises MissingPythonCommandError: If no python could be found in the + staging or part's install area. + """ + python_command = get_python_command( + python_major_version, stage_dir=stage_dir, install_dir=install_dir) + if python_command.startswith(stage_dir): + return os.path.join(stage_dir, 'usr') + else: + return os.path.join(install_dir, 'usr') diff -Nru snapcraft-2.34/snapcraft/plugins/_python/_sitecustomize.py snapcraft-2.35/snapcraft/plugins/_python/_sitecustomize.py --- snapcraft-2.34/snapcraft/plugins/_python/_sitecustomize.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/_python/_sitecustomize.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,100 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import contextlib +import glob +import os +from textwrap import dedent + +from ._python_finder import get_python_command +from . import errors + +_SITECUSTOMIZE_TEMPLATE = dedent("""\ + import site + import os + + snap_dir = os.getenv("SNAP") + snapcraft_stage_dir = os.getenv("SNAPCRAFT_STAGE") + snapcraft_part_install = os.getenv("SNAPCRAFT_PART_INSTALL") + + for d in (snap_dir, snapcraft_stage_dir, snapcraft_part_install): + if d: + site_dir = os.path.join(d, "{site_dir}") + site.addsitedir(site_dir) + + if snap_dir: + site.ENABLE_USER_SITE = False""") + + +def _get_user_site_dir(python_major_version, *, install_dir): + path_glob = os.path.join( + install_dir, 'lib', 'python{}*'.format(python_major_version), + 'site-packages') + user_site_dirs = glob.glob(path_glob) + if not user_site_dirs: + raise errors.MissingUserSitePackagesError(path_glob) + + return user_site_dirs[0][len(install_dir)+1:] + + +def _get_sitecustomize_path(python_major_version, *, stage_dir, install_dir): + # Use the part's install_dir unless there's a python in the staging area + base_dir = install_dir + with contextlib.suppress(errors.MissingPythonCommandError): + python_command = get_python_command( + python_major_version, stage_dir=stage_dir, install_dir=install_dir) + if python_command.startswith(stage_dir): + base_dir = stage_dir + + site_py_glob = os.path.join( + base_dir, 'usr', 'lib', 'python{}*'.format(python_major_version), + 'site.py') + python_sites = glob.glob(site_py_glob) + if not python_sites: + raise errors.MissingSitePyError(site_py_glob) + + python_site_dir = os.path.dirname(python_sites[0]) + + return os.path.join( + install_dir, python_site_dir[len(base_dir)+1:], 'sitecustomize.py') + + +def generate_sitecustomize(python_major_version, *, stage_dir, install_dir): + """Generate a sitecustomize.py to look in staging, part install, and snap. + + This is done by checking the values of the environment variables $SNAP, + $SNAPCRAFT_STAGE, and $SNAPCRAFT_PART_INSTALL. As a result, the same + sitecustomize.py works to find packages at both build- and run-time. + + :param str python_major_version: The python major version to use (2 or 3) + :param str stage_dir: Path to the staging area + :param str install_dir: Path to the part's install area + + :raises MissingUserSitePackagesError: If no user site packages are found in + install_dir/lib/pythonX* + :raises MissingSitePyError: If no site.py can be found in either the + staging area or the part install area. + """ + sitecustomize_path = _get_sitecustomize_path( + python_major_version, stage_dir=stage_dir, install_dir=install_dir) + os.makedirs(os.path.dirname(sitecustomize_path), exist_ok=True) + + # Create our sitecustomize. Python from the archives already has one + # which is distro-specific and not needed here, so we truncate it if it's + # already there. + with open(sitecustomize_path, 'w') as f: + f.write(_SITECUSTOMIZE_TEMPLATE.format(site_dir=_get_user_site_dir( + python_major_version, install_dir=install_dir))) diff -Nru snapcraft-2.34/snapcraft/plugins/python.py snapcraft-2.35/snapcraft/plugins/python.py --- snapcraft-2.34/snapcraft/plugins/python.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/python.py 2017-11-01 19:41:33.000000000 +0000 @@ -55,7 +55,6 @@ import collections import json import os -import re import shutil import stat import subprocess @@ -70,6 +69,7 @@ import snapcraft from snapcraft import file_utils from snapcraft.common import isurl +from snapcraft.internal import mangling _SITECUSTOMIZE_TEMPLATE = dedent("""\ @@ -368,9 +368,7 @@ self._fix_permissions() # Fix all shebangs to use the in-snap python. - file_utils.replace_in_file(self.installdir, re.compile(r''), - re.compile(r'^#!.*python'), - r'#!/usr/bin/env python') + mangling.rewrite_python_shebangs(self.installdir) self._setup_sitecustomize() diff -Nru snapcraft-2.34/snapcraft/plugins/_ros/__init__.py snapcraft-2.35/snapcraft/plugins/_ros/__init__.py --- snapcraft-2.34/snapcraft/plugins/_ros/__init__.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/_ros/__init__.py 2017-11-01 19:41:33.000000000 +0000 @@ -15,3 +15,4 @@ # along with this program. If not, see . from snapcraft.plugins._ros import rosdep # noqa +from snapcraft.plugins._ros import ros2 # noqa diff -Nru snapcraft-2.34/snapcraft/plugins/_ros/ros2.py snapcraft-2.35/snapcraft/plugins/_ros/ros2.py --- snapcraft-2.34/snapcraft/plugins/_ros/ros2.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/_ros/ros2.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,178 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import contextlib +import os +import logging +import shutil +import subprocess + +from snapcraft import ( + repo, + sources, +) + +logger = logging.getLogger(__name__) + +_ROS2_URL_TEMPLATE = ( + 'https://raw.githubusercontent.com/ros2/ros2/{version}/ros2.repos' +) + + +_INSTALL_TOOLS_STEP = 'install-tools' +_FETCH_ROS2_STEP = 'fetch-ros2' +_BUILD_ROS2_STEP = 'build-ros2' + + +class Bootstrapper: + """Bootstrap ROS2 by building the underlay from source.""" + + def __init__(self, *, version, bootstrap_path, ubuntu_sources, project): + """Initialize bootstrapper. + + :param str version: The ROS2 version to boostrap (e.g. release-beta3) + :param str bootstrap_path: Working directory for the bootstrap process + :param str ubuntu_sources: Ubuntu repositories from which dependencies + will be fetched. + :param project: Instance of ProjectOptions for project-wide settings + :type project: snapcraft.ProjectOptions + """ + self._version = version + self._ubuntu_sources = ubuntu_sources + self._project = project + + self._bootstrap_path = bootstrap_path + self._tool_dir = os.path.join(self._bootstrap_path, 'tools') + self._state_dir = os.path.join(self._bootstrap_path, 'state') + self._underlay_dir = os.path.join(self._bootstrap_path, 'underlay') + self._install_dir = os.path.join(self._bootstrap_path, 'install') + self._build_dir = os.path.join(self._bootstrap_path, 'build') + self._source_dir = os.path.join(self._underlay_dir, 'src') + + def get_build_packages(self): + """Return the packages required for building the underlay.""" + + return [ + # Dependencies for FastRTPS + 'libasio-dev', 'libtinyxml2-dev', + + # Dependencies for the rest of ros2 + 'cmake', 'libopencv-dev', 'libpoco-dev', 'libpocofoundation9v5', + 'libpocofoundation9v5-dbg', 'python3-dev', 'python3-empy', + 'python3-nose', 'python3-pip', 'python3-setuptools', + 'python3-yaml', 'libtinyxml-dev', 'libeigen3-dev' + ] + + def get_stage_packages(self): + """Return the packages required for running the underlay.""" + + # Ament is a python3 tool, and it requires setuptools at runtime + return ['python3', 'python3-setuptools'] + + def pull(self): + """Download the ROS2 underlay. + + This first downloads the tools required to fetch the underlay, and then + uses those tools to fetch the underlay. Both steps are only run once, + even if subsequent steps fail. + """ + self._run_step( + self._install_tools, + step=_INSTALL_TOOLS_STEP, + skip_message='Tools already installed. Skipping...') + self._run_step( + self._fetch_ros2, + step=_FETCH_ROS2_STEP, + skip_message='ros2 already fetched. Skipping...') + + def build(self): + """Build the ROS2 underlay. + + Build the ROS2 underlay that was fetched in pull(). This is only done + once, assuming success, which means calling this function multiple + times does not necessarily build multiple times. + + :return: The path into which the underlay is installed. + :rtype: str + """ + self._run_step( + self._build_ros2, + step=_BUILD_ROS2_STEP, + skip_message='ros2 already built. Skipping...') + + return self._install_dir + + def clean(self): + """Clean everything done in this class.""" + + with contextlib.suppress(FileNotFoundError): + shutil.rmtree(self._bootstrap_path) + + def _run(self, command): + env = os.environ.copy() + env['PATH'] = env['PATH'] + ':' + os.path.join( + self._tool_dir, 'usr', 'bin') + env['PYTHONPATH'] = os.path.join( + self._tool_dir, 'usr', 'lib', 'python3', 'dist-packages') + + subprocess.check_call(command, env=env) + + def _is_step_done(self, step): + return os.path.isfile(os.path.join(self._state_dir, step)) + + def _set_step_done(self, step): + os.makedirs(self._state_dir, exist_ok=True) + open(os.path.join(self._state_dir, step), 'w').close() + + def _run_step(self, callable, *, step, skip_message=None): + if self._is_step_done(step): + if skip_message: + logger.debug(skip_message) + else: + callable() + self._set_step_done(step) + + def _install_tools(self): + logger.info('Preparing to fetch vcstool...') + ubuntu = repo.Ubuntu( + self._bootstrap_path, sources=self._ubuntu_sources, + project_options=self._project) + + logger.info('Fetching vcstool...') + ubuntu.get(['python3-vcstool']) + + logger.info('Installing vcstool...') + ubuntu.unpack(self._tool_dir) + + def _fetch_ros2(self): + os.makedirs(self._source_dir, exist_ok=True) + sources.Script( + _ROS2_URL_TEMPLATE.format(version=self._version), + self._underlay_dir).download() + + logger.info('Fetching ros2 sources....') + ros2_repos = os.path.join(self._underlay_dir, 'ros2.repos') + self._run( + ['vcs', 'import', '--input', ros2_repos, self._source_dir]) + + def _build_ros2(self): + logger.info('Building ros2 underlay...') + ament_path = os.path.join( + self._source_dir, 'ament', 'ament_tools', 'scripts', + 'ament.py') + self._run( + [ament_path, 'build', self._source_dir, '--build-space', + self._build_dir, '--install-space', self._install_dir]) diff -Nru snapcraft-2.34/snapcraft/plugins/ruby.py snapcraft-2.35/snapcraft/plugins/ruby.py --- snapcraft-2.34/snapcraft/plugins/ruby.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/ruby.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,202 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 James Beedy +# +# 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 . + +"""The ruby plugin is useful for ruby based parts. +The plugin uses gem to install dependencies from a `Gemfile`. +This plugin uses the common plugin keywords as well as those for "sources". +For more information check the 'plugins' topic for the former and the +'sources' topic for the latter. +Additionally, this plugin uses the following plugin-specific keywords: + - gems: + (list) + A list of gems to install. + - use-bundler + (boolean) + Use bundler to install gems from a Gemfile (defaults 'false'). + - ruby-version: + (string) + The version of ruby you want this snap to run. +""" + +import glob +import logging +import os +import re + +from snapcraft import BasePlugin, file_utils +from snapcraft.internal.errors import SnapcraftEnvironmentError +from snapcraft.sources import Tar + + +logger = logging.getLogger(__name__) + + +class RubyPlugin(BasePlugin): + + @classmethod + def schema(cls): + schema = super().schema() + + schema['properties']['use-bundler'] = { + 'type': 'boolean', + 'default': False + } + schema['properties']['ruby-version'] = { + 'type': 'string', + 'default': '2.4.2' + } + schema['properties']['gems'] = { + 'type': 'array', + 'minitems': 1, + 'uniqueItems': True, + 'items': { + 'type': 'string' + }, + 'default': [] + } + return schema + + @classmethod + def get_pull_properties(cls): + # Inform Snapcraft of the properties associated with pulling. If these + # change in the YAML Snapcraft will consider the build step dirty. + return ['ruby-version', 'gems', 'use-bundler'] + + def __init__(self, name, options, project): + super().__init__(name, options, project) + # Beta Warning + # Remove this comment and warning once ruby plugin is stable. + logger.warn("The ruby plugin is currently in beta, " + "its API may break. Use at your own risk") + + self._ruby_version = options.ruby_version + self._ruby_part_dir = os.path.join(self.partdir, 'ruby') + self._ruby_download_url = \ + 'https://cache.ruby-lang.org/pub/ruby/ruby-{}.tar.gz'.format( + self._ruby_version) + self._ruby_tar = Tar(self._ruby_download_url, self._ruby_part_dir) + self._gems = options.gems or [] + + self.build_packages.extend(['gcc', 'g++', 'make', 'zlib1g-dev', + 'libssl-dev', 'libreadline-dev']) + + def snap_fileset(self): + fileset = super().snap_fileset() + fileset.append('-include/') + fileset.append('-share/') + return fileset + + def pull(self): + super().pull() + os.makedirs(self._ruby_part_dir, exist_ok=True) + + logger.info('Fetching ruby {}...'.format(self._ruby_version)) + self._ruby_tar.download() + + logger.info('Building/installing ruby...') + self._ruby_install(builddir=self._ruby_part_dir) + + self._gem_install() + if self.options.use_bundler: + self._bundle_install() + + def env(self, root): + env = super().env(root) + + for key, value in self._env_dict(root).items(): + env.append('{}="{}"'.format(key, value)) + + return env + + def _env_dict(self, root): + env = dict() + rubydir = os.path.join(root, 'lib', 'ruby') + + # Patch versions of ruby continue to use the minor version's RUBYLIB, + # GEM_HOME, and GEM_PATH. Fortunately there should just be one, so we + # can detect it by globbing instead of trying to determine what the + # minor version is programatically + versions = glob.glob(os.path.join(rubydir, 'gems', '*')) + + # Before Ruby has been pulled/installed, no versions will be found. + # If that's the case, we won't define any Ruby-specific variables yet + if len(versions) == 1: + ruby_version = os.path.basename(versions[0]) + + rubylib = os.path.join(rubydir, ruby_version) + + # Ruby uses some pretty convoluted rules for determining its + # arch-specific RUBYLIB. Rather than try and duplicate that logic + # here, let's just look for a file that we know is in there: + # rbconfig.rb. There should only be one. + paths = glob.glob(os.path.join(rubylib, '*', 'rbconfig.rb')) + if len(paths) != 1: + raise SnapcraftEnvironmentError( + 'Expected a single rbconfig.rb, but found {}'.format( + len(paths))) + + env['RUBYLIB'] = '{}:{}'.format(rubylib, os.path.dirname(paths[0])) + env['GEM_HOME'] = os.path.join(rubydir, 'gems', ruby_version) + env['GEM_PATH'] = os.path.join(rubydir, 'gems', ruby_version) + elif len(versions) > 1: + raise SnapcraftEnvironmentError( + 'Expected a single Ruby version, but found {}'.format( + len(versions))) + + return env + + def _run(self, command, **kwargs): + """Regenerate the build environment, then run requested command. + + Without this function, the build environment would not be regenerated + and thus the newly installed Ruby would not be discovered. + """ + + env = os.environ.copy() + env.update(self._env_dict(self.installdir)) + self.run(command, env=env, **kwargs) + + def _ruby_install(self, builddir): + self._ruby_tar.provision( + builddir, clean_target=False, keep_tarball=True) + self._run(['./configure', '--disable-install-rdoc', '--prefix=/'], + cwd=builddir) + self._run(['make', '-j{}'.format(self.parallel_build_count)], + cwd=builddir) + self._run(['make', 'install', 'DESTDIR={}'.format(self.installdir)], + cwd=builddir) + # Fix all shebangs to use the in-snap ruby + file_utils.replace_in_file( + self.installdir, + re.compile(r''), + re.compile(r'^#!.*ruby'), + r'#!/usr/bin/env ruby') + + def _gem_install(self): + if self.options.use_bundler: + self._gems = self._gems + ['bundler'] + if self._gems: + logger.info('Installing gems...') + gem_install_cmd = [os.path.join(self.installdir, 'bin', 'ruby'), + os.path.join(self.installdir, 'bin', 'gem'), + 'install', '--env-shebang'] + self._run(gem_install_cmd + self._gems) + + def _bundle_install(self): + bundle_install_cmd = [os.path.join(self.installdir, 'bin', 'ruby'), + os.path.join(self.installdir, 'bin', 'bundle'), + 'install'] + self._run(bundle_install_cmd) diff -Nru snapcraft-2.34/snapcraft/plugins/rust.py snapcraft-2.35/snapcraft/plugins/rust.py --- snapcraft-2.34/snapcraft/plugins/rust.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/plugins/rust.py 2017-11-01 19:41:33.000000000 +0000 @@ -1,4 +1,7 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# # Copyright (C) 2016-2017 Marius Gripsgard (mariogrip@ubuntu.com) +# Copyright (C) 2016-2017 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 @@ -33,9 +36,10 @@ Features used to build optional dependencies """ -from contextlib import suppress +import collections import os import shutil +from contextlib import suppress import snapcraft from snapcraft import sources @@ -91,6 +95,7 @@ self._rustlib = os.path.join(self._rustpath, "lib") self._rustup_get = sources.Script(_RUSTUP, self._rustpath) self._rustup = os.path.join(self._rustpath, "rustup.sh") + self._manifest = collections.OrderedDict() def build(self): super().build() @@ -105,6 +110,7 @@ cmd.append("--features") cmd.append(' '.join(self.options.rust_features)) self.run(cmd, env=self._build_env()) + self._record_manifest() def _write_cross_compile_config(self): if not self.project.is_cross_compiling: @@ -122,6 +128,20 @@ '''.format(self._target, self._target, '{}-gcc'.format(self.project.arch_triplet))) + def _record_manifest(self): + self._manifest['rustup-version'] = self.run_output( + [self._rustup, '--version']) + self._manifest['rustc-version'] = self.run_output( + [self._rustc, '--version']) + self._manifest['cargo-version'] = self.run_output( + [self._cargo, '--version']) + with suppress(FileNotFoundError, IsADirectoryError): + with open(os.path.join(self.builddir, 'Cargo.lock')) as lock_file: + self._manifest['cargo-lock-contents'] = lock_file.read() + + def get_manifest(self): + return self._manifest + def enable_cross_compilation(self): # Cf. rustc --print target-list targets = { diff -Nru snapcraft-2.34/snapcraft/storeapi/assertions.py snapcraft-2.35/snapcraft/storeapi/assertions.py --- snapcraft-2.34/snapcraft/storeapi/assertions.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/storeapi/assertions.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,261 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . +import json +import subprocess +from copy import deepcopy +from datetime import datetime + +from . import StoreClient +from . import errors +from . import constants + + +class _BaseAssertion: + """Private Base class to handle assertions. + + Implementations are supposed to define a class attribute to determine + the assertion type. + + :cvar str _assertion_type: the assertion type, also treated as the endpoint + for the assertion on the store and the payload + header key for the returned data. + """ + + @property + def publisher_id(self): + """Return the publisher-id of a snap. + This entry is also known as accound-id or developer-id. + + The value is lazily fetched from the store. + """ + if not self._account_info: + self._account_info = self._store_client.get_account_information() + return self._account_info['account_id'] + + @property + def snap_id(self): + """Return the snap-id of the given snap_name. + + The value is lazily fetched from the store. + """ + if not self._account_info: + self._account_info = self._store_client.get_account_information() + snaps = self._account_info['snaps'][self._release] + try: + return snaps[self._snap_name]['snap-id'] + except KeyError: + raise errors.SnapNotFoundError(self._snap_name) + + def __init__(self, *, snap_name, signing_key=None): + """Create an instance to handle an assertion. + + :param str snap_name: snap name to handle assertion for. + :param str signing_key: the name of the key to use, if not + provided, the default key is used. + :ivar dict assertion: holds the actual assertion. + :ivar dict signed_assertion: holds a signed version of assertion. + """ + self._store_client = StoreClient() + self._snap_name = snap_name + self._signing_key = signing_key + self._release = constants.DEFAULT_SERIES + self._account_info = None + self.assertion = None + self.signed_assertion = None + + def get(self): + """Get the current assertion from the store. + + :returns: the assertion corresponding to the snap_name. + :rtype: dict + """ + # The store adds a header key which is not consistent with the endpoint + # which we need to pop as it is not understood by snap sign. + if self.assertion: + return self.assertion + store_assertion = self._store_client.get_assertion( + self.snap_id, self._assertion_type) + self.assertion = list(store_assertion.values())[0] + return self.assertion + + def sign(self): + """Create a signed version of the obtained assertion. + + :returns: signed assertion document. + """ + cmdline = ['snap', 'sign'] + if self._signing_key: + cmdline += ['-k', self._signing_key] + snap_sign = subprocess.Popen( + cmdline, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + data = json.dumps(self.assertion).encode('utf8') + assertion, err = snap_sign.communicate(input=data) + if snap_sign.returncode != 0: + err = err.decode('ascii', errors='replace') + raise errors.StoreAssertionError( + snap_name=self._snap_name, + endpoint=self._assertion_type, error=err) + + self.signed_assertion = assertion + return assertion + + def push(self, force=False): + """Push the assertion to the store, signing if necessary. + + :param bool force: if True, ignore any conflict with revoked developers + and the snap revisions it would invalidate. + :returns: None + """ + if not self.signed_assertion: + self.sign() + self._store_client.push_assertion( + self.snap_id, self.signed_assertion, self._assertion_type, + force=force) + + +class DeveloperAssertion(_BaseAssertion): + """Implementation of a developer assertion. + The assertion is used to enable collaboration for a given snap_name + by updating a snap-id's developer assertion using the store endpoint. + + The assertion content has the following structure + { + 'type': 'snap-developer', + 'authority-id': '', + 'publisher-id': '', + 'snap-id': '', + 'developers': [{ + 'developer-id': '', + 'since': '2017-02-10T08:35:00.390258Z' + },{ + 'developer-id': '', + 'since': '2017-02-10T08:35:00.390258Z', + 'until': '2018-02-10T08:35:00.390258Z' + }], + } + + """ + + _assertion_type = 'developers' + + def new_assertion(self, *, developers): + """Create a new assertion with developers based out of the current one. + + The new assertion has its assertion's authority-id normalized to the + assertion's publisher_id and the assertion's revision increased by 1. + + :param list developers: a list of a dictionary developers holding the + keys: developer_id (mandatory), since + (mandatory) and until (optional). + :returns: a new assertion based out of the current assertion with the + provided developers list. + :rtype: DeveloperAssertion + """ + new_assertion = deepcopy(self.assertion) + new_assertion['developers'] = developers + + # The revision should be incremented, to avoid `invalid-revision` + # errors. + new_assertion['revision'] = str(int( + self.assertion.get('revision', '-1'))) + + # There is a possibility that the `authority-id` to be `canonical`, + # which should be changed to the `publisher_id` to match the signing + # key. + new_assertion['authority-id'] = self.publisher_id + + new_instance = DeveloperAssertion(snap_name=self._snap_name, + signing_key=self._signing_key) + # Reference the already fetched information + new_instance._account_info = self._account_info + new_instance.assertion = new_assertion + return new_instance + + def get(self): + """Return a dict containing the developer assertion for snap_name. + + The data that comes from the store query looks as follows: + {'snap_developer': { + 'type': 'snap-developer', + 'authority-id': , + 'publisher-id': , + 'snap-id': 'snap_id', + 'developers': [{ + 'developer-id': 'account_id of dev-1', + 'since': '2017-02-10T08:35:00.390258Z' + },{ + 'developer-id': 'account_id of dev-2', + 'since': '2017-02-10T08:35:00.390258Z', + 'until': '2018-02-10T08:35:00.390258Z' + }], + } + } + + The assertion is saved without the snap_developer header. + + :returns: the latest developer assertion corresponding to snap_name. + :rtype: dict + """ + try: + self.assertion = super().get() + except errors.StoreValidationError as e: + if e.error_list[0]['code'] != 'snap-developer-not-found': + raise + self.assertion = { + 'type': 'snap-developer', + 'authority-id': self.publisher_id, + 'publisher-id': self.publisher_id, + 'snap-id': self.snap_id, + 'developers': [], + } + # A safeguard to operate easily on the assertion + if 'developers' not in self.assertion: + self.assertion['developers'] = [] + return self.assertion + + def get_developers(self): + """Return a copy of the current developers listed in the assertion. + + :returns: a list of developers. + :rtype: list + """ + return self.get()['developers'].copy() + + def is_equal(self, other_assertion): + """Determine equality of developer lists in each assertion. + + During the comparison, differences in milliseconds are not considered. + + :returns: Return True if the list of developers in this instances + assertion list is equal to the list from other_assertion. + :rtype: bool + """ + this_devs = self._normalize_time( + self.assertion['developers'].copy()) + other_devs = self._normalize_time( + other_assertion.assertion['developers'].copy()) + return this_devs == other_devs + + def _normalize_time(self, developers): + for dev in developers: + for range_ in ['since', 'until']: + if range_ in dev: + date = datetime.strptime(dev[range_], + '%Y-%m-%dT%H:%M:%S.%fZ') + dev[range_] = date.strftime('%Y-%m-%dT%H:%M:%S.000000Z') + return developers diff -Nru snapcraft-2.34/snapcraft/storeapi/constants.py snapcraft-2.35/snapcraft/storeapi/constants.py --- snapcraft-2.34/snapcraft/storeapi/constants.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/storeapi/constants.py 2017-11-01 19:41:33.000000000 +0000 @@ -22,11 +22,11 @@ SCAN_STATUS_POLL_DELAY = 5 SCAN_STATUS_POLL_RETRIES = 5 UBUNTU_SSO_API_ROOT_URL = 'https://login.ubuntu.com/api/v2/' -UBUNTU_STORE_API_ROOT_URL = 'https://myapps.developer.ubuntu.com/dev/api/' -UBUNTU_STORE_SEARCH_ROOT_URL = 'https://search.apps.ubuntu.com/' +UBUNTU_STORE_API_ROOT_URL = 'https://dashboard.snapcraft.io/dev/api/' +UBUNTU_STORE_SEARCH_ROOT_URL = 'https://api.snapcraft.io/' UBUNTU_STORE_UPLOAD_ROOT_URL = 'https://upload.apps.ubuntu.com/' -UBUNTU_STORE_TOS_URL = 'https://myapps.developer.ubuntu.com/dev/tos/' -UBUNTU_STORE_ACCOUNT_URL = 'https://myapps.developer.ubuntu.com/dev/account/' +UBUNTU_STORE_TOS_URL = 'https://dashboard.snapcraft.io/dev/tos/' +UBUNTU_STORE_ACCOUNT_URL = 'https://dashboard.snapcraft.io/dev/account/' # Messages and warnings. MISSING_AGREEMENT = 'Developer has not signed agreement.' diff -Nru snapcraft-2.34/snapcraft/storeapi/errors.py snapcraft-2.35/snapcraft/storeapi/errors.py --- snapcraft-2.34/snapcraft/storeapi/errors.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/storeapi/errors.py 2017-11-01 19:41:33.000000000 +0000 @@ -29,7 +29,8 @@ class InvalidCredentialsError(StoreError): - fmt = 'Invalid credentials: {message}.' + fmt = ('Invalid credentials: {message}. ' + 'Have you run "snapcraft login"?') def __init__(self, message): super().__init__(message=message) @@ -311,26 +312,71 @@ 'Sorry, try `snapcraft register {snap_name}` before trying to ' 'release or choose an existing revision.') - fmt = 'Received {status_code!r}: {text!r}' + __FMT_BAD_REQUEST = ( + '{code}: {message}\n') + + __FMT_UNAUTHORIZED_OR_FORBIDDEN = ( + 'Received {status_code!r}: {text!r}') def __init__(self, snap_name, response): + self.fmt_errors = { + 400: self.__fmt_error_400, + 401: self.__fmt_error_401_or_403, + 403: self.__fmt_error_401_or_403, + 404: self.__fmt_error_404, + } + + fmt_error = self.fmt_errors.get( + response.status_code, self.__fmt_error_unknown) + + self.fmt = fmt_error(response) + + super().__init__(snap_name=snap_name) + + def __to_json(self, response): try: response_json = response.json() except (AttributeError, JSONDecodeError): response_json = {} - if response.status_code == 404: - self.fmt = self.__FMT_NOT_REGISTERED - elif response.status_code == 401 or response.status_code == 403: - try: - response_json['text'] = response.text - except AttributeError: - response_json['text'] = 'error while releasing' - elif 'errors' in response_json: - self.fmt = '{errors}' + return response_json - super().__init__(snap_name=snap_name, status_code=response.status_code, - **response_json) + def __fmt_error_400(self, response): + response_json = self.__to_json(response) + + try: + fmt = '' + for error in response_json['error_list']: + fmt += self.__FMT_BAD_REQUEST.format(**error) + + except (AttributeError, KeyError): + fmt = self.__fmt_error_unknown(response) + + return fmt + + def __fmt_error_401_or_403(self, response): + try: + text = response.text + + except AttributeError: + text = 'error while releasing' + + return self.__FMT_UNAUTHORIZED_OR_FORBIDDEN.format( + status_code=response.status_code, text=text) + + def __fmt_error_404(self, response): + return self.__FMT_NOT_REGISTERED + + def __fmt_error_unknown(self, response): + response_json = self.__to_json(response) + + try: + fmt = '{errors}'.format(**response_json) + + except AttributeError: + fmt = '{}'.format(response) + + return fmt class StoreValidationError(StoreError): @@ -340,7 +386,9 @@ def __init__(self, snap_id, response, message=None): try: response_json = response.json() - response_json['text'] = response.json()['error_list'][0]['message'] + error = response.json()['error_list'][0] + response_json['text'] = error.get('message') + response_json['extra'] = error.get('extra') except (AttributeError, JSONDecodeError): response_json = {'text': message or response} diff -Nru snapcraft-2.34/snapcraft/storeapi/__init__.py snapcraft-2.35/snapcraft/storeapi/__init__.py --- snapcraft-2.34/snapcraft/storeapi/__init__.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/storeapi/__init__.py 2017-11-01 19:41:33.000000000 +0000 @@ -1,4 +1,3 @@ - # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # # Copyright (C) 2016-2017 Canonical Ltd @@ -41,12 +40,10 @@ import snapcraft from snapcraft import config from snapcraft.internal.indicators import download_requests_stream -from snapcraft.storeapi import ( - _agent, - _upload, - constants, - errors, -) +from . import _agent +from . import _upload +from . import constants +from . import errors logger = logging.getLogger(__name__) @@ -358,8 +355,8 @@ file_sum.update(file_chunk) return expected_sha512 == file_sum.hexdigest() - def push_assertion(self, snap_id, assertion, endpoint): - return self.sca.push_assertion(snap_id, assertion, endpoint) + def push_assertion(self, snap_id, assertion, endpoint, force=False): + return self.sca.push_assertion(snap_id, assertion, endpoint, force) def get_assertion(self, snap_id, endpoint): return self.sca.get_assertion(snap_id, endpoint) @@ -633,7 +630,7 @@ return response_json - def push_assertion(self, snap_id, assertion, endpoint): + def push_assertion(self, snap_id, assertion, endpoint, force): if endpoint == 'validations': data = { 'assertion': assertion.decode('utf-8'), @@ -643,11 +640,20 @@ 'snap_developer': assertion.decode('utf-8'), } auth = _macaroon_auth(self.conf) + + url = 'snaps/{}/{}'.format(snap_id, endpoint) + + # For `snap-developer`, revoking developers will require their uploads + # to be invalidated. + if force: + url = url + '?ignore_revoked_uploads' + response = self.put( - 'snaps/{}/{}'.format(snap_id, endpoint), data=json.dumps(data), + url, data=json.dumps(data), headers={'Authorization': auth, 'Content-Type': 'application/json', 'Accept': 'application/json'}) + if not response.ok: raise errors.StoreValidationError(snap_id, response) try: diff -Nru snapcraft-2.34/snapcraft/_store.py snapcraft-2.35/snapcraft/_store.py --- snapcraft-2.34/snapcraft/_store.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/_store.py 2017-11-01 19:41:33.000000000 +0000 @@ -25,7 +25,6 @@ import subprocess import tempfile from datetime import datetime -from textwrap import dedent from subprocess import Popen import yaml @@ -118,7 +117,9 @@ def _login(store, packages=None, acls=None, channels=None, save=True): - print('Enter your Ubuntu One SSO credentials.') + print('Enter your Ubuntu One e-mail address and password.\n' + 'If you do not have an Ubuntu One account, you can create one at ' + 'https://dashboard.snapcraft.io/openid/login') email = input('Email: ') password = getpass.getpass('Password: ') @@ -756,143 +757,6 @@ validation_re = re.compile('^[^=]+=[0-9]+$') -def collaborate(snap_name, key): - store = storeapi.StoreClient() - - with _requires_login(): - account_info = store.get_account_information() - publisher_id = account_info['account_id'] - - release = storeapi.constants.DEFAULT_SERIES - try: - snap_id = account_info['snaps'][release][snap_name]['snap-id'] - except KeyError: - raise storeapi.errors.SnapNotFoundError(snap_name) - assertion = _get_developers(snap_id, publisher_id) - # The data will look like: - # {'snap_developer': { - # 'type': 'snap-developer', - # 'authority-id': , - # 'publisher-id': , - # 'snap-id': 'snap_id', - # 'developers': [{ - # 'developer-id': 'account_id of dev-1', - # 'since': '2017-02-10T08:35:00.390258Z' - # },{ - # 'developer-id': 'account_id of dev-2', - # 'since': '2017-02-10T08:35:00.390258Z', - # 'until': '2018-02-10T08:35:00.390258Z' - # }], - # } - # } - developers = _edit_collaborators(assertion.get('developers', [])) - if _are_developers_unchanged(developers, assertion.get('developers', [])): - logger.warning('Aborting due to unchanged collaborators list.') - return - assertion['developers'] = developers - - # The revision should be incremented, to avoid `invalid-revision` errors. - assertion['revision'] = str(int(assertion.get('revision', '0'))+1) - - # There is a possibility that the `authority-id` to be `canonical`, - # which should be changed to the `publisher_id` to match the signing key. - assertion['authority-id'] = publisher_id - - signed_assertion = _sign_assertion(snap_name, assertion, key, 'developers') - store.push_assertion(snap_id, signed_assertion, 'developers') - - -_COLLABORATION_HEADER = dedent("""\ - # Change which developers may push or release snaps on the publisher's behalf. - # - # Sample entry: - # - # developers: - # - developer-id: "dev-one" # Which developer - # since: "2017-02-10 08:35:00" # When contributions started - # until: "2018-02-10 08:35:00" # When contributions ceased (optional) - # - # All timestamps are UTC, and the "now" special string will be replaced by - # the current time. Do not remove entries or use an until time in the past - # unless you want existing snaps provided by the developer to stop working.""") # noqa - - -def _edit_collaborators(developers): - """Spawn an editor to modify the snap-developer assertion for a snap.""" - editor_cmd = os.getenv('EDITOR', 'vi') - - developer_wrapper = {'developers': _reformat_time_from_assertion( - developers)} - - with tempfile.NamedTemporaryFile() as ft: - ft.close() - with open(ft.name, 'w') as fw: - print(_COLLABORATION_HEADER, file=fw) - yaml.dump(developer_wrapper, stream=fw, default_flow_style=False) - subprocess.check_call([editor_cmd, ft.name]) - with open(ft.name, 'r') as fr: - developers = yaml.load(fr).get('developers') - return _reformat_time_for_assertion(developers) - - -def _reformat_time_from_assertion(developers): - reformatted_developers = [] - for developer in developers: - developer_it = {'developer-id': developer['developer-id']} - for range in ['since', 'until']: - if range in developer: - date = datetime.strptime(developer[range], - '%Y-%m-%dT%H:%M:%S.%fZ') - developer_it[range] = datetime.strftime(date, - '%Y-%m-%d %H:%M:%S') - reformatted_developers.append(developer_it) - return reformatted_developers - - -def _reformat_time_for_assertion(developers): - reformatted_developers = [] - for developer in developers: - developer_it = {'developer-id': developer['developer-id']} - for range_ in ['since', 'until']: - if range_ in developer: - if developer[range_] == 'now': - date = datetime.now() - else: - date = datetime.strptime(developer[range_], - '%Y-%m-%d %H:%M:%S') - # We don't care about microseconds because we cannot edit - # later so we set that to 0. - developer_it[range_] = date.strftime( - '%Y-%m-%dT%H:%M:%S.000000Z') - reformatted_developers.append(developer_it) - return reformatted_developers - - -def _are_developers_unchanged(edited_developers, developers_from_assertion): - # We need to compare without milliseconds, so drop them from the assertion. - for developer in developers_from_assertion: - for range_ in ['since', 'until']: - if range_ in developer: - date = datetime.strptime( - developer[range_], '%Y-%m-%dT%H:%M:%S.%fZ') - developer[range_] = date.strftime('%Y-%m-%dT%H:%M:%S.000000Z') - return developers_from_assertion == edited_developers - - -def _get_developers(snap_id, publisher_id): - store = storeapi.StoreClient() - try: - return store.get_assertion(snap_id, 'developers')['snap_developer'] - except storeapi.errors.StoreValidationError as e: - if e.error_list[0]['code'] == 'snap-developer-not-found': - return { - 'type': 'snap-developer', - 'authority-id': publisher_id, - 'publisher-id': publisher_id, - 'snap-id': snap_id} - raise - - def _check_validations(validations): invalids = [v for v in validations if not validation_re.match(v)] if invalids: @@ -910,7 +774,7 @@ echo.info('Signing {} assertion for {}'.format(endpoint, snap_name)) assertion, err = snap_sign.communicate(input=data) if snap_sign.returncode != 0: - err = err.decode('ascii', errors='replace') + err = err.decode() raise storeapi.errors.StoreAssertionError( endpoint=endpoint, snap_name=snap_name, error=err) diff -Nru snapcraft-2.34/snapcraft/tests/cache/test_snap.py snapcraft-2.35/snapcraft/tests/cache/test_snap.py --- snapcraft-2.34/snapcraft/tests/cache/test_snap.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/cache/test_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -13,19 +13,14 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . - import glob -import logging import os -from unittest import mock -import fixtures from testtools.matchers import Equals import snapcraft from snapcraft import file_utils from snapcraft.internal import cache -from snapcraft.tests import fixture_setup from snapcraft.tests.commands import CommandBaseTestCase @@ -33,15 +28,8 @@ def setUp(self): super().setUp() - self.fake_logger = fixtures.FakeLogger(level=logging.INFO) - self.useFixture(self.fake_logger) - - patcher = mock.patch('snapcraft.internal.lifecycle.ProgressBar') - patcher.start() - self.addCleanup(patcher.stop) self.deb_arch = snapcraft.ProjectOptions().deb_arch - self.snap_path = os.path.join( os.path.dirname(snapcraft.tests.__file__), 'data', 'test-snap.snap') @@ -50,8 +38,6 @@ class SnapCacheTestCase(SnapCacheBaseTestCase): def test_snap_cache(self): - self.useFixture(fixture_setup.FakeTerminal()) - # cache snap snap_cache = cache.SnapCache(project_name='cache-test') cached_snap_path = snap_cache.cache(snap_filename=self.snap_path) @@ -68,8 +54,6 @@ self.assertTrue(os.path.isfile(cached_snap_path)) def test_snap_cache_get_latest(self): - self.useFixture(fixture_setup.FakeTerminal()) - # Create snaps with open(os.path.join(self.path, 'snapcraft.yaml'), 'w') as f: f.write("""name: my-snap-name @@ -123,8 +107,6 @@ self.assertThat(latest_snap, Equals(expected_snap_path)) def test_snap_cache_get_by_hash(self): - self.useFixture(fixture_setup.FakeTerminal()) - snap_cache = cache.SnapCache(project_name='my-snap-name') snap_cache.cache(snap_filename=self.snap_path) @@ -142,16 +124,7 @@ class SnapCachePruneTestCase(SnapCacheBaseTestCase): - def setUp(self): - super().setUp() - self.fake_logger = fixtures.FakeLogger(level=logging.INFO) - self.useFixture(self.fake_logger) - - self.deb_arch = snapcraft.ProjectOptions().deb_arch - def test_prune_snap_cache(self): - self.useFixture(fixture_setup.FakeTerminal()) - # Create snaps with open(os.path.join(self.path, 'snapcraft.yaml'), 'w') as f: f.write("""name: my-snap-name diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_cleanbuild.py snapcraft-2.35/snapcraft/tests/commands/test_cleanbuild.py --- snapcraft-2.34/snapcraft/tests/commands/test_cleanbuild.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_cleanbuild.py 2017-11-01 19:41:33.000000000 +0000 @@ -16,6 +16,7 @@ import logging import os +import subprocess import tarfile from textwrap import dedent @@ -25,6 +26,8 @@ from snapcraft import tests from . import CommandBaseTestCase +from snapcraft.internal.errors import InvalidContainerRemoteError + class CleanBuildCommandBaseTestCase(CommandBaseTestCase): @@ -89,8 +92,6 @@ self.assertThat(result.exit_code, Equals(0)) self.assertIn( 'Setting up container with project assets\n' - 'Waiting for a network connection...\n' - 'Network connection established\n' 'Retrieved snap-test_1.0_amd64.snap\n', self.fake_logger.output) @@ -113,19 +114,47 @@ 'snap/snapcraft unexpectedly excluded from tarball') def test_cleanbuild_debug_appended_goes_to_shell_on_errors(self): - fake_lxd = tests.fixture_setup.FakeLXD(fail_on_snapcraft_run=True) + fake_lxd = tests.fixture_setup.FakeLXD() self.useFixture(fake_lxd) + def call_effect(*args, **kwargs): + # Fail on an actual snapcraft command and not the command + # for the installation of it. + if 'snapcraft snap' in ' '.join(args[0]): + raise subprocess.CalledProcessError( + returncode=255, cmd=args[0]) + + fake_lxd.check_call_mock.side_effect = call_effect + result = self.run_command(['cleanbuild', '--debug']) self.assertThat(result.exit_code, Equals(0)) self.assertThat(self.fake_logger.output, Contains( 'Debug mode enabled, dropping into a shell')) def test_cleanbuild_debug_prepended_goes_to_shell_on_errors(self): - fake_lxd = tests.fixture_setup.FakeLXD(fail_on_snapcraft_run=True) + fake_lxd = tests.fixture_setup.FakeLXD() self.useFixture(fake_lxd) + def call_effect(*args, **kwargs): + # Fail on an actual snapcraft command and not the command + # for the installation of it. + if 'snapcraft snap' in ' '.join(args[0]): + raise subprocess.CalledProcessError( + returncode=255, cmd=args[0]) + + fake_lxd.check_call_mock.side_effect = call_effect + result = self.run_command(['--debug', 'cleanbuild']) self.assertThat(result.exit_code, Equals(0)) self.assertThat(self.fake_logger.output, Contains( 'Debug mode enabled, dropping into a shell')) + + def test_invalid_remote(self): + fake_lxd = tests.fixture_setup.FakeLXD() + self.useFixture(fake_lxd) + + self.assertIn( + "'foo/bar' is not a valid LXD remote name", + str(self.assertRaises( + InvalidContainerRemoteError, + self.run_command, ['cleanbuild', '--remote', 'foo/bar']))) diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_clean.py snapcraft-2.35/snapcraft/tests/commands/test_clean.py --- snapcraft-2.34/snapcraft/tests/commands/test_clean.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_clean.py 2017-11-01 19:41:33.000000000 +0000 @@ -16,7 +16,7 @@ import os import shutil import fixtures -from unittest.mock import call +from unittest.mock import call, patch, ANY from testtools.matchers import Contains, Equals, DirExists, FileExists, Not from snapcraft.tests import fixture_setup @@ -97,11 +97,19 @@ self.assertThat(self.stage_dir, Not(DirExists())) self.assertThat(self.prime_dir, Not(DirExists())) + +class ContainerizedCleanCommandTestCase(CleanCommandTestCase): + + scenarios = [ + ('local', dict(snapcraft_container_builds='1', remote='local')), + ('remote', dict(snapcraft_container_builds='foo', remote='foo')), + ] + def test_clean_containerized_noop(self): fake_lxd = fixture_setup.FakeLXD() self.useFixture(fake_lxd) self.useFixture(fixtures.EnvironmentVariable( - 'SNAPCRAFT_CONTAINER_BUILDS', '1')) + 'SNAPCRAFT_CONTAINER_BUILDS', self.snapcraft_container_builds)) self.make_snapcraft_yaml(n=3) result = self.run_command(['clean']) @@ -110,14 +118,15 @@ # clean should be a noop if no container exists yet/ anymore fake_lxd.check_call_mock.assert_not_called() - def test_clean_containerized_exists_stopped(self): + @patch('snapcraft.internal.lifecycle.clean') + def test_clean_containerized_exists_stopped(self, mock_lifecycle_clean): fake_lxd = fixture_setup.FakeLXD() self.useFixture(fake_lxd) # Container was created before, and isn't running - fake_lxd.name = 'local:snapcraft-clean-test' + fake_lxd.name = '{}:snapcraft-clean-test'.format(self.remote) fake_lxd.status = 'Stopped' self.useFixture(fixtures.EnvironmentVariable( - 'SNAPCRAFT_CONTAINER_BUILDS', '1')) + 'SNAPCRAFT_CONTAINER_BUILDS', self.snapcraft_container_builds)) self.make_snapcraft_yaml(n=3) result = self.run_command(['clean']) @@ -129,12 +138,37 @@ ]) # no other commands should be run in the container self.assertThat(fake_lxd.check_call_mock.call_count, Equals(1)) + # clean should be called normally, outside of the container + mock_lifecycle_clean.assert_has_calls([ + call(ANY, (), 'pull')]) + + @patch('snapcraft.internal.lifecycle.clean') + def test_clean_containerized_pull_retains_container( + self, mock_lifecycle_clean): + fake_lxd = fixture_setup.FakeLXD() + self.useFixture(fake_lxd) + # Container was created before, and isn't running + fake_lxd.name = '{}:snapcraft-clean-test'.format(self.remote) + fake_lxd.status = 'Stopped' + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_CONTAINER_BUILDS', self.snapcraft_container_builds)) + self.make_snapcraft_yaml(n=3) + + result = self.run_command(['clean', '-s', 'pull']) + + self.assertThat(result.exit_code, Equals(0)) + # clean pull should NOT delete the container + fake_lxd.check_call_mock.assert_not_called() + # clean should be called normally, outside of the container + mock_lifecycle_clean.assert_has_calls([ + call(ANY, (), 'pull')]) def test_clean_containerized_with_part(self): fake_lxd = fixture_setup.FakeLXD() + fake_lxd.name = 'local:snapcraft-clean-test' self.useFixture(fake_lxd) self.useFixture(fixtures.EnvironmentVariable( - 'SNAPCRAFT_CONTAINER_BUILDS', '1')) + 'SNAPCRAFT_CONTAINER_BUILDS', self.snapcraft_container_builds)) self.make_snapcraft_yaml(n=3) result = self.run_command(['clean', 'clean1']) @@ -144,6 +178,9 @@ self.assertNotEqual(fake_lxd.check_call_mock.call_args, call(['lxc', 'delete', '-f', fake_lxd.name])) + +class CleanCommandPartsTestCase(CleanCommandTestCase): + def test_local_plugin_not_removed(self): self.make_snapcraft_yaml(n=3) diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_collaborate.py snapcraft-2.35/snapcraft/tests/commands/test_collaborate.py --- snapcraft-2.34/snapcraft/tests/commands/test_collaborate.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_collaborate.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,122 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017 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 . - -import logging -from unittest import mock - -import fixtures -from testtools.matchers import Equals - -from snapcraft import ( - storeapi, - tests -) -from snapcraft import _store - - -class CollaborateBaseTestCase(tests.TestCase): - - def setUp(self): - super().setUp() - self.fake_logger = fixtures.FakeLogger(level=logging.INFO) - self.useFixture(self.fake_logger) - self.fake_store = tests.fixture_setup.FakeStore() - self.useFixture(self.fake_store) - self.client = storeapi.StoreClient() - patcher = mock.patch('snapcraft._store.Popen') - self.popen_mock = patcher.start() - process_mock = mock.Mock() - process_mock.returncode = 0 - process_mock.communicate.return_value = [b'foo', b''] - self.popen_mock.return_value = process_mock - self.addCleanup(patcher.stop) - patcher = mock.patch('snapcraft._store._edit_collaborators') - self.edit_collaborators_mock = patcher.start() - self.addCleanup(patcher.stop) - - -class CollaborateTestCase(CollaborateBaseTestCase): - - def test_collaborate_success(self): - self.edit_collaborators_mock.return_value = { - 'developers': [{ - 'developer-id': 'dummy-id', - 'since': '2015-07-19 19:30:00'}] - } - self.client.login('dummy', 'test correct password') - _store.collaborate('ubuntu-core', 'keyname') - - self.popen_mock.assert_called_with(['snap', 'sign', '-k', 'keyname'], - stderr=-1, stdin=-1, stdout=-1) - self.assertNotIn('Error signing developers assertion', - self.fake_logger.output) - self.assertNotIn('Invalid response from the server', - self.fake_logger.output) - - -class CollaborateErrorsTestCase(CollaborateBaseTestCase): - - def setUp(self): - super().setUp() - self.edit_collaborators_mock.return_value = { - 'developers': [{ - 'developer-id': 'dummy-id', - 'since': '2015-07-19 19:30:00'}] - } - - def test_collaborate_snap_not_found(self): - self.client.login('dummy', 'test correct password') - - err = self.assertRaises( - storeapi.errors.SnapNotFoundError, - _store.collaborate, - 'notfound', 'key') - - self.assertIn("Snap 'notfound' was not found", str(err)) - - def test_collaborate_snap_developer_not_found(self): - self.client.login('dummy', 'test correct password') - - _store.collaborate('core-no-dev', 'keyname') - - self.assertNotIn('Error signing developers assertion', - self.fake_logger.output) - self.assertNotIn('Invalid response from the server', - self.fake_logger.output) - - def test_collaborate_bad_request(self): - self.client.login('dummy', 'test correct password') - err = self.assertRaises( - storeapi.errors.StoreValidationError, - _store.collaborate, - 'badrequest', 'keyname') - - self.assertThat( - str(err), - Equals('Received error 400: "The given `snap-id` does not match ' - 'the assertion\'s."')) - - def test_collaborate_unchanged_collaborators(self): - self.edit_collaborators_mock.return_value = [{ - 'developer-id': 'test-dev-id', - 'since': '2017-02-10T08:35:00.000000Z', - 'until': '2018-02-10T08:35:00.000000Z' - }] - self.client.login('dummy', 'test correct password') - _store.collaborate('test-snap-with-dev', 'keyname') - - self.assertIn('Aborting due to unchanged collaborators list.', - self.fake_logger.output) diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_collaborators.py snapcraft-2.35/snapcraft/tests/commands/test_collaborators.py --- snapcraft-2.34/snapcraft/tests/commands/test_collaborators.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_collaborators.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,238 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . +from unittest import mock + +import fixtures +from testtools.matchers import Contains, Equals + +from snapcraft import storeapi, tests +from snapcraft.cli import assertions +from . import StoreCommandsBaseTestCase + + +class CollaborateBaseTestCase(StoreCommandsBaseTestCase): + + def setUp(self): + super().setUp() + patcher = mock.patch('subprocess.Popen') + self.popen_mock = patcher.start() + process_mock = mock.Mock() + process_mock.returncode = 0 + process_mock.communicate.return_value = [b'foo', b''] + self.popen_mock.return_value = process_mock + self.addCleanup(patcher.stop) + patcher = mock.patch('snapcraft.cli.assertions._edit_developers') + self.edit_developers_mock = patcher.start() + self.edit_developers_mock.return_value = [ + {'developer-id': 'dummy-id', + 'since': '2015-07-19 19:30:00'} + ] + self.addCleanup(patcher.stop) + self.client.login('dummy', 'test correct password') + + +class CollaboratorsCommandTestCase(CollaborateBaseTestCase): + + scenarios = [ + ('edit-collaborators', {'command_name': 'edit-collaborators'}), + ('collaborators alias', {'command_name': 'collaborators'}), + ] + + def test_collaborators_without_snap_name_must_error(self): + result = self.run_command([self.command_name]) + + self.assertThat(result.exit_code, Equals(2)) + self.assertThat(result.output, Contains('Usage:')) + + def test_collaborators_update_developers(self): + result = self.run_command([self.command_name, 'test-snap-with-dev', + '--key-name', 'key-name']) + + self.assertThat(result.exit_code, Equals(0)) + self.popen_mock.assert_called_with(['snap', 'sign', '-k', 'key-name'], + stderr=-1, stdin=-1, stdout=-1) + + def test_collaborators_add_developers(self): + result = self.run_command([self.command_name, 'core', + '--key-name', 'key-name']) + + self.assertThat(result.exit_code, Equals(0)) + self.popen_mock.assert_called_with(['snap', 'sign', '-k', 'key-name'], + stderr=-1, stdin=-1, stdout=-1) + + def test_no_prior_developer_assertion(self): + result = self.run_command([self.command_name, 'core-no-dev', + '--key-name', 'key-name']) + + self.assertThat(result.exit_code, Equals(0)) + self.popen_mock.assert_called_with(['snap', 'sign', '-k', 'key-name'], + stderr=-1, stdin=-1, stdout=-1) + + def test_collaborate_no_revoke_uploads_request(self): + result = self.run_command([self.command_name] + + ['no-revoked', + '--key-name', 'key-name']) + + self.assertThat(result.exit_code, Equals(0)) + self.assertThat(result.output, Contains( + 'This will revoke the following collaborators: \'this\'')) + self.assertThat(result.output, Contains( + 'Are you sure you want to continue? [y/N]:')) + self.assertThat(result.output, Contains( + 'The collaborators for this snap have not been altered.')) + + def test_collaborate_unchanged_collaborators(self): + self.edit_developers_mock.return_value = [{ + 'developer-id': 'test-dev-id', + 'since': '2017-02-10 08:35:00', + 'until': '2018-02-10 08:35:00' + }] + + result = self.run_command([self.command_name] + + ['test-snap-with-dev', + '--key-name', 'key-name']) + + self.assertThat(result.exit_code, Equals(0)) + self.assertThat(result.output, Contains( + 'Aborting due to unchanged collaborators list.')) + + +class CollaborateErrorsTestCase(CollaborateBaseTestCase): + + scenarios = [ + ('edit-collaborators', {'command_name': 'edit-collaborators'}), + ('collaborators alias', {'command_name': 'collaborators'}), + ] + + def test_collaborate_snap_not_found(self): + raised = self.assertRaises( + storeapi.errors.SnapNotFoundError, + self.run_command, + [self.command_name, 'notfound', '--key-name', 'key-name']) + + self.assertThat(str(raised), Contains( + 'Snap \'notfound\' was not found')) + + def test_collaborate_bad_request(self): + raised = self.assertRaises( + storeapi.errors.StoreValidationError, + self.run_command, + [self.command_name, 'badrequest', '--key-name', 'key-name']) + + self.assertThat(str(raised), Contains( + 'Received error 400: \'The given `snap-id` does not match ' + 'the assertion.\'')) + + def test_collaborate_yes_revoke_uploads_request(self): + raised = self.assertRaises( + storeapi.errors.StoreValidationError, + self.run_command, + [self.command_name, 'revoked', '--key-name', 'key-name'], + input='y\n') + + self.assertThat(str(raised), Contains( + 'Received error 409: "{}"'.format( + "The assertion's `developers` would revoke " + "existing uploads."))) + + +class EditDevelopersTestCase(tests.TestCase): + + def setUp(self): + super().setUp() + patcher = mock.patch('subprocess.check_call') + patcher.start() + self.addCleanup(patcher.stop) + + def test_edit_developers_must_write_header_and_developers(self): + developers_from_assertion = [ + {'developer-id': 'test-dev-id1', + 'since': '2017-02-10T08:35:00.000000Z', + 'until': '2018-02-10T08:35:00.000000Z'}, + {'developer-id': 'test-dev-id2', + 'since': '2016-02-10T08:35:00.000000Z', + 'until': '2019-02-10T08:35:00.000000Z'}, + ] + + existing_developers = ("developers:\n" + "- developer-id: test-dev-id1\n" + " since: '2017-02-10 08:35:00'\n" + " until: '2018-02-10 08:35:00'\n" + "- developer-id: test-dev-id2\n" + " since: '2016-02-10 08:35:00'\n" + " until: '2019-02-10 08:35:00'\n") + expected_written = (assertions._COLLABORATION_HEADER + '\n' + + existing_developers) + + with mock.patch( + 'builtins.open', + new_callable=mock.mock_open, + read_data=expected_written) as mock_open: + developers_for_assertion = assertions._update_developers( + developers_from_assertion) + + written = '' + for call in mock_open().write.call_args_list: + written += str(call.call_list()[0][0][0]) + self.assertThat(written, Equals(expected_written)) + self.assertThat( + developers_for_assertion, Equals(developers_from_assertion)) + + def test_edit_developers_must_return_new_developers(self): + developers_from_assertion = [ + {'developer-id': 'test-dev-id1', + 'since': '2017-02-10T08:35:00.000000Z', + 'until': '2018-02-10T08:35:00.000000Z'}, + ] + + new_developers = ("developers:\n" + "- developer-id: test-dev-id1\n" + " since: '2017-02-10 08:35:00'\n" + " until: '2018-02-10 08:35:00'\n" + "- developer-id: test-dev-id2\n" + " since: '2016-02-10 08:35:00'\n" + " until: '2019-02-10 08:35:00'\n") + + with mock.patch('builtins.open', new_callable=mock.mock_open, + read_data=new_developers): + developers_for_assertion = assertions._update_developers( + developers_from_assertion) + + expected_developers = developers_from_assertion + [{ + 'developer-id': 'test-dev-id2', + 'since': '2016-02-10T08:35:00.000000Z', + 'until': '2019-02-10T08:35:00.000000Z', + }] + self.assertThat(developers_for_assertion, Equals(expected_developers)) + + +class EditDevelopersOpenEditorTestCase(tests.TestCase): + + scenarios = ( + ('default', {'editor': None, 'expected': 'vi'}), + ('non-default', {'editor': 'test-editor', 'expected': 'test-editor'}) + ) + + def setUp(self): + super().setUp() + patcher = mock.patch('subprocess.check_call') + self.check_call_mock = patcher.start() + self.addCleanup(patcher.stop) + + def test_edit_collaborators_must_open_editor(self): + self.useFixture(fixtures.EnvironmentVariable('EDITOR', self.editor)) + assertions._edit_developers({}) + self.check_call_mock.assert_called_with([self.expected, mock.ANY]) diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_gated.py snapcraft-2.35/snapcraft/tests/commands/test_gated.py --- snapcraft-2.34/snapcraft/tests/commands/test_gated.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_gated.py 2017-11-01 19:41:33.000000000 +0000 @@ -24,7 +24,7 @@ account_info_data = { 'snaps': { '16': { - 'ubuntu-core': {'snap-id': 'good'}, + 'core': {'snap-id': 'good'}, } } } @@ -44,7 +44,7 @@ def test_gated_success(self): self.client.login('dummy', 'test correct password') - result = self.run_command(['gated', 'ubuntu-core']) + result = self.run_command(['gated', 'core']) self.assertThat(result.exit_code, Equals(0)) expected_output = textwrap.dedent("""\ diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_list_plugins.py snapcraft-2.35/snapcraft/tests/commands/test_list_plugins.py --- snapcraft-2.34/snapcraft/tests/commands/test_list_plugins.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_list_plugins.py 2017-11-01 19:41:33.000000000 +0000 @@ -28,12 +28,12 @@ # plugin list when wrapper at MAX_CHARACTERS_WRAP default_plugin_output = ( - 'ant cmake go gulp kbuild maven ' - 'nodejs python2 rust waf\n' - 'autotools copy godeps jdk kernel meson ' - 'plainbox-provider python3 scons \n' - 'catkin dump gradle jhbuild make nil ' - 'python qmake tar-content\n' + 'ant cmake dump gradle jhbuild make ' + 'nil python qmake scons \n' + 'autotools copy go gulp kbuild maven ' + 'nodejs python2 ruby tar-content\n' + 'catkin dotnet godeps jdk kernel meson ' + 'plainbox-provider python3 rust waf \n' ) def test_list_plugins_non_tty(self): @@ -54,7 +54,7 @@ result = self.run_command([self.command_name]) self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains(self.default_plugin_output)) + self.assertThat(result.output, Equals(self.default_plugin_output)) def test_list_plugins_small_terminal(self): self.maxDiff = None @@ -62,15 +62,15 @@ self.useFixture(fake_terminal) expected_output = ( - 'ant go kbuild nodejs rust \n' - 'autotools godeps kernel plainbox-provider scons \n' - 'catkin gradle make python tar-content\n' - 'cmake gulp maven python2 waf \n' - 'copy jdk meson python3 \n' - 'dump jhbuild nil qmake \n' + 'ant dump jhbuild nil qmake \n' + 'autotools go kbuild nodejs ruby \n' + 'catkin godeps kernel plainbox-provider rust \n' + 'cmake gradle make python scons \n' + 'copy gulp maven python2 tar-content\n' + 'dotnet jdk meson python3 waf \n' ) result = self.run_command([self.command_name]) self.assertThat(result.exit_code, Equals(0)) - self.assertThat(result.output, Contains(expected_output)) + self.assertThat(result.output, Equals(expected_output)) diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_logout.py snapcraft-2.35/snapcraft/tests/commands/test_logout.py --- snapcraft-2.34/snapcraft/tests/commands/test_logout.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_logout.py 2017-11-01 19:41:33.000000000 +0000 @@ -30,6 +30,4 @@ self.assertThat(result.exit_code, Equals(0)) self.assertThat(result.output, MatchesRegex( - 'Clearing credentials for Ubuntu One SSO.\n' - '.*' - 'Credentials cleared.\n', flags=re.DOTALL)) + '.*Credentials cleared.\n', flags=re.DOTALL)) diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_pack.py snapcraft-2.35/snapcraft/tests/commands/test_pack.py --- snapcraft-2.34/snapcraft/tests/commands/test_pack.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_pack.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,111 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . +import os +import os.path +import subprocess +from textwrap import dedent +from unittest import mock + +from testtools.matchers import Contains, Equals, FileExists +from . import CommandBaseTestCase + + +class PackCommandBaseTestCase(CommandBaseTestCase): + + def setUp(self): + super().setUp() + patcher = mock.patch('snapcraft.internal.lifecycle._packer.Popen', + new=mock.Mock(wraps=subprocess.Popen)) + self.popen_spy = patcher.start() + self.addCleanup(patcher.stop) + + +class PackCommandTestCase(PackCommandBaseTestCase): + + scenarios = ( + ('pack', dict(command='pack')), + ('deprecated snap', dict(command='snap')), + ) + + def setUp(self): + super().setUp() + self.snap_dir = 'mysnap' + self.meta_dir = os.path.join(self.snap_dir, 'meta') + os.makedirs(self.meta_dir) + self.snap_yaml = os.path.join(self.meta_dir, 'snap.yaml') + + def test_snap_from_dir(self): + with open(self.snap_yaml, 'w') as f: + f.write(dedent("""\ + name: my_snap + version: 99 + architectures: [amd64, armhf] + """)) + + result = self.run_command([self.command, self.snap_dir]) + + self.assertThat(result.exit_code, Equals(0)) + self.assertThat(result.output, Contains( + 'Snapped my_snap_99_multi.snap\n')) + + self.popen_spy.assert_called_once_with([ + 'mksquashfs', 'mysnap', 'my_snap_99_multi.snap', + '-noappend', '-comp', 'xz', '-no-xattrs', '-all-root'], + stderr=subprocess.STDOUT, stdout=subprocess.PIPE) + + self.assertThat('my_snap_99_multi.snap', FileExists()) + + def test_snap_from_dir_with_no_arch(self): + with open(self.snap_yaml, 'w') as f: + f.write(dedent("""\ + name: my_snap + version: 99 + """)) + + result = self.run_command([self.command, self.snap_dir]) + + self.assertThat(result.exit_code, Equals(0)) + self.assertThat(result.output, Contains( + 'Snapped my_snap_99_all.snap\n')) + + self.popen_spy.assert_called_once_with([ + 'mksquashfs', 'mysnap', 'my_snap_99_all.snap', + '-noappend', '-comp', 'xz', '-no-xattrs', '-all-root'], + stderr=subprocess.STDOUT, stdout=subprocess.PIPE) + + self.assertThat('my_snap_99_all.snap', FileExists()) + + def test_snap_from_dir_type_os_does_not_use_all_root(self): + with open(self.snap_yaml, 'w') as f: + f.write(dedent("""\ + name: my_snap + version: 99 + architectures: [amd64, armhf] + type: os + """)) + + result = self.run_command([self.command, self.snap_dir]) + + self.assertThat(result.exit_code, Equals(0)) + self.assertThat(result.output, Contains( + 'Snapped my_snap_99_multi.snap\n')) + + self.popen_spy.assert_called_once_with([ + 'mksquashfs', 'mysnap', 'my_snap_99_multi.snap', + '-noappend', '-comp', 'xz', '-no-xattrs'], + stderr=subprocess.STDOUT, stdout=subprocess.PIPE) + + self.assertThat('my_snap_99_multi.snap', FileExists()) diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_push.py snapcraft-2.35/snapcraft/tests/commands/test_push.py --- snapcraft-2.34/snapcraft/tests/commands/test_push.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_push.py 2017-11-01 19:41:33.000000000 +0000 @@ -37,10 +37,6 @@ def setUp(self): super().setUp() - patcher = mock.patch('snapcraft.internal.lifecycle.ProgressBar') - patcher.start() - self.addCleanup(patcher.stop) - patcher = mock.patch('snapcraft.storeapi.StoreClient.push_precheck') patcher.start() self.addCleanup(patcher.stop) diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_refresh.py snapcraft-2.35/snapcraft/tests/commands/test_refresh.py --- snapcraft-2.34/snapcraft/tests/commands/test_refresh.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_refresh.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,104 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . +import os + +import fixtures +from xdg import BaseDirectory +from textwrap import dedent +from unittest import mock +from unittest.mock import call + +from testtools.matchers import Equals +from snapcraft.tests import TestWithFakeRemoteParts +from snapcraft.tests import fixture_setup +from . import CommandBaseTestCase +from snapcraft.internal.errors import SnapcraftEnvironmentError + + +class RefreshCommandBaseTestCase(CommandBaseTestCase, TestWithFakeRemoteParts): + + yaml_template = dedent("""\ + name: snap-test + version: 1.0 + summary: test snapping + description: if snap is succesful a snap package will be available + architectures: ['amd64'] + type: {} + confinement: strict + grade: stable + + parts: + part1: + plugin: nil + """) + + def setUp(self): + super().setUp() + self.parts_dir = os.path.join(BaseDirectory.xdg_data_home, 'snapcraft') + self.parts_yaml = os.path.join(self.parts_dir, 'parts.yaml') + self.headers_yaml = os.path.join(self.parts_dir, 'headers.yaml') + + def make_snapcraft_yaml(self, n=1, snap_type='app', snapcraft_yaml=None): + if not snapcraft_yaml: + snapcraft_yaml = self.yaml_template.format(snap_type) + super().make_snapcraft_yaml(snapcraft_yaml) + self.state_dir = os.path.join(self.parts_dir, 'part1', 'state') + + +class RefreshCommandTestCase(RefreshCommandBaseTestCase): + + scenarios = [ + ('local', dict(snapcraft_container_builds='1', remote='local')), + ('remote', dict(snapcraft_container_builds='foo', remote='foo')), + ] + + @mock.patch('snapcraft.internal.lxd.Containerbuild._container_run') + def test_refresh(self, mock_container_run): + mock_container_run.side_effect = lambda cmd, **kwargs: cmd + fake_lxd = fixture_setup.FakeLXD() + self.useFixture(fake_lxd) + fake_filesystem = fixture_setup.FakeFilesystem() + self.useFixture(fake_filesystem) + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_CONTAINER_BUILDS', self.snapcraft_container_builds)) + self.make_snapcraft_yaml() + + self.run_command(['refresh']) + + mock_container_run.assert_has_calls([ + call(['apt-get', 'update']), + call(['apt-get', 'upgrade', '-y']), + call(['snap', 'refresh']), + ]) + self.assertThat(fake_lxd.name, + Equals('{}:snapcraft-snap-test'.format(self.remote))) + + +class RefreshCommandErrorsTestCase(RefreshCommandBaseTestCase): + + @mock.patch('snapcraft.internal.lxd.Containerbuild._container_run') + def test_refresh_fails_without_env_var(self, mock_container_run): + mock_container_run.side_effect = lambda cmd, **kwargs: cmd + fake_lxd = fixture_setup.FakeLXD() + self.useFixture(fake_lxd) + fake_filesystem = fixture_setup.FakeFilesystem() + self.useFixture(fake_filesystem) + self.make_snapcraft_yaml() + + self.assertRaises(SnapcraftEnvironmentError, + self.run_command, + ['refresh']) + mock_container_run.assert_not_called() diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_snap.py snapcraft-2.35/snapcraft/tests/commands/test_snap.py --- snapcraft-2.34/snapcraft/tests/commands/test_snap.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_snap.py 2017-11-01 19:41:33.000000000 +0000 @@ -63,7 +63,7 @@ self.useFixture(fixture_setup.FakeTerminal()) - patcher = mock.patch('snapcraft.internal.lifecycle.Popen', + patcher = mock.patch('snapcraft.internal.lifecycle._packer.Popen', new=mock.Mock(wraps=subprocess.Popen)) self.popen_spy = patcher.start() self.addCleanup(patcher.stop) @@ -116,116 +116,75 @@ '-noappend', '-comp', 'xz', '-no-xattrs', '-all-root'], stderr=subprocess.STDOUT, stdout=subprocess.PIPE) - @mock.patch('os.getuid') @mock.patch('snapcraft.internal.lxd.Containerbuild._container_run') - @mock.patch('snapcraft.internal.lxd.Containerbuild._inject_snapcraft') - def test_snap_containerized(self, - mock_inject, - mock_container_run, - mock_getuid): + @mock.patch('os.pipe') + def test_snap_containerized_remote(self, + mock_pipe, + mock_container_run): mock_container_run.side_effect = lambda cmd, **kwargs: cmd - mock_getuid.return_value = 1234 + mock_pipe.return_value = (9, 9) fake_lxd = fixture_setup.FakeLXD() self.useFixture(fake_lxd) + fake_filesystem = fixture_setup.FakeFilesystem() + self.useFixture(fake_filesystem) fake_logger = fixtures.FakeLogger(level=logging.INFO) self.useFixture(fake_logger) self.useFixture(fixtures.EnvironmentVariable( - 'SNAPCRAFT_CONTAINER_BUILDS', '1')) + 'SNAPCRAFT_CONTAINER_BUILDS', 'myremote')) self.make_snapcraft_yaml() - result = self.run_command(['snap']) + result = self.run_command(['--debug', 'snap']) self.assertThat(result.exit_code, Equals(0)) source = os.path.realpath(os.path.curdir) self.assertIn( - 'Mounting {} into container\n' + "Using LXD remote 'myremote' from SNAPCRAFT_CONTAINER_BUILDS\n" 'Waiting for a network connection...\n' - 'Network connection established\n'.format(source), + 'Network connection established\n' + 'Mounting {} into container\n'.format(source), fake_logger.output) - container_name = 'local:snapcraft-snap-test' project_folder = '/root/build_snap-test' - fake_lxd.check_call_mock.assert_has_calls([ - call(['lxc', 'init', 'ubuntu:xenial/amd64', container_name]), - call(['lxc', 'config', 'set', container_name, - 'environment.SNAPCRAFT_SETUP_CORE', '1']), - call(['lxc', 'config', 'set', container_name, - 'raw.idmap', 'both {} 0'.format(mock_getuid.return_value)]), - call(['lxc', 'start', container_name]), - call(['lxc', 'config', 'device', 'add', container_name, - project_folder, 'disk', 'source={}'.format(source), - 'path={}'.format(project_folder)]), - call(['lxc', 'stop', '-f', container_name]), - ]) mock_container_run.assert_has_calls([ - call(['python3', '-c', 'import urllib.request; ' + - 'urllib.request.urlopen(' + - '"http://start.ubuntu.com/connectivity-check.html"' + - ', timeout=5)']), - call(['apt-get', 'update']), - call(['snapcraft', 'snap', '--output', - 'snap-test_1.0_amd64.snap'], - cwd=project_folder), + call(['apt-get', 'install', '-y', 'sshfs']), + ]) + fake_lxd.popen_mock.assert_has_calls([ + call(['/usr/lib/sftp-server'], + stdin=9, stdout=9), + call(['lxc', 'exec', fake_lxd.name, '--', + 'sshfs', '-o', 'slave', '-o', 'nonempty', + ':{}'.format(source), project_folder], + stdin=9, stdout=9), ]) - @mock.patch('os.getuid') @mock.patch('snapcraft.internal.lxd.Containerbuild._container_run') - @mock.patch('snapcraft.internal.lxd.Containerbuild._inject_snapcraft') - def test_snap_containerized_exists_stopped(self, - mock_inject, - mock_container_run, - mock_getuid): + @mock.patch('shutil.rmtree') + @mock.patch('os.makedirs') + @mock.patch('snapcraft.internal.lxd.open') + def test_snap_containerized_invalid_remote(self, + mock_open, + mock_makedirs, + mock_rmtree, + mock_container_run): mock_container_run.side_effect = lambda cmd, **kwargs: cmd - mock_getuid.return_value = 1234 + mock_open.return_value = mock.MagicMock(spec=open) fake_lxd = fixture_setup.FakeLXD() self.useFixture(fake_lxd) - # Container was created before, and isn't running - fake_lxd.devices = '{"/root/build_snap-test":[]}' - fake_lxd.name = 'local:snapcraft-snap-test' - fake_lxd.status = 'Stopped' + fake_logger = fixtures.FakeLogger(level=logging.INFO) self.useFixture(fake_logger) self.useFixture(fixtures.EnvironmentVariable( - 'SNAPCRAFT_CONTAINER_BUILDS', '1')) + 'SNAPCRAFT_CONTAINER_BUILDS', 'foo/bar')) self.make_snapcraft_yaml() - result = self.run_command(['snap']) - - self.assertThat(result.exit_code, Equals(0)) - - source = os.path.realpath(os.path.curdir) self.assertIn( - 'Mounting {} into container\n' - 'Waiting for a network connection...\n' - 'Network connection established\n'.format(source), - fake_logger.output) + "'foo/bar' is not a valid LXD remote name", + str(self.assertRaises( + snapcraft.internal.errors.InvalidContainerRemoteError, + self.run_command, ['--debug', 'snap']))) - container_name = 'local:snapcraft-snap-test' - project_folder = '/root/build_snap-test' - fake_lxd.check_call_mock.assert_has_calls([ - call(['lxc', 'config', 'set', container_name, - 'environment.SNAPCRAFT_SETUP_CORE', '1']), - call(['lxc', 'config', 'set', container_name, - 'raw.idmap', 'both {} 0'.format(mock_getuid.return_value)]), - call(['lxc', 'config', 'device', 'remove', container_name, - project_folder]), - call(['lxc', 'start', container_name]), - call(['lxc', 'stop', '-f', container_name]), - ]) - mock_container_run.assert_has_calls([ - call(['python3', '-c', 'import urllib.request; ' + - 'urllib.request.urlopen(' + - '"http://start.ubuntu.com/connectivity-check.html"' + - ', timeout=5)']), - call(['apt-get', 'update']), - call(['snapcraft', 'snap', '--output', - 'snap-test_1.0_amd64.snap'], - cwd=project_folder), - ]) - - @mock.patch('snapcraft.internal.lifecycle.ProgressBar') - def test_snap_defaults_on_a_tty(self, progress_mock): + def test_snap_defaults_on_a_tty(self): fake_logger = fixtures.FakeLogger(level=logging.INFO) self.useFixture(fake_logger) self.useFixture(fixture_setup.FakeTerminal()) @@ -310,7 +269,7 @@ 'Snapped my_snap_99_multi.snap\n')) self.popen_spy.assert_called_once_with([ - 'mksquashfs', os.path.abspath('mysnap'), 'my_snap_99_multi.snap', + 'mksquashfs', 'mysnap', 'my_snap_99_multi.snap', '-noappend', '-comp', 'xz', '-no-xattrs', '-all-root'], stderr=subprocess.STDOUT, stdout=subprocess.PIPE) @@ -334,7 +293,7 @@ 'Snapped my_snap_99_all.snap\n')) self.popen_spy.assert_called_once_with([ - 'mksquashfs', os.path.abspath('mysnap'), 'my_snap_99_all.snap', + 'mksquashfs', 'mysnap', 'my_snap_99_all.snap', '-noappend', '-comp', 'xz', '-no-xattrs', '-all-root'], stderr=subprocess.STDOUT, stdout=subprocess.PIPE) @@ -361,7 +320,7 @@ 'Snapped my_snap_99_multi.snap\n')) self.popen_spy.assert_called_once_with([ - 'mksquashfs', os.path.abspath('mysnap'), 'my_snap_99_multi.snap', + 'mksquashfs', 'mysnap', 'my_snap_99_multi.snap', '-noappend', '-comp', 'xz', '-no-xattrs'], stderr=subprocess.STDOUT, stdout=subprocess.PIPE) @@ -453,6 +412,190 @@ snap_build_renamed, FileContains('signed assertion?')) +class SnapCommandWithContainerBuildTestCase(SnapCommandBaseTestCase): + + scenarios = ( + ('with SUDO_UID', { + 'SUDO_UID': 'test_sudo_uid', + 'getuid': None, + 'expected_idmap': 'test_sudo_uid'}), + ('without SUDO_UID', { + 'SUDO_UID': None, + 'getuid': 'test_getuid', + 'expected_idmap': 'test_getuid'}), + ) + + @mock.patch('os.getuid') + @mock.patch('snapcraft.internal.lxd.Containerbuild._container_run') + @mock.patch('snapcraft.internal.lxd.Containerbuild._inject_snapcraft') + def test_snap_containerized(self, + mock_inject, + mock_container_run, + mock_getuid): + self.useFixture( + fixtures.EnvironmentVariable('SUDO_UID', self.SUDO_UID)) + mock_getuid.return_value = self.getuid + mock_container_run.side_effect = lambda cmd, **kwargs: cmd + fake_lxd = fixture_setup.FakeLXD() + self.useFixture(fake_lxd) + fake_logger = fixtures.FakeLogger(level=logging.INFO) + self.useFixture(fake_logger) + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_CONTAINER_BUILDS', '1')) + self.make_snapcraft_yaml() + + result = self.run_command(['snap']) + + self.assertThat(result.exit_code, Equals(0)) + + source = os.path.realpath(os.path.curdir) + self.assertIn( + 'Using default LXD remote because ' + 'SNAPCRAFT_CONTAINER_BUILDS is set to 1\n' + 'Waiting for a network connection...\n' + 'Network connection established\n' + 'Mounting {} into container\n'.format(source), + fake_logger.output) + + container_name = 'local:snapcraft-snap-test' + project_folder = '/root/build_snap-test' + fake_lxd.check_call_mock.assert_has_calls([ + call(['lxc', 'init', 'ubuntu:xenial/amd64', container_name]), + call(['lxc', 'config', 'set', container_name, + 'environment.SNAPCRAFT_SETUP_CORE', '1']), + call(['lxc', 'config', 'set', container_name, + 'environment.LC_ALL', 'C.UTF-8']), + call(['lxc', 'config', 'set', container_name, + 'environment.SNAPCRAFT_IMAGE_INFO', + '{"fingerprint": "test-fingerprint", ' + '"architecture": "test-architecture", ' + '"created_at": "test-created-at"}']), + call(['lxc', 'config', 'set', container_name, + 'raw.idmap', 'both {} 0'.format(self.expected_idmap)]), + call(['lxc', 'config', 'device', 'add', container_name, + 'fuse', 'unix-char', 'path=/dev/fuse']), + call(['lxc', 'start', container_name]), + call(['lxc', 'config', 'device', 'add', container_name, + project_folder, 'disk', 'source={}'.format(source), + 'path={}'.format(project_folder)]), + call(['lxc', 'stop', '-f', container_name]), + ]) + mock_container_run.assert_has_calls([ + call(['python3', '-c', 'import urllib.request; ' + + 'urllib.request.urlopen(' + + '"http://start.ubuntu.com/connectivity-check.html"' + + ', timeout=5)']), + call(['apt-get', 'update']), + call(['snapcraft', 'snap', '--output', + 'snap-test_1.0_amd64.snap'], cwd=project_folder), + ]) + + @mock.patch('snapcraft.internal.lxd.Containerbuild._container_run') + @mock.patch('os.getuid') + def test_snap_containerized_exists_running(self, + mock_getuid, + mock_container_run): + self.useFixture( + fixtures.EnvironmentVariable('SUDO_UID', self.SUDO_UID)) + mock_getuid.return_value = self.getuid + fake_lxd = fixture_setup.FakeLXD() + self.useFixture(fake_lxd) + # Container was created before and is running + fake_lxd.name = 'local:snapcraft-snap-test' + fake_lxd.status = 'Running' + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_CONTAINER_BUILDS', '1')) + self.make_snapcraft_yaml() + + self.run_command(['snap']) + + source = os.path.realpath(os.path.curdir) + project_folder = '/root/build_snap-test' + fake_lxd.check_call_mock.assert_has_calls([ + call(['lxc', 'config', 'device', 'add', fake_lxd.name, + project_folder, 'disk', 'source={}'.format(source), + 'path={}'.format(project_folder)]), + call(['lxc', 'stop', '-f', fake_lxd.name]), + ]) + mock_container_run.assert_has_calls([ + call(['python3', '-c', 'import urllib.request; ' + + 'urllib.request.urlopen(' + + '"http://start.ubuntu.com/connectivity-check.html"' + + ', timeout=5)']), + call(['snapcraft', 'snap', '--output', + 'snap-test_1.0_amd64.snap'], + cwd=project_folder), + ]) + + @mock.patch('os.getuid') + @mock.patch('snapcraft.internal.lxd.Containerbuild._container_run') + @mock.patch('snapcraft.internal.lxd.Containerbuild._inject_snapcraft') + def test_snap_containerized_exists_stopped(self, + mock_inject, + mock_container_run, + mock_getuid): + + self.useFixture( + fixtures.EnvironmentVariable('SUDO_UID', self.SUDO_UID)) + mock_getuid.return_value = self.getuid + mock_container_run.side_effect = lambda cmd, **kwargs: cmd + fake_lxd = fixture_setup.FakeLXD() + self.useFixture(fake_lxd) + # Container was created before, and isn't running + fake_lxd.devices = '{"/root/build_snap-test":[]}' + fake_lxd.name = 'local:snapcraft-snap-test' + fake_lxd.status = 'Stopped' + fake_logger = fixtures.FakeLogger(level=logging.INFO) + self.useFixture(fake_logger) + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_CONTAINER_BUILDS', '1')) + self.make_snapcraft_yaml() + + result = self.run_command(['snap']) + + self.assertThat(result.exit_code, Equals(0)) + + source = os.path.realpath(os.path.curdir) + self.assertIn( + 'Waiting for a network connection...\n' + 'Network connection established\n' + 'Mounting {} into container\n'.format(source), + fake_logger.output) + + container_name = 'local:snapcraft-snap-test' + project_folder = '/root/build_snap-test' + fake_lxd.check_call_mock.assert_has_calls([ + call(['lxc', 'config', 'set', container_name, + 'environment.SNAPCRAFT_SETUP_CORE', '1']), + call(['lxc', 'config', 'set', container_name, + 'environment.LC_ALL', 'C.UTF-8']), + call(['lxc', 'config', 'set', container_name, + 'environment.SNAPCRAFT_IMAGE_INFO', + '{"fingerprint": "test-fingerprint", ' + '"architecture": "test-architecture", ' + '"created_at": "test-created-at"}']), + call(['lxc', 'config', 'set', container_name, + 'raw.idmap', 'both {} 0'.format(self.expected_idmap)]), + call(['lxc', 'config', 'device', 'remove', container_name, + project_folder]), + call(['lxc', 'config', 'device', 'add', container_name, + 'fuse', 'unix-char', 'path=/dev/fuse']), + call(['lxc', 'start', container_name]), + call(['lxc', 'stop', '-f', container_name]), + ]) + mock_container_run.assert_has_calls([ + call(['python3', '-c', 'import urllib.request; ' + + 'urllib.request.urlopen(' + + '"http://start.ubuntu.com/connectivity-check.html"' + + ', timeout=5)']), + call(['snapcraft', 'snap', '--output', + 'snap-test_1.0_amd64.snap'], + cwd=project_folder), + ]) + # Ensure there's no unexpected calls eg. two network checks + self.assertThat(mock_container_run.call_count, Equals(2)) + + class SnapCommandAsDefaultTestCase(SnapCommandBaseTestCase): scenarios = [ diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_update.py snapcraft-2.35/snapcraft/tests/commands/test_update.py --- snapcraft-2.34/snapcraft/tests/commands/test_update.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_update.py 2017-11-01 19:41:33.000000000 +0000 @@ -20,13 +20,32 @@ import yaml from testtools.matchers import Contains, Equals, FileExists from xdg import BaseDirectory +from textwrap import dedent +from unittest import mock +from unittest.mock import call from snapcraft.tests import TestWithFakeRemoteParts +from snapcraft.tests import fixture_setup from . import CommandBaseTestCase class UpdateCommandTestCase(CommandBaseTestCase, TestWithFakeRemoteParts): + yaml_template = dedent("""\ + name: snap-test + version: 1.0 + summary: test snapping + description: if snap is succesful a snap package will be available + architectures: ['amd64'] + type: app + confinement: strict + grade: stable + + parts: + part1: + plugin: nil + """) + def setUp(self): super().setUp() self.parts_dir = os.path.join(BaseDirectory.xdg_data_home, 'snapcraft') @@ -106,3 +125,27 @@ self.assertThat(result.exit_code, Equals(0)) self.assertThat(self.parts_yaml, FileExists()) self.assertThat(self.headers_yaml, FileExists()) + + @mock.patch('snapcraft.internal.lxd.Containerbuild._container_run') + @mock.patch('os.getuid') + def test_update_containerized_exists_running(self, + mock_getuid, + mock_container_run): + mock_container_run.side_effect = lambda cmd, **kwargs: cmd + mock_getuid.return_value = 1234 + fake_lxd = fixture_setup.FakeLXD() + self.useFixture(fake_lxd) + # Container was created before and is running + fake_lxd.name = 'local:snapcraft-snap-test' + fake_lxd.status = 'Running' + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_CONTAINER_BUILDS', '1')) + self.make_snapcraft_yaml(self.yaml_template) + + result = self.run_command(['update']) + self.assertThat(result.exit_code, Equals(0)) + + project_folder = '/root/build_snap-test' + mock_container_run.assert_has_calls([ + call(['snapcraft', 'update'], cwd=project_folder), + ]) diff -Nru snapcraft-2.34/snapcraft/tests/commands/test_validate.py snapcraft-2.35/snapcraft/tests/commands/test_validate.py --- snapcraft-2.34/snapcraft/tests/commands/test_validate.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/commands/test_validate.py 2017-11-01 19:41:33.000000000 +0000 @@ -40,11 +40,11 @@ self.client.login('dummy', 'test correct password') result = self.run_command([ - 'validate', 'ubuntu-core', 'ubuntu-core=3', 'test-snap=4']) + 'validate', 'core', 'core=3', 'test-snap=4']) self.assertThat(result.exit_code, Equals(0)) self.assertThat(result.output, Contains( - 'Signing validations assertion for ubuntu-core=3')) + 'Signing validations assertion for core=3')) self.assertThat(result.output, Contains( 'Signing validations assertion for test-snap=4')) @@ -52,12 +52,12 @@ self.client.login('dummy', 'test correct password') result = self.run_command([ - 'validate', 'ubuntu-core', 'ubuntu-core=3', + 'validate', 'core', 'core=3', 'test-snap=4', '--key-name=keyname']) self.assertThat(result.exit_code, Equals(0)) self.assertThat(result.output, Contains( - 'Signing validations assertion for ubuntu-core=3')) + 'Signing validations assertion for core=3')) self.assertThat(result.output, Contains( 'Signing validations assertion for test-snap=4')) self.popen_mock.assert_called_with(['snap', 'sign', '-k', 'keyname'], @@ -72,7 +72,7 @@ 'SNAPCRAFT_UBUNTU_STORE', 'Test-Branded')) result = self.run_command( - ['validate', 'ubuntu-core', 'test-snap-branded-store=1']) + ['validate', 'core', 'test-snap-branded-store=1']) self.assertThat(result.exit_code, Equals(0)) self.assertThat(result.output, Contains( @@ -84,7 +84,7 @@ raised = self.assertRaises( snapcraft.storeapi.errors.SnapNotFoundError, self.run_command, - ['validate', 'notfound', 'ubuntu-core=3', 'test-snap=4']) + ['validate', 'notfound', 'core=3', 'test-snap=4']) self.assertThat(str(raised), Equals("Snap 'notfound' was not found.")) @@ -94,7 +94,7 @@ raised = self.assertRaises( snapcraft.storeapi.errors.InvalidValidationRequestsError, self.run_command, - ['validate', 'ubuntu-core', 'ubuntu-core=foo']) + ['validate', 'core', 'core=foo']) self.assertThat(str(raised), Contains('format must be name=revision')) @@ -102,6 +102,6 @@ raised = self.assertRaises( snapcraft.storeapi.errors.InvalidCredentialsError, self.run_command, - ['validate', 'ubuntu-core', 'ubuntu-core=3', 'test-snap=4']) + ['validate', 'core', 'core=3', 'test-snap=4']) self.assertThat(str(raised), Contains('Invalid credentials')) diff -Nru snapcraft-2.34/snapcraft/tests/fake_servers/api.py snapcraft-2.35/snapcraft/tests/fake_servers/api.py --- snapcraft-2.34/snapcraft/tests/fake_servers/api.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/fake_servers/api.py 2017-11-01 19:41:33.000000000 +0000 @@ -442,6 +442,15 @@ } } }).encode() + elif 'notanumber' in revision: + response_code = 400 + payload = json.dumps({ + 'success': False, + 'error_list': [{ + 'code': 'invalid-field', + 'message': "The 'revision' field must be an integer"}], + 'errors': {'revision': ['This field must be an integer.']}} + ).encode() else: raise NotImplementedError( 'Cannot handle release request for {!r}'.format(name)) @@ -620,15 +629,21 @@ 'status': 'Approved', 'private': False, 'price': None, 'since': '2016-12-12T01:01:01Z'}, - 'ubuntu-core': {'snap-id': 'good', 'status': 'Approved', - 'private': False, 'price': None, - 'since': '2016-12-12T01:01:01Z'}, + 'core': {'snap-id': 'good', 'status': 'Approved', + 'private': False, 'price': None, + 'since': '2016-12-12T01:01:01Z'}, 'core-no-dev': {'snap-id': 'no-dev', 'status': 'Approved', 'private': False, 'price': None, 'since': '2016-12-12T01:01:01Z'}, 'badrequest': {'snap-id': 'badrequest', 'status': 'Approved', 'private': False, 'price': None, 'since': '2016-12-12T01:01:01Z'}, + 'revoked': {'snap-id': 'revoked', 'status': 'Approved', + 'private': False, 'price': None, + 'since': '2016-12-12T01:01:01Z'}, + 'no-revoked': {'snap-id': 'no-revoked', 'status': 'Approved', + 'private': False, 'price': None, + 'since': '2016-12-12T01:01:01Z'}, } snaps.update({ name: {'snap-id': 'fake-snap-id', 'status': 'Approved', @@ -817,13 +832,13 @@ if snap_id == 'good': payload = json.dumps({'snap_developer': {}}).encode() response_code = 200 - elif snap_id == 'test-snap-id-with-dev': + elif snap_id in ('test-snap-id-with-dev', 'revoked', 'no-revoked'): payload = json.dumps({ 'snap_developer': { 'type': 'snap-developer', 'authority-id': 'dummy', 'publisher-id': 'dummy', - 'snap-id': 'test-snap-id-with-dev', + 'snap-id': snap_id, 'developers': [{ 'developer-id': 'test-dev-id', 'since': '2017-02-10T08:35:00.390258Z', @@ -866,7 +881,7 @@ def put_snap_developers(self, request): snap_id = request.matchdict['snap_id'] - if snap_id == 'good': + if snap_id in ('good', 'test-snap-id-with-dev'): payload = request.body response_code = 200 elif snap_id == 'no-dev': @@ -874,10 +889,24 @@ response_code = 200 elif snap_id == 'badrequest': payload = json.dumps({'error_list': [ - {'message': "The given `snap-id` does not match the " - "assertion's.", + {'message': 'The given `snap-id` does not match the ' + 'assertion.', 'code': 'invalid-request'}]}).encode() response_code = 400 + elif snap_id == 'revoked': + payload = json.dumps({'error_list': [ + {'message': "The assertion's `developers` would revoke " + "existing uploads.", + 'code': 'revoked-uploads', + 'extra': ['this']}]}).encode() + response_code = 409 + elif snap_id == 'no-revoked': + payload = json.dumps({'error_list': [ + {'message': "The collaborators for this snap haven't been " + "altered. Exiting... ", + 'code': 'revoked-uploads', + 'extra': ['this']}]}).encode() + response_code = 409 content_type = 'application/json' return response.Response( payload, response_code, [('Content-Type', content_type)]) diff -Nru snapcraft-2.34/snapcraft/tests/fake_servers/search.py snapcraft-2.35/snapcraft/tests/fake_servers/search.py --- snapcraft-2.34/snapcraft/tests/fake_servers/search.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/fake_servers/search.py 2017-11-01 19:41:33.000000000 +0000 @@ -66,7 +66,7 @@ payload, response_code, [('Content-Type', content_type)]) def _get_details_payload(self, request): - # ubuntu-core is used in integration tests with fake servers. + # core snap is used in integration tests with fake servers. snap = request.matchdict['snap'] # sha512sum snapcraft/tests/data/test-snap.snap test_sha512 = ( @@ -74,7 +74,7 @@ 'd22a956457f14146f7f067b47bd976cf0292f2993ad864ccb498b' 'fda4128234e4c201f28fe9') - if snap in ('test-snap', 'ubuntu-core'): + if snap in ('test-snap', 'core'): sha512 = test_sha512 elif snap == 'test-snap-with-wrong-sha': sha512 = 'wrong sha' diff -Nru snapcraft-2.34/snapcraft/tests/fake_servers/snapd.py snapcraft-2.35/snapcraft/tests/fake_servers/snapd.py --- snapcraft-2.34/snapcraft/tests/fake_servers/snapd.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/fake_servers/snapd.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,64 +14,74 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . import json -from http.server import BaseHTTPRequestHandler from urllib import parse +from snapcraft.tests import fake_servers -class FakeSnapdServer(BaseHTTPRequestHandler): + +class FakeSnapdRequestHandler(fake_servers.BaseHTTPRequestHandler): + + snaps_result = [] + snap_details_func = None + find_result = [] + _private_data = {'new_fake_snap_installed': False} def do_GET(self): parsed_url = parse.urlparse(self.path) - if parsed_url.path.startswith('/v2/snaps/'): - self._handle_snaps(parsed_url) + if parsed_url.path == '/v2/snaps': + self._handle_snaps() + elif parsed_url.path.startswith('/v2/snaps/'): + self._handle_snap_details(parsed_url) elif parsed_url.path == '/v2/find': self._handle_find(parsed_url) else: self.wfile.write(parsed_url.path.encode()) - def _handle_snaps(self, parsed_url): - status_code = 404 - params = {} + def _handle_snaps(self): + status_code = 200 + params = self.snaps_result + self.send_response(status_code) + self.send_header('Content-Type', 'text/application+json') + self.end_headers() + response = json.dumps({'result': params}).encode() + self.wfile.write(response) - if parsed_url.path.endswith('/fake-snap'): - status_code = 200 - params = {'channel': 'stable'} - elif parsed_url.path.endswith('/fake-snap-stable'): - status_code = 200 - params = {'channel': 'stable'} - elif parsed_url.path.endswith('/fake-snap-branch'): - status_code = 200 - params = {'channel': 'candidate/branch'} - elif parsed_url.path.endswith('/fake-snap-track-stable'): - status_code = 200 - params = {'channel': 'track/stable'} - elif parsed_url.path.endswith('/fake-snap-track-stable-branch'): - status_code = 200 - params = {'channel': 'track/stable/branch'} - elif parsed_url.path.endswith('/fake-snap-edge'): - status_code = 200 - params = {'channel': 'edge'} + def _handle_snap_details(self, parsed_url): + status_code = 404 + params = {'message': 'not found'} + type_ = 'error' + snap_name = parsed_url.path.split('/')[-1] + if self.snap_details_func: + status_code, params = self.snap_details_func(snap_name) + else: + for snap in self.snaps_result: + if snap['name'] == snap_name: + status_code = 200 + type_ = 'sync' + params = {} + for key in ('channel', 'revision', 'confinement', 'id'): + if key in snap: + params.update({key: snap[key]}) + break self.send_response(status_code) self.send_header('Content-Type', 'text/application+json') self.end_headers() - response = json.dumps({'result': params}).encode() + response = json.dumps({'result': params, 'type': type_}).encode() + self.wfile.write(response) def _handle_find(self, parsed_url): query = parse.parse_qs(parsed_url.query) + snap_name = query['name'][0] status_code = 404 params = {} - - if query['name'][0] == 'fake-snap': - status_code = 200 - params = {'channels': { - 'latest/stable': {'confinement': 'strict'}, - 'classic/stable': {'confinement': 'classic'}, - 'strict/stable': {'confinement': 'strict'}, - 'devmode/stable': {'confinement': 'devmode'}, - }} - if query['name'][0] == 'new-fake-snap': + for result in self.find_result: + if snap_name in result: + status_code = 200 + params = result[snap_name] + break + if snap_name == 'new-fake-snap': status_code = 200 params = {'channels': { 'latest/stable': {'confinement': 'strict'}, diff -Nru snapcraft-2.34/snapcraft/tests/fixture_setup.py snapcraft-2.35/snapcraft/tests/fixture_setup.py --- snapcraft-2.34/snapcraft/tests/fixture_setup.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/fixture_setup.py 2017-11-01 19:41:33.000000000 +0000 @@ -19,8 +19,11 @@ import copy import io import os +import socketserver import string +import subprocess import sys +import tempfile import threading import urllib.parse import uuid @@ -31,12 +34,14 @@ import fixtures import xdg +import yaml import snapcraft from snapcraft.tests import fake_servers from snapcraft.tests.fake_servers import ( api, search, + snapd, upload ) from snapcraft.tests.subprocess_utils import ( @@ -47,6 +52,11 @@ class TempCWD(fixtures.TempDir): + def __init__(self, rootdir=None): + if rootdir is None and 'TMPDIR' in os.environ: + rootdir = os.environ.get('TMPDIR') + super().__init__(rootdir) + def setUp(self): """Create a temporary directory an cd into it for the test duration.""" super().setUp() @@ -126,7 +136,8 @@ def setUp(self): super().setUp() - patcher = mock.patch('snapcraft.internal.lifecycle.ProgressBar') + patcher = mock.patch( + 'snapcraft.internal.lifecycle._packer.ProgressBar') patcher.start() self.addCleanup(patcher.stop) @@ -360,7 +371,7 @@ super().setUp() self.useFixture(fixtures.EnvironmentVariable( 'UBUNTU_STORE_API_ROOT_URL', - 'https://myapps.developer.staging.ubuntu.com/dev/api/')) + 'https://dashboard.staging.snapcraft.io/dev/api/')) self.useFixture(fixtures.EnvironmentVariable( 'UBUNTU_STORE_UPLOAD_ROOT_URL', 'https://upload.apps.staging.ubuntu.com/')) @@ -369,7 +380,7 @@ 'https://login.staging.ubuntu.com/api/v2/')) self.useFixture(fixtures.EnvironmentVariable( 'UBUNTU_STORE_SEARCH_ROOT_URL', - 'https://search.apps.staging.ubuntu.com/')) + 'https://api.staging.snapcraft.io/')) class TestStore(fixtures.Fixture): @@ -441,7 +452,8 @@ self.mkdtemp_mock.side_effect = self.mkdtemp_side_effect() self.addCleanup(patcher.stop) - patcher = mock.patch('snapcraft.internal.lxd.open', mock.mock_open()) + patcher = mock.patch( + 'snapcraft.internal.lxd._containerbuild.open', mock.mock_open()) self.open_mock = patcher.start() self.open_mock_default_side_effect = self.open_mock.side_effect self.open_mock.side_effect = self.open_side_effect() @@ -493,23 +505,29 @@ class FakeLXD(fixtures.Fixture): '''...''' - def __init__(self, fail_on_snapcraft_run=False): + def __init__(self): self.status = None + self.files = [] + self.kernel_arch = 'x86_64' self.devices = '{}' - self.fail_on_snapcraft_run = fail_on_snapcraft_run def _setUp(self): - patcher = mock.patch('snapcraft.internal.lxd.check_call') + patcher = mock.patch('subprocess.check_call') self.check_call_mock = patcher.start() self.check_call_mock.side_effect = self.check_output_side_effect() self.addCleanup(patcher.stop) - patcher = mock.patch('snapcraft.internal.lxd.check_output') + patcher = mock.patch('subprocess.check_output') self.check_output_mock = patcher.start() self.check_output_mock.side_effect = self.check_output_side_effect() self.addCleanup(patcher.stop) - patcher = mock.patch('snapcraft.internal.lxd.sleep', lambda _: None) + patcher = mock.patch('subprocess.Popen') + self.popen_mock = patcher.start() + self.popen_mock.side_effect = self.check_output_side_effect() + self.addCleanup(patcher.stop) + + patcher = mock.patch('time.sleep', lambda _: None) patcher.start() self.addCleanup(patcher.stop) @@ -522,86 +540,82 @@ self.architecture_mock.return_value = ('64bit', 'ELF') self.addCleanup(patcher.stop) - def check_output_side_effect(self): - def call_effect(*args, **kwargs): - if args[0] == ['lxc', 'remote', 'get-default']: - return 'local'.encode('utf-8') - elif args[0][:2] == ['lxc', 'info']: - return ''' - environment: - kernel_architecture: x86_64 - '''.encode('utf-8') - elif args[0][:3] == ['lxc', 'list', '--format=json']: - if self.status and args[0][3] == self.name: - return string.Template(''' - [{"name": "$NAME", - "status": "$STATUS", - "devices": $DEVICES}] - ''').substitute({ - # Container name without remote prefix - 'NAME': self.name.split(':')[-1], - 'STATUS': self.status, - 'DEVICES': self.devices, - }).encode('utf-8') - return '[]'.encode('utf-8') - elif args[0][:2] == ['lxc', 'init']: - self.name = args[0][3] - self.status = 'Stopped' - elif args[0][:2] == ['lxc', 'launch']: - self.name = args[0][4] - self.status = 'Running' - elif args[0][:2] == ['lxc', 'stop'] and not self.status: - # error: not found - raise CalledProcessError(returncode=1, cmd=args[0]) - # Fail on an actual snapcraft command and not the command - # for the installation of it. - elif ('snapcraft snap' in ' '.join(args[0]) - and self.fail_on_snapcraft_run): - raise CalledProcessError(returncode=255, cmd=args[0]) - else: - return ''.encode('utf-8') - return call_effect - - -class FakeSnapd(fixtures.Fixture): - '''...''' + def call_effect(self, *args, **kwargs): + if args[0] == ['lxc', 'remote', 'get-default']: + return 'local'.encode('utf-8') + elif args[0][:2] == ['lxc', 'info']: + return ''' + environment: + kernel_architecture: {} + '''.format(self.kernel_arch).encode('utf-8') + elif args[0][:3] == ['lxc', 'list', '--format=json']: + if self.status and args[0][3] == self.name: + return string.Template(''' + [{"name": "$NAME", + "status": "$STATUS", + "devices": $DEVICES}] + ''').substitute({ + # Container name without remote prefix + 'NAME': self.name.split(':')[-1], + 'STATUS': self.status, + 'DEVICES': self.devices, + }).encode('utf-8') + return '[]'.encode('utf-8') + elif (args[0][0] == 'lxc' and + args[0][1] in ['init', 'start', 'launch', 'stop']): + return self._lxc_create_start_stop(args) + elif args[0][:2] == ['lxc', 'exec']: + return self._lxc_exec(args) + elif args[0][:4] == ['lxc', 'image', 'list', '--format=json']: + return ( + '[{"architecture":"test-architecture",' + '"fingerprint":"test-fingerprint",' + '"created_at":"test-created-at"}]').encode('utf-8') + elif args[0][0] == 'sha384sum': + return 'deadbeef {}'.format(args[0][1]).encode('utf-8') + elif '/usr/lib/sftp-server' in args[0]: + return self._popen(args[0]) + else: + return ''.encode('utf-8') - def __init__(self): - self.snaps = { - 'core': {'confinement': 'strict', - 'id': '2kkitQurgOkL3foImG4wDwn9CIANuHlt', - 'revision': '123'}, - 'snapcraft': {'confinement': 'classic', - 'id': '3lljuRvshPlM4gpJnH5xExo0DJBOvImu', - 'revision': '345'}, - } + def check_output_side_effect(self): + return self.call_effect - def _setUp(self): - patcher = mock.patch('requests_unixsocket.Session.request') - self.session_request_mock = patcher.start() - self.session_request_mock.side_effect = self.request_side_effect() - self.addCleanup(patcher.stop) - - def request_side_effect(self): - def request_effect(*args, **kwargs): - if args[0] == 'GET' and '/v2/snaps/' in args[1]: - class Session: - def __init__(self, name, snaps): - self._name = name - self._snaps = snaps - - def json(self): - if self._name not in self._snaps: - return {'status': 'Not Found', - 'result': {'message': 'not found'}, - 'status-code': 404, - 'type': 'error'} - return {'status': 'OK', - 'type': 'sync', - 'result': self._snaps[self._name]} - name = args[1].split('/')[-1] - return Session(name, self.snaps) - return request_effect + def _lxc_create_start_stop(self, args): + if args[0][1] == 'init': + self.name = args[0][3] + self.status = 'Stopped' + elif args[0][1] == 'launch': + self.name = args[0][4] + self.status = 'Running' + elif args[0][1] == 'start' and self.name == args[0][2]: + self.status = 'Running' + elif args[0][1] == 'stop' and not self.status: + # error: not found + raise CalledProcessError(returncode=1, cmd=args[0]) + + def _lxc_exec(self, args): + if self.status and args[0][2] == self.name: + cmd = args[0][4] + if cmd == 'ls': + return ' '.join(self.files).encode('utf-8') + elif cmd == 'readlink': + if args[0][-1].endswith('/current'): + raise CalledProcessError(returncode=1, cmd=cmd) + elif cmd == 'sshfs': + self.files = ['foo', 'bar'] + return self._popen(args[0]) + elif 'sha384sum' in args[0][-1]: + raise CalledProcessError(returncode=1, cmd=cmd) + + def _popen(self, args): + class Popen: + def __init__(self, args): + self.args = args + + def terminate(self): + pass + return Popen(args) class GitRepo(fixtures.Fixture): @@ -811,8 +825,7 @@ self.cache = self.Cache() self.mock_apt_cache.return_value = self.cache for package, version in self.packages: - self.cache[package] = FakeAptCachePackage( - self.path, package, version) + self.add_package(FakeAptCachePackage(package, version)) # Add all the packages in the manifest. with open(os.path.abspath( @@ -821,16 +834,20 @@ 'internal', 'repo', 'manifest.txt'))) as manifest_file: self.add_packages([line.strip() for line in manifest_file]) + def add_package(self, package): + package.temp_dir = self.path + self.cache[package.name] = package + def add_packages(self, package_names): for name in package_names: - self.cache[name] = FakeAptCachePackage(self.path, name) + self.cache[name] = FakeAptCachePackage(name) class FakeAptCachePackage(): def __init__( - self, temp_dir, name, version=None, - provides=None, installed=False, + self, name, version=None, installed=None, + temp_dir=None, provides=None, priority='non-essential'): super().__init__() self.temp_dir = temp_dir @@ -839,9 +856,14 @@ self.versions = {} self.version = version self.candidate = self - self.installed = version self.provides = provides if provides else [] - self.installed = installed + if installed: + # XXX The installed attribute requires some values that the fake + # package also requires. The shortest path to do it that I found + # was to get installed to return the same fake package. + self.installed = self + else: + self.installed = None self.priority = priority self.marked_install = False @@ -875,3 +897,130 @@ def get_dependencies(self, _): return [] + + +class WithoutSnapInstalled(fixtures.Fixture): + """Assert that a snap is not installed and remove it on clean up. + + :raises: AssertionError: if the snap is installed when this fixture is + set up. + """ + + def __init__(self, snap_name): + super().__init__() + self.snap_name = snap_name.split('/')[0] + + def setUp(self): + super().setUp() + if snapcraft.repo.snaps.SnapPackage.is_snap_installed(self.snap_name): + raise AssertionError( + "This test cannot run if you already have the {snap!r} snap " + "installed. Please uninstall it by running " + "'sudo snap remove {snap}'.".format(snap=self.snap_name)) + + self.addCleanup(self._remove_snap) + + def _remove_snap(self): + try: + subprocess.check_output( + ['sudo', 'snap', 'remove', self.snap_name], + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + RuntimeError("unable to remove {!r}: {}".format( + self.snap_name, e.output)) + + +class SnapcraftYaml(fixtures.Fixture): + + def __init__( + self, path, name='test-snap', version='test-version', + summary='test-summary', description='test-description'): + super().__init__() + self.path = path + self.data = { + 'name': name, + 'version': version, + 'summary': summary, + 'description': description, + 'parts': {} + } + + def update_part(self, name, data): + part = {name: data} + self.data['parts'].update(part) + + def setUp(self): + super().setUp() + with open(os.path.join(self.path, 'snapcraft.yaml'), + 'w') as snapcraft_yaml_file: + yaml.dump(self.data, snapcraft_yaml_file) + + +class UnixHTTPServer(socketserver.UnixStreamServer): + + def get_request(self): + request, client_address = self.socket.accept() + # BaseHTTPRequestHandler expects a tuple with the client address at + # index 0, so we fake one + if len(client_address) == 0: + client_address = (self.server_address,) + return (request, client_address) + + +class FakeSnapd(fixtures.Fixture): + + @property + def snaps_result(self): + self.request_handler.snaps_result + + @snaps_result.setter + def snaps_result(self, value): + self.request_handler.snaps_result = value + + @property + def snap_details_func(self): + self.request_handler.snap_details_func + + @snap_details_func.setter + def snap_details_func(self, value): + self.request_handler.snap_details_func = value + + @property + def find_result(self): + self.request_handler.find_result + + @find_result.setter + def find_result(self, value): + self.request_handler.find_result = value + + def __init__(self): + super().__init__() + self.request_handler = snapd.FakeSnapdRequestHandler + self.snaps_result = [] + self.find_result = [] + self.snap_details_func = None + + def setUp(self): + super().setUp() + snapd_fake_socket_path = tempfile.mkstemp()[1] + os.unlink(snapd_fake_socket_path) + + socket_path_patcher = mock.patch( + 'snapcraft.internal.repo.snaps.get_snapd_socket_path_template') + mock_socket_path = socket_path_patcher.start() + mock_socket_path.return_value = 'http+unix://{}/v2/{{}}'.format( + snapd_fake_socket_path.replace('/', '%2F')) + self.addCleanup(socket_path_patcher.stop) + + self._start_fake_server(snapd_fake_socket_path) + + def _start_fake_server(self, socket): + self.server = UnixHTTPServer(socket, self.request_handler) + server_thread = threading.Thread(target=self.server.serve_forever) + server_thread.start() + self.addCleanup(self._stop_fake_server, server_thread) + + def _stop_fake_server(self, thread): + self.server.shutdown() + self.server.socket.close() + thread.join() diff -Nru snapcraft-2.34/snapcraft/tests/__init__.py snapcraft-2.35/snapcraft/tests/__init__.py --- snapcraft-2.34/snapcraft/tests/__init__.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/__init__.py 2017-11-01 19:41:33.000000000 +0000 @@ -27,7 +27,10 @@ import testtools import snapcraft -from snapcraft.internal import common +from snapcraft.internal import ( + common, + log, +) from snapcraft.tests import fake_servers, fixture_setup from snapcraft.internal.project_loader import grammar_processing @@ -148,6 +151,9 @@ self.mock_machine.return_value = machine self.addCleanup(patcher.stop) + # Ensure logging is set back to the default + self.addCleanup(log.configure) + def make_snapcraft_yaml(self, content, encoding='utf-8'): with contextlib.suppress(FileExistsError): os.mkdir('snap') diff -Nru snapcraft-2.34/snapcraft/tests/pluginhandler/test_pluginhandler.py snapcraft-2.35/snapcraft/tests/pluginhandler/test_pluginhandler.py --- snapcraft-2.34/snapcraft/tests/pluginhandler/test_pluginhandler.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/pluginhandler/test_pluginhandler.py 2017-11-01 19:41:33.000000000 +0000 @@ -869,9 +869,10 @@ self.assertTrue(state, 'Expected build to save state YAML') self.assertTrue(type(state) is states.BuildState) self.assertTrue(type(state.properties) is OrderedDict) - self.assertThat(len(state.properties), Equals(5)) + self.assertThat(len(state.properties), Equals(8)) for expected in ['after', 'build-attributes', 'build-packages', - 'disable-parallel', 'organize']: + 'disable-parallel', 'organize', 'prepare', 'build', + 'install']: self.assertTrue(expected in state.properties) self.assertTrue(type(state.project_options) is OrderedDict) self.assertTrue('deb_arch' in state.project_options) diff -Nru snapcraft-2.34/snapcraft/tests/pluginhandler/test_scriptlets.py snapcraft-2.35/snapcraft/tests/pluginhandler/test_scriptlets.py --- snapcraft-2.34/snapcraft/tests/pluginhandler/test_scriptlets.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/pluginhandler/test_scriptlets.py 2017-11-01 19:41:33.000000000 +0000 @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright (C) 2016 Canonical Ltd +# Copyright (C) 2016-2017 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 @@ -15,6 +15,8 @@ # along with this program. If not, see . import os +from subprocess import CalledProcessError +from textwrap import dedent from testtools.matchers import FileExists @@ -42,3 +44,23 @@ after_build_file_path = os.path.join(handler.plugin.build_basedir, 'after-build') self.assertThat(after_build_file_path, FileExists()) + + def test_failure_on_last_script_command_results_in_failure(self): + script = dedent("""\ + echo success + false # this should trigger an error + """) + handler = self.load_part( + 'test-part', part_properties={'prepare': script}) + + self.assertRaises(CalledProcessError, handler.build) + + def test_failure_to_execute_mid_script_results_in_failure(self): + script = dedent("""\ + false # this should trigger an error + echo success + """) + handler = self.load_part( + 'test-part', part_properties={'prepare': script}) + + self.assertRaises(CalledProcessError, handler.build) diff -Nru snapcraft-2.34/snapcraft/tests/plugins/python/__init__.py snapcraft-2.35/snapcraft/tests/plugins/python/__init__.py --- snapcraft-2.34/snapcraft/tests/plugins/python/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/python/__init__.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,29 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os + +import snapcraft + + +class PythonBaseTestCase(snapcraft.tests.TestCase): + + def _create_python_binary(self, base_dir): + python_command_path = os.path.join( + base_dir, 'usr', 'bin', 'pythontest') + os.makedirs(os.path.dirname(python_command_path), exist_ok=True) + open(python_command_path, 'w').close() + return python_command_path diff -Nru snapcraft-2.34/snapcraft/tests/plugins/python/test_pip.py snapcraft-2.35/snapcraft/tests/plugins/python/test_pip.py --- snapcraft-2.34/snapcraft/tests/plugins/python/test_pip.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/python/test_pip.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,604 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os +import subprocess + +import fixtures +from unittest import mock + +from testtools.matchers import ( + Contains, + Equals, + HasLength, +) + +from snapcraft.plugins._python import ( + _pip, + errors, +) + +from . import PythonBaseTestCase + + +class PipRunTestCase(PythonBaseTestCase): + + def setUp(self): + super().setUp() + + patcher = mock.patch('snapcraft.internal.common.run_output') + self.mock_run_output = patcher.start() + self.addCleanup(patcher.stop) + + def _assert_expected_enviroment(self, expected_python, headers_path): + _pip.Pip( + python_major_version='test', + part_dir='part_dir', + install_dir='install_dir', + stage_dir='stage_dir').setup() + + class check_env(): + def __init__(self, test): + self.test = test + + def __eq__(self, env): + self.test.assertThat(env, Contains('PYTHONUSERBASE')) + self.test.assertThat( + env['PYTHONUSERBASE'], Equals('install_dir')) + + self.test.assertThat(env, Contains('PYTHONHOME')) + if expected_python.startswith('install_dir'): + self.test.assertThat( + env['PYTHONHOME'], Equals(os.path.join( + 'install_dir', 'usr'))) + else: + self.test.assertThat( + env['PYTHONHOME'], Equals(os.path.join( + 'stage_dir', 'usr'))) + + self.test.assertThat(env, Contains('PATH')) + self.test.assertThat( + env['PATH'], Contains(os.path.join( + 'install_dir', 'usr', 'bin'))) + + if headers_path: + self.test.assertThat(env, Contains('CPPFLAGS')) + self.test.assertThat( + env['CPPFLAGS'], Contains('-I{}'.format(headers_path))) + + return True + + self.mock_run_output.assert_called_once_with( + [expected_python, '-m', 'pip'], env=check_env(self), + stderr=subprocess.STDOUT) + + def test_environment_part_python_without_headers(self): + expected_python = self._create_python_binary('install_dir') + self._assert_expected_enviroment(expected_python, None) + + def test_environment_part_python_with_staged_headers(self): + expected_python = self._create_python_binary('install_dir') + # First, create staged headers + staged_headers = os.path.join( + 'stage_dir', 'usr', 'include', 'pythontest') + os.makedirs(staged_headers) + self._assert_expected_enviroment(expected_python, staged_headers) + + @mock.patch('glob.glob') + def test_environment_part_python_with_host_headers(self, mock_glob): + host_headers = os.path.join(os.sep, 'usr', 'include', 'pythontest') + + # Fake out glob so it looks like the headers are installed on the host + def _fake_glob(pattern): + if pattern.startswith(os.sep): + return [host_headers] + return [] + mock_glob.side_effect = _fake_glob + + expected_python = self._create_python_binary('install_dir') + self._assert_expected_enviroment(expected_python, host_headers) + + def test_environment_staged_python_without_headers(self): + expected_python = self._create_python_binary('stage_dir') + self._assert_expected_enviroment(expected_python, None) + + def test_environment_staged_python_with_staged_headers(self): + # First, create staged headers + staged_headers = os.path.join( + 'stage_dir', 'usr', 'include', 'pythontest') + os.makedirs(staged_headers) + + # Also create staged python + expected_python = self._create_python_binary('stage_dir') + + self._assert_expected_enviroment(expected_python, staged_headers) + + @mock.patch('glob.glob') + def test_environment_staged_python_with_host_headers(self, mock_glob): + host_headers = os.path.join(os.sep, 'usr', 'include', 'pythontest') + + # Fake out glob so it looks like the headers are installed on the host + def _fake_glob(pattern): + if pattern.startswith(os.sep): + return [host_headers] + return [] + mock_glob.side_effect = _fake_glob + + expected_python = self._create_python_binary('stage_dir') + self._assert_expected_enviroment(expected_python, host_headers) + + def test_with_extra_cppflags(self): + """Verify that existing CPPFLAGS are preserved""" + + expected_python = self._create_python_binary('install_dir') + + self.useFixture(fixtures.EnvironmentVariable( + 'CPPFLAGS', '-I/opt/include')) + _pip.Pip( + python_major_version='test', + part_dir='part_dir', + install_dir='install_dir', + stage_dir='stage_dir').setup() + + class check_env(): + def __init__(self, test): + self.test = test + + def __eq__(self, env): + self.test.assertThat(env, Contains('CPPFLAGS')) + self.test.assertThat( + env['CPPFLAGS'], Contains('-I/opt/include')) + + return True + + self.mock_run_output.assert_has_calls([ + mock.call([expected_python, '-m', 'pip'], env=check_env(self), + stderr=subprocess.STDOUT)]) + + +class SetupTestCase(PipRunTestCase): + + def setUp(self): + super().setUp() + + self.command = [self._create_python_binary('install_dir'), '-m', 'pip'] + + def test_setup_with_pip_installed(self): + """Test that no attempt is made to reinstall pip""" + + # Since _run doesn't raise an exception indicating pip isn't installed, + # it must be installed. + + # Verify that no attempt is made to reinstall pip + _pip.Pip( + python_major_version='test', + part_dir='part_dir', + install_dir='install_dir', + stage_dir='stage_dir').setup() + + self.mock_run_output.assert_called_once_with( + self.command, stderr=subprocess.STDOUT, env=mock.ANY) + + def test_setup_without_pip_installed(self): + """Test that the system pip is used to install our own pip""" + + # Raise an exception indicating that pip isn't installed + def fake_run(command, **kwargs): + if command == self.command: + raise subprocess.CalledProcessError( + 1, 'foo', b'no module named pip') + self.mock_run_output.side_effect = fake_run + + # Verify that pip is then installed + _pip.Pip( + python_major_version='test', + part_dir='part_dir', + install_dir='install_dir', + stage_dir='stage_dir').setup() + + part_pythonhome = os.path.join('install_dir', 'usr') + host_pythonhome = os.path.join(os.path.sep, 'usr') + + # What we're asserting here: + # 1. That we test for the installed pip + # 2. That we then download pip (and associated tools) using host pip + # 3. That we then install pip (and associated tools) using host pip + self.assertThat(self.mock_run_output.mock_calls, HasLength(3)) + self.mock_run_output.assert_has_calls([ + mock.call( + self.command, env=_CheckPythonhomeEnv(self, part_pythonhome), + stderr=subprocess.STDOUT), + mock.call( + _CheckCommand( + self, 'download', ['pip', 'setuptools', 'wheel'], []), + env=_CheckPythonhomeEnv(self, host_pythonhome), cwd=None), + mock.call( + _CheckCommand( + self, 'install', ['pip', 'setuptools', 'wheel'], + ['--ignore-installed']), + env=_CheckPythonhomeEnv(self, host_pythonhome), cwd=None), + ]) + + def test_setup_unexpected_error(self): + """Test that pip initialization doesn't eat legit errors""" + + # Raises an exception indicating something bad happened + self.mock_run_output.side_effect = subprocess.CalledProcessError( + 1, 'foo', b'no good, very bad') + + pip = _pip.Pip( + python_major_version='test', + part_dir='part_dir', + install_dir='install_dir', + stage_dir='stage_dir') + + # Verify that pip lets that exception through + self.assertRaises(subprocess.CalledProcessError, pip.setup) + + +class PipTestCase(PythonBaseTestCase): + + def setUp(self): + super().setUp() + + patcher = mock.patch.object(_pip.Pip, '_run') + self.mock_run = patcher.start() + self.addCleanup(patcher.stop) + + self._create_python_binary('install_dir') + + def test_clean_packages(self): + pip = _pip.Pip( + python_major_version='test', + part_dir='part_dir', + install_dir='install_dir', + stage_dir='stage_dir') + + packages_dir = os.path.join('part_dir', 'python-packages') + self.assertTrue(os.path.exists(packages_dir)) + + # Now verify that asking pip to clean removes its packages + pip.clean_packages() + self.assertFalse(os.path.exists(packages_dir)) + + +class PipCommandTestCase(PipTestCase): + + def setUp(self): + super().setUp() + + self.pip = _pip.Pip( + python_major_version='test', + part_dir='part_dir', + install_dir='install_dir', + stage_dir='stage_dir') + + # We don't care about anything init did to the mock here: reset it + self.mock_run.reset_mock() + + +class PipDownloadTestCase(PipCommandTestCase): + + scenarios = [ + ('packages', { + 'packages': ['foo', 'bar'], + 'kwargs': {}, + 'expected_args': ['foo', 'bar'], + 'expected_kwargs': {'cwd': None}, + }), + ('setup_py_dir', { + 'packages': [], + 'kwargs': {'setup_py_dir': 'test_setup_py_dir'}, + 'expected_args': ['.'], + 'expected_kwargs': {'cwd': 'test_setup_py_dir'}, + }), + ('single constraint', { + 'packages': [], + 'kwargs': {'constraints': ['constraint']}, + 'expected_args': ['--constraint', 'constraint'], + 'expected_kwargs': {'cwd': None}, + }), + ('multiple constraints', { + 'packages': [], + 'kwargs': {'constraints': ['constraint1', 'constraint2']}, + 'expected_args': [ + '--constraint', 'constraint1', '--constraint', 'constraint2'], + 'expected_kwargs': {'cwd': None}, + }), + ('single requirement', { + 'packages': [], + 'kwargs': {'requirements': ['requirement']}, + 'expected_args': ['--requirement', 'requirement'], + 'expected_kwargs': {'cwd': None}, + }), + ('multiple requirements', { + 'packages': [], + 'kwargs': {'requirements': ['requirement1', 'requirement2']}, + 'expected_args': [ + '--requirement', 'requirement1', '--requirement', + 'requirement2'], + 'expected_kwargs': {'cwd': None}, + }), + ('process dependency links', { + 'packages': [], + 'kwargs': {'process_dependency_links': True}, + 'expected_args': ['--process-dependency-links'], + 'expected_kwargs': {'cwd': None}, + }), + ('packages and setup_py_dir', { + 'packages': ['foo', 'bar'], + 'kwargs': {'setup_py_dir': 'test_setup_py_dir'}, + 'expected_args': ['foo', 'bar', '.'], + 'expected_kwargs': {'cwd': 'test_setup_py_dir'}, + }), + ] + + def _assert_mock_run_with(self, *args, **kwargs): + common_args = [ + 'download', '--disable-pip-version-check', '--dest', mock.ANY] + common_args.extend(*args) + self.mock_run.assert_called_once_with( + common_args, **kwargs) + + def test_without_packages_or_kwargs_should_noop(self): + self.pip.download([]) + self.mock_run.assert_not_called() + + def test_with_packages_and_kwargs(self): + self.pip.download(self.packages, **self.kwargs) + self._assert_mock_run_with(self.expected_args, **self.expected_kwargs) + + +class PipInstallTestCase(PipCommandTestCase): + + scenarios = [ + ('packages', { + 'packages': ['foo', 'bar'], + 'kwargs': {}, + 'expected_args': ['foo', 'bar'], + 'expected_kwargs': {'cwd': None}, + }), + ('setup_py_dir', { + 'packages': [], + 'kwargs': {'setup_py_dir': 'test_setup_py_dir'}, + 'expected_args': ['.'], + 'expected_kwargs': {'cwd': 'test_setup_py_dir'}, + }), + ('single constraint', { + 'packages': [], + 'kwargs': {'constraints': ['constraint']}, + 'expected_args': ['--constraint', 'constraint'], + 'expected_kwargs': {'cwd': None}, + }), + ('multiple constraints', { + 'packages': [], + 'kwargs': {'constraints': ['constraint1', 'constraint2']}, + 'expected_args': [ + '--constraint', 'constraint1', '--constraint', 'constraint2'], + 'expected_kwargs': {'cwd': None}, + }), + ('single requirement', { + 'packages': [], + 'kwargs': {'requirements': ['requirement']}, + 'expected_args': ['--requirement', 'requirement'], + 'expected_kwargs': {'cwd': None}, + }), + ('multiple requirements', { + 'packages': [], + 'kwargs': {'requirements': ['requirement1', 'requirement2']}, + 'expected_args': [ + '--requirement', 'requirement1', '--requirement', + 'requirement2'], + 'expected_kwargs': {'cwd': None}, + }), + ('process dependency links', { + 'packages': [], + 'kwargs': {'process_dependency_links': True}, + 'expected_args': ['--process-dependency-links'], + 'expected_kwargs': {'cwd': None}, + }), + ('upgrade', { + 'packages': [], + 'kwargs': {'upgrade': True}, + 'expected_args': ['--upgrade'], + 'expected_kwargs': {'cwd': None}, + }), + ('install_deps', { + 'packages': [], + 'kwargs': {'install_deps': False}, + 'expected_args': ['--no-deps'], + 'expected_kwargs': {'cwd': None}, + }), + ('ignore_installed', { + 'packages': [], + 'kwargs': {'ignore_installed': True}, + 'expected_args': ['--ignore-installed'], + 'expected_kwargs': {'cwd': None}, + }), + ('packages and setup_py_dir', { + 'packages': ['foo', 'bar'], + 'kwargs': {'setup_py_dir': 'test_setup_py_dir'}, + 'expected_args': ['foo', 'bar', '.'], + 'expected_kwargs': {'cwd': 'test_setup_py_dir'}, + }), + ] + + def _assert_mock_run_with(self, *args, **kwargs): + common_args = [ + 'install', '--user', '--no-compile', '--no-index', '--find-links', + mock.ANY] + common_args.extend(*args) + self.mock_run.assert_called_once_with( + common_args, **kwargs) + + def test_without_packages_or_kwargs_should_noop(self): + self.pip.install([]) + self.mock_run.assert_not_called() + + def test_with_packages_and_kwargs(self): + self.pip.install(self.packages, **self.kwargs) + self._assert_mock_run_with(self.expected_args, **self.expected_kwargs) + + +class PipWheelTestCase(PipCommandTestCase): + + scenarios = [ + ('packages', { + 'packages': ['foo', 'bar'], + 'kwargs': {}, + 'expected_args': ['foo', 'bar'], + 'expected_kwargs': {'cwd': None}, + }), + ('setup_py_dir', { + 'packages': [], + 'kwargs': {'setup_py_dir': 'test_setup_py_dir'}, + 'expected_args': ['.'], + 'expected_kwargs': {'cwd': 'test_setup_py_dir'}, + }), + ('single constraint', { + 'packages': [], + 'kwargs': {'constraints': ['constraint']}, + 'expected_args': ['--constraint', 'constraint'], + 'expected_kwargs': {'cwd': None}, + }), + ('multiple constraints', { + 'packages': [], + 'kwargs': {'constraints': ['constraint1', 'constraint2']}, + 'expected_args': [ + '--constraint', 'constraint1', '--constraint', 'constraint2'], + 'expected_kwargs': {'cwd': None}, + }), + ('single requirement', { + 'packages': [], + 'kwargs': {'requirements': ['requirement']}, + 'expected_args': ['--requirement', 'requirement'], + 'expected_kwargs': {'cwd': None}, + }), + ('multiple requirements', { + 'packages': [], + 'kwargs': {'requirements': ['requirement1', 'requirement2']}, + 'expected_args': [ + '--requirement', 'requirement1', '--requirement', + 'requirement2'], + 'expected_kwargs': {'cwd': None}, + }), + ('process dependency links', { + 'packages': [], + 'kwargs': {'process_dependency_links': True}, + 'expected_args': ['--process-dependency-links'], + 'expected_kwargs': {'cwd': None}, + }), + ('packages and setup_py_dir', { + 'packages': ['foo', 'bar'], + 'kwargs': {'setup_py_dir': 'test_setup_py_dir'}, + 'expected_args': ['foo', 'bar', '.'], + 'expected_kwargs': {'cwd': 'test_setup_py_dir'}, + }), + ] + + def _assert_mock_run_with(self, *args, **kwargs): + common_args = [ + 'wheel', '--no-index', '--find-links', mock.ANY, '--wheel-dir', + mock.ANY] + common_args.extend(*args) + self.mock_run.assert_called_once_with( + common_args, **kwargs) + + def test_without_packages_or_kwargs_should_noop(self): + self.pip.wheel([]) + self.mock_run.assert_not_called() + + def test_with_packages_and_kwargs(self): + self.pip.wheel(self.packages, **self.kwargs) + self._assert_mock_run_with(self.expected_args, **self.expected_kwargs) + + +class PipListTestCase(PipCommandTestCase): + + def test_none(self): + self.mock_run.return_value = '{}' + self.assertFalse(self.pip.list()) + self.mock_run.assert_called_once_with(['list', '--format=json']) + + def test_package(self): + self.mock_run.return_value = '[{"name": "foo", "version": "1.0"}]' + self.assertThat(self.pip.list(), Equals({'foo': '1.0'})) + self.mock_run.assert_called_once_with(['list', '--format=json']) + + def test_user(self): + self.mock_run.return_value = '[{"name": "foo", "version": "1.0"}]' + self.assertThat(self.pip.list(user=True), Equals({'foo': '1.0'})) + self.mock_run.assert_called_once_with( + ['list', '--format=json', '--user']) + + def test_missing_name(self): + self.mock_run.return_value = '[{"version": "1.0"}]' + raised = self.assertRaises( + errors.PipListMissingFieldError, self.pip.list) + self.assertThat( + str(raised), Contains("Pip packages json missing 'name' field")) + + def test_missing_version(self): + self.mock_run.return_value = '[{"name": "foo"}]' + raised = self.assertRaises( + errors.PipListMissingFieldError, self.pip.list) + self.assertThat( + str(raised), Contains("Pip packages json missing 'version' field")) + + def test_invalid_json(self): + self.mock_run.return_value = '[{]' + raised = self.assertRaises( + errors.PipListInvalidJsonError, self.pip.list) + self.assertThat( + str(raised), Contains("Pip packages output isn't valid json")) + + +class _CheckPythonhomeEnv(): + def __init__(self, test, expected_pythonhome): + self.test = test + self.expected_pythonhome = expected_pythonhome + + def __eq__(self, env): + # Verify that we're using the installed pip + self.test.assertThat(env, Contains('PYTHONHOME')) + self.test.assertThat( + env['PYTHONHOME'], Equals(self.expected_pythonhome)) + + return True + + +class _CheckCommand(): + def __init__(self, test, command, packages, flags): + self.test = test + self.command = command + self.packages = packages + self.flags = flags + + def __eq__(self, command): + # Not worrying about the command arguments here, those are + # tested elsewhere. Just want to test that the right command + # is called with the right packages. + self.test.assertTrue(command) + self.test.assertThat( + command[len(self.test.command)], Equals(self.command)) + + for package in self.packages: + self.test.assertThat(command, Contains(package)) + + for flag in self.flags: + self.test.assertThat(command, Contains(flag)) + + return True diff -Nru snapcraft-2.34/snapcraft/tests/plugins/python/test_python_finder.py snapcraft-2.35/snapcraft/tests/plugins/python/test_python_finder.py --- snapcraft-2.34/snapcraft/tests/plugins/python/test_python_finder.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/python/test_python_finder.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,228 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os +import re + +from unittest import mock +from testtools.matchers import ( + Equals, + MatchesRegex, +) + +from snapcraft.plugins._python import ( + errors, + _python_finder, +) + +from . import PythonBaseTestCase + + +class GetPythonCommandTestCase(PythonBaseTestCase): + + def test_staged(self): + """get_python_command should support staged python""" + + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + # Create staged binary + self._create_python_binary(stage_dir) + + python_command = _python_finder.get_python_command( + 'test', stage_dir=stage_dir, install_dir=install_dir) + self.assertThat( + python_command, Equals(os.path.join( + stage_dir, 'usr', 'bin', 'pythontest'))) + + def test_in_part(self): + """get_python_command should support in-part python""" + + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + # Create installed binary + self._create_python_binary(install_dir) + + python_command = _python_finder.get_python_command( + 'test', stage_dir=stage_dir, install_dir=install_dir) + self.assertThat( + python_command, Equals(os.path.join( + install_dir, 'usr', 'bin', 'pythontest'))) + + def test_staged_and_in_part(self): + """get_python_command should prefer staged python over in-part""" + + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + # Create both staged and installed binaries + self._create_python_binary(stage_dir) + self._create_python_binary(install_dir) + + python_command = _python_finder.get_python_command( + 'test', stage_dir=stage_dir, install_dir=install_dir) + self.assertThat( + python_command, Equals(os.path.join( + stage_dir, 'usr', 'bin', 'pythontest'))) + + def test_missing_raises(self): + """get_python_command should raise if no python can be found""" + + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + raised = self.assertRaises( + errors.MissingPythonCommandError, + _python_finder.get_python_command, 'test', stage_dir=stage_dir, + install_dir=install_dir) + self.assertThat(str(raised), Equals( + 'Unable to find pythontest, searched: stage_dir:install_dir')) + + +class GetPythonHeadersTestCase(PythonBaseTestCase): + + def setUp(self): + super().setUp() + + patcher = mock.patch('glob.glob') + self.mock_glob = patcher.start() + self.addCleanup(patcher.stop) + + def test_staged(self): + """get_python_headers should support staged headers""" + + stage_dir = 'stage_dir' + + # Fake out glob so it looks like the headers are in the staging area + def _fake_glob(pattern): + if pattern.startswith(stage_dir): + return [ + os.path.join(stage_dir, 'usr', 'include', 'pythontest')] + return [] + self.mock_glob.side_effect = _fake_glob + + self.assertThat( + _python_finder.get_python_headers('test', stage_dir=stage_dir), + Equals(os.path.join(stage_dir, 'usr', 'include', 'pythontest'))) + + def test_host(self): + """get_python_headers should support the hosts's headers""" + + stage_dir = 'stage_dir' + + # Fake out glob so it looks like the headers are installed on the host + def _fake_glob(pattern): + if pattern.startswith(os.sep): + return [ + os.path.join(os.sep, 'usr', 'include', 'pythontest')] + return [] + self.mock_glob.side_effect = _fake_glob + + self.assertThat( + _python_finder.get_python_headers('test', stage_dir=stage_dir), + MatchesRegex(re.escape(os.path.join( + os.sep, 'usr', 'include', 'pythontest')))) + + def test_staged_and_host(self): + """get_python_headers should prefer staged headers over host""" + + stage_dir = 'stage_dir' + + # Fake out glob so it looks like the headers are in the staging area + # AND on the host + def _fake_glob(pattern): + if pattern.startswith(stage_dir): + return [ + os.path.join(stage_dir, 'usr', 'include', 'pythontest')] + elif pattern.startswith(os.sep): + return [ + os.path.join(os.sep, 'usr', 'include', 'pythontest')] + return [] + self.mock_glob.side_effect = _fake_glob + + self.assertThat( + _python_finder.get_python_headers('test', stage_dir=stage_dir), + Equals(os.path.join(stage_dir, 'usr', 'include', 'pythontest'))) + + def test_none(self): + """get_python_headers should return an empty string if no headers""" + + stage_dir = 'stage_dir' + + # Fake out glob so it looks like the headers aren't installed anywhere + def _fake_glob(pattern): + return [] + self.mock_glob.side_effect = _fake_glob + + self.assertFalse( + _python_finder.get_python_headers('test', stage_dir=stage_dir)) + + +class GetPythonHomeTestCase(PythonBaseTestCase): + + def test_staged(self): + """get_python_home should support staged python""" + + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + # Create staged binary + self._create_python_binary(stage_dir) + + python_home = _python_finder.get_python_home( + 'test', stage_dir=stage_dir, install_dir=install_dir) + self.assertThat(python_home, Equals(os.path.join(stage_dir, 'usr'))) + + def test_in_part(self): + """get_python_home should support in-part python""" + + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + # Create installed binary + self._create_python_binary(install_dir) + + python_home = _python_finder.get_python_home( + 'test', stage_dir=stage_dir, install_dir=install_dir) + self.assertThat(python_home, Equals(os.path.join(install_dir, 'usr'))) + + def test_staged_and_in_part(self): + """get_python_home should prefer staged python over in-part""" + + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + # Create both staged and installed binaries + self._create_python_binary(stage_dir) + self._create_python_binary(install_dir) + + python_home = _python_finder.get_python_home( + 'test', stage_dir=stage_dir, install_dir=install_dir) + self.assertThat(python_home, Equals(os.path.join(stage_dir, 'usr'))) + + def test_missing_raises(self): + """get_python_home should raise if no python can be found""" + + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + raised = self.assertRaises( + errors.MissingPythonCommandError, + _python_finder.get_python_home, 'test', stage_dir=stage_dir, + install_dir=install_dir) + self.assertThat(str(raised), Equals( + 'Unable to find pythontest, searched: stage_dir:install_dir')) diff -Nru snapcraft-2.34/snapcraft/tests/plugins/python/test_sitecustomize.py snapcraft-2.35/snapcraft/tests/plugins/python/test_sitecustomize.py --- snapcraft-2.34/snapcraft/tests/plugins/python/test_sitecustomize.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/python/test_sitecustomize.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,159 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os + +from testtools.matchers import ( + Contains, + FileContains, +) + +from snapcraft.plugins import _python + +from . import PythonBaseTestCase + + +def _create_site_py(base_dir): + site_py = os.path.join( + base_dir, 'usr', 'lib', 'pythontest', 'site.py') + os.makedirs(os.path.dirname(site_py)) + open(site_py, 'w').close() + + +def _create_user_site_packages(base_dir): + user_site_dir = os.path.join( + base_dir, 'lib', 'pythontest', 'site-packages') + os.makedirs(user_site_dir) + + +class SiteCustomizeTestCase(PythonBaseTestCase): + + def test_generate_sitecustomize_staged(self): + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + # Create the python binary in the staging area + self._create_python_binary(stage_dir) + + # Create a site.py in both staging and install areas + _create_site_py(stage_dir) + _create_site_py(install_dir) + + # Create a user site dir in install area + _create_user_site_packages(install_dir) + + _python.generate_sitecustomize( + 'test', stage_dir=stage_dir, install_dir=install_dir) + + expected_sitecustomize = ( + 'import site\n' + 'import os\n' + '\n' + 'snap_dir = os.getenv("SNAP")\n' + 'snapcraft_stage_dir = os.getenv("SNAPCRAFT_STAGE")\n' + 'snapcraft_part_install = os.getenv("SNAPCRAFT_PART_INSTALL")\n' + '\n' + 'for d in (snap_dir, snapcraft_stage_dir, ' + 'snapcraft_part_install):\n' + ' if d:\n' + ' site_dir = os.path.join(d, ' + '"lib/pythontest/site-packages")\n' + ' site.addsitedir(site_dir)\n' + '\n' + 'if snap_dir:\n' + ' site.ENABLE_USER_SITE = False') + + site_path = os.path.join( + install_dir, 'usr', 'lib', 'pythontest', 'sitecustomize.py') + self.assertThat(site_path, FileContains(expected_sitecustomize)) + + def test_generate_sitecustomize_installed(self): + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + # Create the python binary in the installed area + self._create_python_binary(install_dir) + + # Create a site.py in both staging and install areas + _create_site_py(stage_dir) + _create_site_py(install_dir) + + # Create a user site dir in install area + _create_user_site_packages(install_dir) + + _python.generate_sitecustomize( + 'test', stage_dir=stage_dir, install_dir=install_dir) + + expected_sitecustomize = ( + 'import site\n' + 'import os\n' + '\n' + 'snap_dir = os.getenv("SNAP")\n' + 'snapcraft_stage_dir = os.getenv("SNAPCRAFT_STAGE")\n' + 'snapcraft_part_install = os.getenv("SNAPCRAFT_PART_INSTALL")\n' + '\n' + 'for d in (snap_dir, snapcraft_stage_dir, ' + 'snapcraft_part_install):\n' + ' if d:\n' + ' site_dir = os.path.join(d, ' + '"lib/pythontest/site-packages")\n' + ' site.addsitedir(site_dir)\n' + '\n' + 'if snap_dir:\n' + ' site.ENABLE_USER_SITE = False') + + site_path = os.path.join( + install_dir, 'usr', 'lib', 'pythontest', 'sitecustomize.py') + self.assertThat(site_path, FileContains(expected_sitecustomize)) + + def test_generate_sitecustomize_missing_user_site_raises(self): + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + # Create the python binary in the installed area + self._create_python_binary(install_dir) + + # Create a site.py in both staging and install areas + _create_site_py(stage_dir) + _create_site_py(install_dir) + + # Do NOT create a user site dir, and attempt to generate sitecustomize. + raised = self.assertRaises( + _python.errors.MissingUserSitePackagesError, + _python.generate_sitecustomize, 'test', stage_dir=stage_dir, + install_dir=install_dir) + self.assertThat( + str(raised), Contains('Unable to find user site packages')) + + def test_generate_sitecustomize_missing_site_py_raises(self): + stage_dir = 'stage_dir' + install_dir = 'install_dir' + + # Create the python binary in the staging area + self._create_python_binary(stage_dir) + + # Create a site.py, but only in install area (not staging area) + _create_site_py(install_dir) + + # Create a user site dir in install area + _create_user_site_packages(install_dir) + + raised = self.assertRaises( + _python.errors.MissingSitePyError, + _python.generate_sitecustomize, 'test', stage_dir=stage_dir, + install_dir=install_dir) + self.assertThat( + str(raised), Contains('Unable to find site.py')) diff -Nru snapcraft-2.34/snapcraft/tests/plugins/ros/test_ros2.py snapcraft-2.35/snapcraft/tests/plugins/ros/test_ros2.py --- snapcraft-2.34/snapcraft/tests/plugins/ros/test_ros2.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/ros/test_ros2.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,77 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os + +from unittest import mock + +from snapcraft.plugins._ros import ros2 + +import snapcraft +from snapcraft import ( + tests +) + + +class Ros2TestCase(tests.TestCase): + + def setUp(self): + super().setUp() + self.project = snapcraft.ProjectOptions() + + self.bootstrapper = ros2.Bootstrapper( + version='release-beta3', + bootstrap_path='bootstrap_path', + ubuntu_sources='sources', + project=self.project) + + patcher = mock.patch('snapcraft.repo.Ubuntu') + self.ubuntu_mock = patcher.start() + self.addCleanup(patcher.stop) + + patcher = mock.patch('subprocess.check_call') + self.check_call_mock = patcher.start() + self.addCleanup(patcher.stop) + + @mock.patch('snapcraft.sources.Script') + def test_pull(self, mock_script): + self.bootstrapper.pull() + + # Assert that the ros2 repos file was pulled down + mock_script.return_value.download.assert_called_once_with() + + # Verify that python3-vcstool is installed + self.ubuntu_mock.assert_has_calls([ + mock.call().get(['python3-vcstool']), + mock.call().unpack(self.bootstrapper._tool_dir)]) + + # Verify that vcstool is then used to fetch the ROS2 underlay + ros2_repos = os.path.join( + self.bootstrapper._underlay_dir, 'ros2.repos') + self.check_call_mock.assert_called_once_with([ + 'vcs', 'import', '--input', ros2_repos, + self.bootstrapper._source_dir], env=mock.ANY) + + def test_build(self): + self.bootstrapper.build() + + ament_path = os.path.join( + self.bootstrapper._source_dir, 'ament', 'ament_tools', + 'scripts', 'ament.py') + self.check_call_mock.assert_called_once_with([ + ament_path, 'build', self.bootstrapper._source_dir, + '--build-space', self.bootstrapper._build_dir, + '--install-space', self.bootstrapper._install_dir], env=mock.ANY) diff -Nru snapcraft-2.34/snapcraft/tests/plugins/test_autotools.py snapcraft-2.35/snapcraft/tests/plugins/test_autotools.py --- snapcraft-2.34/snapcraft/tests/plugins/test_autotools.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/test_autotools.py 2017-11-01 19:41:33.000000000 +0000 @@ -346,11 +346,11 @@ class AutotoolsCrossCompilePluginTestCase(tests.TestCase): scenarios = [ - ('armv7l', dict(deb_arch='armhf')), - ('aarch64', dict(deb_arch='arm64')), - ('i386', dict(deb_arch='i386')), - ('x86_64', dict(deb_arch='amd64')), - ('ppc64le', dict(deb_arch='ppc64el')), + ('armv7l', dict(deb_arch='armhf', triplet='arm-linux-gnueabihf')), + ('aarch64', dict(deb_arch='arm64', triplet='aarch64-linux-gnu')), + ('i386', dict(deb_arch='i386', triplet='i386-linux-gnu')), + ('x86_64', dict(deb_arch='amd64', triplet='x86_64-linux-gnu')), + ('ppc64le', dict(deb_arch='ppc64el', triplet='powerpc64le-linux-gnu')), ] def setUp(self): @@ -373,20 +373,17 @@ self.run_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch('snapcraft.ProjectOptions.is_cross_compiling') + patcher = mock.patch( + 'snapcraft.ProjectOptions.is_cross_compiling', + return_value=True) patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch.dict(os.environ, {}) - self.env_mock = patcher.start() - self.addCleanup(patcher.stop) - def test_cross_compile(self): plugin = autotools.AutotoolsPlugin('test-part', self.options, self.project_options) plugin.enable_cross_compilation() - env = plugin.env(plugin.sourcedir) - self.assertIn('CC={}-gcc'.format( - self.project_options.arch_triplet), env) - self.assertIn('CXX={}-g++'.format( - self.project_options.arch_triplet), env) + plugin.build() + self.run_mock.assert_has_calls([mock.call([ + './configure', '--prefix=', '--host={}'.format(self.triplet)], + cwd=mock.ANY)]) diff -Nru snapcraft-2.34/snapcraft/tests/plugins/test_catkin.py snapcraft-2.35/snapcraft/tests/plugins/test_catkin.py --- snapcraft-2.34/snapcraft/tests/plugins/test_catkin.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/test_catkin.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,21 +14,27 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import ast import builtins import os import os.path import re import subprocess import shutil +import sys +import tempfile +import textwrap from unittest import mock import testtools from testtools.matchers import ( Contains, Equals, + FileExists, HasLength, LessThan, MatchesRegex, + Not, ) import snapcraft @@ -76,6 +82,7 @@ underlay = None rosinstall_files = None build_attributes = [] + catkin_ros_master_uri = 'http://localhost:11311' self.properties = props() self.project_options = snapcraft.ProjectOptions() @@ -85,12 +92,12 @@ self.addCleanup(patcher.stop) patcher = mock.patch( - 'snapcraft.plugins.catkin._find_system_dependencies') + 'snapcraft.plugins.catkin._find_system_dependencies', + return_value={}) self.dependencies_mock = patcher.start() self.addCleanup(patcher.stop) - patcher = mock.patch( - 'snapcraft.plugins._ros.rosdep.Rosdep') + patcher = mock.patch('snapcraft.plugins._ros.rosdep.Rosdep') self.rosdep_mock = patcher.start() self.addCleanup(patcher.stop) @@ -102,6 +109,11 @@ self.wstool_mock = patcher.start() self.addCleanup(patcher.stop) + patcher = mock.patch('snapcraft.plugins._python.Pip') + self.pip_mock = patcher.start() + self.addCleanup(patcher.stop) + self.pip_mock.return_value.list.return_value = {} + def assert_rosdep_setup(self, rosdistro, package_path, rosdep_path, ubuntu_distro, sources): self.rosdep_mock.assert_has_calls([ @@ -120,6 +132,16 @@ self.project_options), mock.call().setup()]) + def assert_pip_setup(self, python_major_version, part_dir, install_dir, + stage_dir): + self.pip_mock.assert_has_calls([ + mock.call( + python_major_version=python_major_version, + part_dir=part_dir, + install_dir=install_dir, + stage_dir=stage_dir), + mock.call().setup()]) + class CatkinPluginTestCase(CatkinPluginBaseTestCase): @@ -129,7 +151,7 @@ properties = schema['properties'] expected = ('rosdistro', 'catkin-packages', 'source-space', 'include-roscore', 'catkin-cmake-args', 'underlay', - 'rosinstall-files') + 'rosinstall-files', 'catkin-ros-master-uri') self.assertThat(properties, HasLength(len(expected))) for prop in expected: self.assertThat(properties, Contains(prop)) @@ -251,6 +273,19 @@ self.assertThat(rosinstall_files['items'], Contains('type')) self.assertThat(rosinstall_files['items']['type'], Equals('string')) + def test_schema_catkin_ros_master_uri(self): + schema = catkin.CatkinPlugin.schema() + + # Check ros-master-uri property + catkin_ros_master_uri = schema['properties']['catkin-ros-master-uri'] + expected = ('type', 'default') + self.assertThat(catkin_ros_master_uri, HasLength(len(expected))) + for prop in expected: + self.assertThat(catkin_ros_master_uri, Contains(prop)) + self.assertThat(catkin_ros_master_uri['type'], Equals('string')) + self.assertThat(catkin_ros_master_uri['default'], + Equals('http://localhost:11311')) + def test_get_pull_properties(self): expected_pull_properties = ['rosdistro', 'catkin-packages', 'source-space', 'include-roscore', @@ -264,7 +299,7 @@ self.assertIn(property, actual_pull_properties) def test_get_build_properties(self): - expected_build_properties = ['build-attributes', 'catkin-cmake-args'] + expected_build_properties = ['catkin-cmake-args'] actual_build_properties = catkin.CatkinPlugin.get_build_properties() self.assertThat(actual_build_properties, @@ -327,7 +362,7 @@ plugin.pull) self.assertThat(str(raised), - Equals('Failed to fetch system dependencies: The ' + Equals('Failed to fetch apt dependencies: The ' "package 'foo' was not found.")) def test_pull_unable_to_resolve_roscore(self): @@ -584,10 +619,10 @@ environment = '\n'.join(plugin.env(plugin.installdir)).split('\n') self.assertThat(environment, Contains( - 'PYTHONPATH={}:$PYTHONPATH'.format(python_path))) + 'PYTHONPATH={}${{PYTHONPATH:+:$PYTHONPATH}}'.format(python_path))) self.assertThat(environment, Contains( - 'ROS_MASTER_URI=http://localhost:11311')) + 'ROS_MASTER_URI={}'.format(self.properties.catkin_ros_master_uri))) self.assertThat( environment, Contains('ROS_HOME=${SNAP_USER_DATA:-/tmp}/ros')) @@ -630,6 +665,80 @@ os.path.join(plugin.rosdir, 'snapcraft-setup.sh')))) @mock.patch.object(catkin.CatkinPlugin, '_source_setup_sh', + return_value='test-source-setup.sh') + @mock.patch.object(catkin.CatkinPlugin, 'run_output', + return_value='bar') + def test_run_environment_with_catkin_ros_master_uri(self, run_mock, + source_setup_sh_mock): + + self.properties.catkin_ros_master_uri = 'http://rosmaster:11311' + plugin = catkin.CatkinPlugin('test-part', self.properties, + self.project_options) + + self._verify_run_environment(plugin) + + def _evaluate_environment(self, predefinition=''): + plugin = catkin.CatkinPlugin('test-part', self.properties, + self.project_options) + + python_path = os.path.join( + plugin.installdir, 'usr', 'lib', 'python2.7', 'dist-packages') + os.makedirs(python_path) + + # Save plugin environment off into a file and read the evaluated + # version back in, thus obtaining the real environment + with tempfile.NamedTemporaryFile(mode='w+') as f: + f.write(predefinition) + f.write('\n'.join(['export ' + e for e in plugin.env( + plugin.installdir)])) + f.write('python3 -c "import os; print(dict(os.environ))"') + f.flush() + return ast.literal_eval(subprocess.check_output( + ['/bin/sh', f.name]).decode( + sys.getfilesystemencoding()).strip()) + + def _pythonpath_segments(self, environment): + # Verify that the environment contains PYTHONPATH, and return its + # segments as a list. + self.assertThat(environment, Contains('PYTHONPATH')) + return environment['PYTHONPATH'].split(':') + + def _list_contains_empty_items(self, item_list): + empty_items = [i for i in item_list if not i.strip()] + return len(empty_items) > 0 + + def test_pythonpath_if_not_defined(self): + environment = self._evaluate_environment() + segments = self._pythonpath_segments(environment) + self.assertFalse( + self._list_contains_empty_items(segments), + 'PYTHONPATH unexpectedly contains empty segments: {}'.format( + environment['PYTHONPATH'])) + + def test_pythonpath_if_null(self): + environment = self._evaluate_environment(textwrap.dedent(""" + export PYTHONPATH= + """)) + + segments = self._pythonpath_segments(environment) + self.assertFalse( + self._list_contains_empty_items(segments), + 'PYTHONPATH unexpectedly contains empty segments: {}'.format( + environment['PYTHONPATH'])) + + def test_pythonpath_if_not_empty(self): + environment = self._evaluate_environment(textwrap.dedent(""" + export PYTHONPATH=foo + """)) + + segments = self._pythonpath_segments(environment) + self.assertFalse( + self._list_contains_empty_items(segments), + 'PYTHONPATH unexpectedly contains empty segments: {}'.format( + environment['PYTHONPATH'])) + self.assertThat(segments, Contains('foo')) + + @mock.patch.object(catkin.CatkinPlugin, '_source_setup_sh', return_value='test-source-setup') def test_generate_snapcraft_sh(self, source_setup_sh_mock): plugin = catkin.CatkinPlugin('test-part', self.properties, @@ -960,6 +1069,49 @@ self.assertTrue(mock.call().unpack(plugin.installdir) not in self.ubuntu_mock.mock_calls) + @mock.patch.object(catkin.CatkinPlugin, '_generate_snapcraft_setup_sh') + def test_pull_pip_dependencies(self, generate_setup_mock): + plugin = catkin.CatkinPlugin('test-part', self.properties, + self.project_options) + os.makedirs(os.path.join(plugin.sourcedir, 'src')) + + self.dependencies_mock.return_value = {'pip': {'foo', 'bar', 'baz'}} + + plugin.pull() + + self.assert_rosdep_setup( + self.properties.rosdistro, + os.path.join(plugin.sourcedir, 'src'), + os.path.join(plugin.partdir, 'rosdep'), + self.properties.ubuntu_distro, + plugin.PLUGIN_STAGE_SOURCES) + + self.wstool_mock.assert_not_called() + + self.assert_pip_setup( + '2', plugin.partdir, plugin.installdir, plugin.project.stage_dir) + + # This shouldn't be called unless there's an underlay + if self.properties.underlay: + generate_setup_mock.assert_called_once_with( + plugin.installdir, self.expected_underlay_path) + else: + generate_setup_mock.assert_not_called() + + # Verify that dependencies were found as expected. TODO: Would really + # like to use ANY here instead of verifying explicit arguments, but + # Python issue #25195 won't let me. + self.assertThat(self.dependencies_mock.call_count, Equals(1)) + self.assertThat( + self.dependencies_mock.call_args[0][0], + Equals({'my_package'})) + + # Verify that the pip dependencies were installed + self.pip_mock.return_value.download.assert_called_once_with( + {'foo', 'bar', 'baz'}) + self.pip_mock.return_value.install.assert_called_once_with( + {'foo', 'bar', 'baz'}) + class BuildTestCase(CatkinPluginBaseTestCase): @@ -1126,6 +1278,11 @@ with open(path, 'r') as f: self.assertThat(f.read(), Equals(file_info['expected'])) + # Verify that no sitecustomize.py was generated + self.assertThat(os.path.join( + self.plugin.installdir, 'usr', 'lib', 'python2.7', + 'sitecustomize.py'), Not(FileExists())) + @mock.patch.object(catkin.CatkinPlugin, '_generate_snapcraft_setup_sh') @mock.patch.object(catkin.CatkinPlugin, 'run') @mock.patch.object(catkin.CatkinPlugin, 'run_output', return_value='foo') @@ -1159,6 +1316,40 @@ 'The absolute path to python or the CMAKE_PREFIX_PATH ' 'was not replaced as expected') + # Verify that no sitecustomize.py was generated + self.assertThat(os.path.join( + self.plugin.installdir, 'usr', 'lib', 'python2.7', + 'sitecustomize.py'), Not(FileExists())) + + @mock.patch.object(catkin.CatkinPlugin, '_generate_snapcraft_setup_sh') + @mock.patch.object(catkin.CatkinPlugin, 'run') + @mock.patch.object(catkin.CatkinPlugin, 'run_output', return_value='foo') + @mock.patch.object(catkin.CatkinPlugin, '_use_in_snap_python') + def test_finish_build_python_sitecustomize(self, use_python_mock, + run_output_mock, run_mock, + generate_setup_mock): + self.pip_mock.return_value.list.return_value = {'test-package'} + + # Create site.py, indicating that python2 was a stage-package + site_py_path = os.path.join( + self.plugin.installdir, 'usr', 'lib', 'python2.7', 'site.py') + os.makedirs(os.path.dirname(site_py_path), exist_ok=True) + open(site_py_path, 'w').close() + + # Also create python2 site-packages, indicating that pip packages were + # installed. + os.makedirs( + os.path.join( + self.plugin.installdir, 'lib', 'python2.7', 'site-packages'), + exist_ok=True) + + self.plugin._finish_build() + + # Verify that sitecustomize.py was generated + self.assertThat(os.path.join( + self.plugin.installdir, 'usr', 'lib', 'python2.7', + 'sitecustomize.py'), FileExists()) + class FindSystemDependenciesTestCase(tests.TestCase): def setUp(self): @@ -1247,7 +1438,7 @@ def test_find_system_dependencies_raises_if_unsupported_type(self): self.rosdep_mock.resolve_dependency.return_value = { - 'pip': {'baz'}, + 'unsupported-type': {'baz'}, } raised = self.assertRaises( @@ -1257,7 +1448,7 @@ self.assertThat(str(raised), Equals( "Package 'bar' resolved to an unsupported type of dependency: " - "'pip'")) + "'unsupported-type'")) class HandleRosinstallFilesTestCase(tests.TestCase): diff -Nru snapcraft-2.34/snapcraft/tests/plugins/test_dotnet.py snapcraft-2.35/snapcraft/tests/plugins/test_dotnet.py --- snapcraft-2.34/snapcraft/tests/plugins/test_dotnet.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/test_dotnet.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,176 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os +import subprocess +import tarfile +from unittest import mock + +from testtools.matchers import ( + Contains, + DirExists, + Equals, + FileExists, + Not) + +import snapcraft +from snapcraft import ( + file_utils, + tests) +from snapcraft.internal import sources +from snapcraft.plugins import dotnet + + +def _setup_dirs(plugin): + os.makedirs(plugin.builddir) + open(os.path.join(plugin.builddir, 'test-app.xxproj'), 'w').close() + os.makedirs(plugin.installdir) + executable_path = os.path.join(plugin.installdir, 'test-app') + open(executable_path, 'w').close() + + +class DotNetPluginTestCase(tests.TestCase): + + def test_schema(self): + schema = dotnet.DotNetPlugin.schema() + self.assertThat(schema, Not(Contains('required'))) + + def test_get_pull_properties(self): + expected_pull_properties = [] + self.assertThat( + dotnet.DotNetPlugin.get_pull_properties(), + Equals(expected_pull_properties)) + + def test_get_build_properties(self): + expected_build_properties = [] + self.assertThat( + dotnet.DotNetPlugin.get_build_properties(), + Equals(expected_build_properties)) + + +class DotNetProjectBaseTestCase(tests.TestCase): + + def setUp(self): + super().setUp() + + class Options: + build_attributes = [] + + self.options = Options() + self.project = snapcraft.ProjectOptions() + + # Only amd64 is supported for now. + patcher = mock.patch( + 'snapcraft.ProjectOptions.deb_arch', + new_callable=mock.PropertyMock, + return_value='amd64') + patcher.start() + self.addCleanup(patcher.stop) + + original_check_call = subprocess.check_call + patcher = mock.patch('subprocess.check_call') + self.mock_check_call = patcher.start() + self.addCleanup(patcher.stop) + + def side_effect(cmd, *args, **kwargs): + if cmd[2].endswith('dotnet'): + pass + else: + original_check_call(cmd, *args, **kwargs) + + self.mock_check_call.side_effect = side_effect + + +class DotNetProjectTestCase(DotNetProjectBaseTestCase): + + def test_init_with_non_amd64_architecture(self): + with mock.patch( + 'snapcraft.ProjectOptions.deb_arch', + new_callable=mock.PropertyMock, + return_value='non-amd64'): + error = self.assertRaises( + NotImplementedError, + dotnet.DotNetPlugin, + 'test-part', self.options, self.project) + self.assertThat( + str(error), + Equals("This plugin does not support architecture 'non-amd64'")) + + def test_pull_sdk(self): + with tarfile.open('test-sdk.tar', 'w') as test_sdk_tar: + open('test-sdk', 'w').close() + test_sdk_tar.add('test-sdk') + with mock.patch.dict( + dotnet._SDKS_AMD64['2.0.0'], + {'checksum': 'sha256/{}'.format( + file_utils.calculate_hash( + 'test-sdk.tar', algorithm='sha256'))}): + plugin = dotnet.DotNetPlugin( + 'test-part', self.options, self.project) + + with mock.patch.object( + sources.Tar, 'download', return_value='test-sdk.tar'): + plugin.pull() + + self.assertThat( + os.path.join('parts', 'test-part', 'dotnet', 'sdk', 'test-sdk'), + FileExists()) + + def test_clean_pull_removes_dotnet_dir(self): + dotnet_dir = os.path.join('parts', 'test-part', 'dotnet', 'sdk') + os.makedirs(dotnet_dir) + plugin = dotnet.DotNetPlugin( + 'test-part', self.options, self.project) + plugin.clean_pull() + self.assertThat(dotnet_dir, Not(DirExists())) + + def test_build_changes_executable_permissions(self): + plugin = dotnet.DotNetPlugin( + 'test-part', self.options, self.project) + _setup_dirs(plugin) + plugin.build() + + self.assertThat( + os.stat(os.path.join( + plugin.installdir, 'test-app')).st_mode & 0o777, + Equals(0o755)) + + +class DotNetProjectBuildCommandsTestCase(DotNetProjectBaseTestCase): + + scenarios = [ + ('Debug', dict(configuration='Debug', build_attributes=['debug'])), + ('Release', dict(configuration='Release', build_attributes=[]))] + + def test_build_commands(self): + self.options.build_attributes = self.build_attributes + plugin = dotnet.DotNetPlugin( + 'test-part', self.options, self.project) + _setup_dirs(plugin) + plugin.build() + + part_dir = os.path.join(self.path, 'parts', 'test-part') + dotnet_command = os.path.join(part_dir, 'dotnet', 'sdk', 'dotnet') + self.assertThat( + self.mock_check_call.mock_calls, Equals([ + mock.call([ + mock.ANY, mock.ANY, dotnet_command, + 'build', '-c', self.configuration], cwd=mock.ANY), + mock.call([ + mock.ANY, mock.ANY, dotnet_command, + 'publish', '-c', self.configuration, + '-o', plugin.installdir, + '--self-contained', '-r', 'linux-x64'], cwd=mock.ANY)])) diff -Nru snapcraft-2.34/snapcraft/tests/plugins/test_kbuild.py snapcraft-2.35/snapcraft/tests/plugins/test_kbuild.py --- snapcraft-2.34/snapcraft/tests/plugins/test_kbuild.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/test_kbuild.py 2017-11-01 19:41:33.000000000 +0000 @@ -65,8 +65,7 @@ def test_get_build_properties(self): expected_build_properties = ['kdefconfig', 'kconfigfile', - 'kconfigflavour', 'kconfigs', - 'build-attributes'] + 'kconfigflavour', 'kconfigs'] resulting_build_properties = kbuild.KBuildPlugin.get_build_properties() self.assertThat(resulting_build_properties, diff -Nru snapcraft-2.34/snapcraft/tests/plugins/test_kernel.py snapcraft-2.35/snapcraft/tests/plugins/test_kernel.py --- snapcraft-2.34/snapcraft/tests/plugins/test_kernel.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/test_kernel.py 2017-11-01 19:41:33.000000000 +0000 @@ -1078,7 +1078,7 @@ plugin.pull() download_mock.assert_called_once_with( - 'ubuntu-core', 'edge', plugin.os_snap, + 'core', 'stable', plugin.os_snap, self.project_options.deb_arch, '') diff -Nru snapcraft-2.34/snapcraft/tests/plugins/test_nodejs.py snapcraft-2.35/snapcraft/tests/plugins/test_nodejs.py --- snapcraft-2.34/snapcraft/tests/plugins/test_nodejs.py 2017-09-10 17:25:47.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/test_nodejs.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,9 +14,11 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import collections import os from unittest import mock +from testscenarios.scenarios import multiply_scenarios from testtools.matchers import DirExists, Equals, HasLength import snapcraft @@ -30,6 +32,15 @@ def setUp(self): super().setUp() + class Options: + source = '.' + node_packages = [] + node_engine = nodejs._NODEJS_VERSION + npm_run = [] + node_package_manager = 'npm' + source = '.' + self.options = Options() + self.project_options = snapcraft.ProjectOptions() self.useFixture(tests.fixture_setup.CleanEnvironment()) @@ -38,6 +49,11 @@ self.run_mock = patcher.start() self.addCleanup(patcher.stop) + patcher = mock.patch('snapcraft.internal.common.run_output') + self.run_output_mock = patcher.start() + self.addCleanup(patcher.stop) + self.run_output_mock.return_value = '{"dependencies": []}' + patcher = mock.patch('snapcraft.sources.Tar') self.tar_mock = patcher.start() self.addCleanup(patcher.stop) @@ -58,15 +74,9 @@ ] def test_pull_local_sources(self): - class Options: - source = '.' - node_packages = [] - node_engine = nodejs._NODEJS_VERSION - npm_run = [] - node_package_manager = self.package_manager - source = '.' + self.options.node_package_manager = self.package_manager - plugin = nodejs.NodePlugin('test-part', Options(), + plugin = nodejs.NodePlugin('test-part', self.options, self.project_options) os.makedirs(plugin.sourcedir) @@ -97,17 +107,11 @@ self.tar_mock.assert_has_calls(expected_tar_calls) def test_build_local_sources(self): - class Options: - source = '.' - node_packages = [] - node_engine = nodejs._NODEJS_VERSION - npm_run = [] - node_package_manager = self.package_manager - source = '.' + self.options.node_package_manager = self.package_manager open('package.json', 'w').close() - plugin = nodejs.NodePlugin('test-part', Options(), + plugin = nodejs.NodePlugin('test-part', self.options, self.project_options) os.makedirs(plugin.builddir) @@ -150,15 +154,10 @@ self.tar_mock.assert_has_calls(expected_tar_calls) def test_pull_and_build_node_packages_sources(self): - class Options: - source = None - node_packages = ['my-pkg'] - node_engine = nodejs._NODEJS_VERSION - npm_run = [] - node_package_manager = self.package_manager - source = '.' + self.options.node_packages = ['my-pkg'] + self.options.node_package_manager = self.package_manager - plugin = nodejs.NodePlugin('test-part', Options(), + plugin = nodejs.NodePlugin('test-part', self.options, self.project_options) os.makedirs(plugin.sourcedir) @@ -210,15 +209,10 @@ self.tar_mock.assert_has_calls(expected_tar_calls) def test_pull_executes_npm_run_commands(self): - class Options: - source = '.' - node_packages = [] - node_engine = '4' - npm_run = ['command_one', 'avocado'] - node_package_manager = self.package_manager - source = '.' + self.options.npm_run = ['command_one', 'avocado'] + self.options.node_package_manager = self.package_manager - plugin = nodejs.NodePlugin('test-part', Options(), + plugin = nodejs.NodePlugin('test-part', self.options, self.project_options) os.makedirs(plugin.sourcedir) @@ -250,15 +244,10 @@ self.run_mock.assert_has_calls(expected_run_calls) def test_build_executes_npm_run_commands(self): - class Options: - source = '.' - node_packages = [] - node_engine = '4' - npm_run = ['command_one', 'avocado'] - node_package_manager = self.package_manager - source = '.' + self.options.npm_run = ['command_one', 'avocado'] + self.options.node_package_manager = self.package_manager - plugin = nodejs.NodePlugin('test-part', Options(), + plugin = nodejs.NodePlugin('test-part', self.options, self.project_options) os.makedirs(plugin.sourcedir) @@ -287,18 +276,12 @@ @mock.patch('snapcraft.ProjectOptions.deb_arch', 'fantasy-arch') def test_unsupported_arch_raises_exception(self): - class Options: - source = None - node_packages = [] - node_engine = '4' - npm_run = [] - node_package_manager = self.package_manager - source = '.' + self.options.node_package_manager = self.package_manager raised = self.assertRaises( errors.SnapcraftEnvironmentError, nodejs.NodePlugin, - 'test-part', Options(), + 'test-part', self.options, self.project_options) self.assertThat(raised.__str__(), @@ -325,14 +308,9 @@ self.assertIn(property, resulting_pull_properties) def test_clean_pull_step(self): - class Options: - source = '.' - node_packages = [] - node_engine = '4' - npm_run = [] - node_package_manager = self.package_manager + self.options.node_package_manager = self.package_manager - plugin = nodejs.NodePlugin('test-part', Options(), + plugin = nodejs.NodePlugin('test-part', self.options, self.project_options) os.makedirs(plugin.sourcedir) @@ -346,18 +324,70 @@ self.assertFalse(os.path.exists(plugin._npm_dir)) +class NodePluginManifestTestCase(NodePluginBaseTestCase): + + scenarios = multiply_scenarios( + [ + ('simple', dict(ls_output=( + '{"dependencies": {' + ' "testpackage1": {"version": "1.0"},' + ' "testpackage2": {"version": "1.2"}}}'))), + ('nested', dict(ls_output=( + '{"dependencies": {' + ' "testpackage1": {' + ' "version": "1.0",' + ' "dependencies": {' + ' "testpackage2": {"version": "1.2"}}}}}'))), + ('missing', dict(ls_output=( + '{"dependencies": {' + ' "testpackage1": {"version": "1.0"},' + ' "testpackage2": {"version": "1.2"},' + ' "missing": {"noversion": "dummy"}}}')))], + [('npm', dict(package_manager='npm')), + ('yarn', dict(package_manager='yarn'))]) + + def test_get_manifest_with_node_packages(self): + self.run_output_mock.return_value = self.ls_output + + self.options.node_package_manager = self.package_manager + plugin = nodejs.NodePlugin('test-part', self.options, + self.project_options) + os.makedirs(plugin.sourcedir) + + plugin.build() + + self.assertThat( + plugin.get_manifest(), Equals( + collections.OrderedDict( + {'node-packages': + ['testpackage1=1.0', 'testpackage2=1.2']}))) + + +class NodePluginYarnLockManifestTestCase(NodePluginBaseTestCase): + + def test_get_manifest_with_yarn_lock_file(self): + self.options.node_package_manager = 'yarn' + plugin = nodejs.NodePlugin('test-part', self.options, + self.project_options) + os.makedirs(plugin.sourcedir) + with open(os.path.join( + plugin.sourcedir, + 'yarn.lock'), 'w') as yarn_lock_file: + yarn_lock_file.write('test yarn lock contents') + + plugin.build() + + expected_manifest = collections.OrderedDict() + expected_manifest['yarn-lock-contents'] = 'test yarn lock contents' + expected_manifest['node-packages'] = [] + + self.assertThat(plugin.get_manifest(), Equals(expected_manifest)) + + class NodePluginNpmWorkaroundsTestCase(NodePluginBaseTestCase): def test_build_from_local_fixes_symlinks(self): - class Options: - source = '.' - node_packages = [] - node_engine = '4' - npm_run = [] - node_package_manager = 'npm' - source = '.' - - plugin = nodejs.NodePlugin('test-part', Options(), + plugin = nodejs.NodePlugin('test-part', self.options, self.project_options) os.makedirs(plugin.sourcedir) diff -Nru snapcraft-2.34/snapcraft/tests/plugins/test_plainbox_provider.py snapcraft-2.35/snapcraft/tests/plugins/test_plainbox_provider.py --- snapcraft-2.34/snapcraft/tests/plugins/test_plainbox_provider.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/test_plainbox_provider.py 2017-11-01 19:41:33.000000000 +0000 @@ -78,9 +78,10 @@ plugin.build() + env = os.environ.copy() + env['PROVIDERPATH'] = '' calls = [ - mock.call(['python3', 'manage.py', 'validate'], - env=os.environ.copy()), + mock.call(['python3', 'manage.py', 'validate'], env=env), mock.call(['python3', 'manage.py', 'build']), mock.call(['python3', 'manage.py', 'i18n']), mock.call(['python3', 'manage.py', 'install', diff -Nru snapcraft-2.34/snapcraft/tests/plugins/test_ruby.py snapcraft-2.35/snapcraft/tests/plugins/test_ruby.py --- snapcraft-2.34/snapcraft/tests/plugins/test_ruby.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/test_ruby.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,253 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os +from unittest import mock + +from testtools.matchers import Equals, HasLength + +import snapcraft +from snapcraft import tests +from snapcraft.plugins import ruby + + +class RubyPluginTestCase(tests.TestCase): + + def setUp(self): + super().setUp() + + class Options(snapcraft.ProjectOptions): + source = '.' + ruby_version = '2.4.2' + gems = [] + use_bundler = False + + self.options = Options() + self.project_options = snapcraft.ProjectOptions() + + def test_schema(self): + schema = ruby.RubyPlugin.schema() + expected_use_bundler = { + 'type': 'boolean', + 'default': False, + } + expected_ruby_version = { + 'type': 'string', + 'default': '2.4.2' + } + expected_gems = { + 'type': 'array', + 'minitems': 1, + 'uniqueItems': True, + 'items': {'type': 'string'}, + 'default': [], + } + + self.assertThat(schema['properties'], HasLength(3)) + self.assertDictEqual(expected_use_bundler, + schema['properties']['use-bundler']) + self.assertDictEqual(expected_ruby_version, + schema['properties']['ruby-version']) + self.assertDictEqual(expected_gems, + schema['properties']['gems']) + + def test_get_pull_properties(self): + expected_pull_properties = [ + 'ruby-version', + 'gems', + 'use-bundler' + ] + resulting_pull_properties = ruby.RubyPlugin.get_pull_properties() + + self.assertThat(resulting_pull_properties, + HasLength(len(expected_pull_properties))) + + for property in expected_pull_properties: + self.assertIn(property, resulting_pull_properties) + + def test_snap_fileset(self): + expected_fileset = (['-include/', '-share/']) + + plugin = ruby.RubyPlugin( + 'test-part', self.options, self.project_options) + fileset = plugin.snap_fileset() + + self.assertThat(fileset, Equals(expected_fileset)) + + def test_env_without_ruby(self): + plugin = ruby.RubyPlugin( + 'test-part', self.options, self.project_options) + env = plugin.env('dummy-path') + self.assertThat(env, Equals([])) + + def test_env_with_ruby(self): + plugin = ruby.RubyPlugin( + 'test-part', self.options, self.project_options) + part_dir = os.path.join(self.path, 'test-part-path') + + os.makedirs( + os.path.join(part_dir, 'lib', 'ruby', 'gems', 'test-version')) + libdir = os.path.join('test-part-path', 'lib', 'ruby', 'test-version') + arch_libdir = os.path.join(libdir, 'foo-linux-bar') + real_arch_libdir = os.path.join(self.path, arch_libdir) + os.makedirs(real_arch_libdir) + open(os.path.join(real_arch_libdir, 'rbconfig.rb'), 'w').close() + env = plugin.env('test-part-path') + + expected_env = { + 'GEM_HOME="test-part-path/lib/ruby/gems/test-version"', + 'RUBYLIB="{}:{}"'.format(libdir, arch_libdir), + 'GEM_PATH="test-part-path/lib/ruby/gems/test-version"' + } + self.assertThat(set(env), Equals(expected_env)) + + def test_env_with_multiple_ruby(self): + plugin = ruby.RubyPlugin( + 'test-part', self.options, self.project_options) + part_dir = os.path.join(self.path, 'test-part-path') + + os.makedirs( + os.path.join(part_dir, 'lib', 'ruby', 'gems', 'test-version1')) + os.makedirs( + os.path.join(part_dir, 'lib', 'ruby', 'gems', 'test-version2')) + + error = self.assertRaises( + snapcraft.internal.errors.SnapcraftEnvironmentError, + plugin.env, 'test-part-path') + + self.assertThat( + str(error), + Equals('Expected a single Ruby version, but found 2')) + + def test_env_with_rbconfigs(self): + plugin = ruby.RubyPlugin( + 'test-part', self.options, self.project_options) + part_dir = os.path.join(self.path, 'test-part-path') + + os.makedirs( + os.path.join(part_dir, 'lib', 'ruby', 'gems', 'test-version')) + libdir = os.path.join('test-part-path', 'lib', 'ruby', 'test-version') + + for arch in ('foo-linux-bar1', 'foo-linux-bar2'): + arch_libdir1 = os.path.join(libdir, arch) + real_arch_libdir1 = os.path.join(self.path, arch_libdir1) + os.makedirs(real_arch_libdir1) + open(os.path.join(real_arch_libdir1, 'rbconfig.rb'), 'w').close() + + error = self.assertRaises( + snapcraft.internal.errors.SnapcraftEnvironmentError, + plugin.env, 'test-part-path') + + self.assertThat( + str(error), + Equals('Expected a single rbconfig.rb, but found 2')) + + def test_pull_downloads_ruby(self): + plugin = ruby.RubyPlugin( + 'test-part', self.options, self.project_options) + + self.assertThat( + plugin._ruby_tar.source, + Equals('https://cache.ruby-lang.org/pub/ruby/ruby-2.4.2.tar.gz')) + self.assertThat( + plugin._ruby_tar.source_dir, + Equals(os.path.join(self.path, 'parts', 'test-part', 'ruby'))) + + with mock.patch.multiple( + plugin, _ruby_install=mock.DEFAULT, _gem_install=mock.DEFAULT): + # We don't want to wait for the download to finish. It is already + # tested in the Tar object tests. + with mock.patch.object( + plugin._ruby_tar, 'download') as mock_download: + plugin.pull() + + mock_download.assert_called_once_with() + + def test_pull_installs_ruby(self): + plugin = ruby.RubyPlugin( + 'test-part', self.options, self.project_options) + with mock.patch.multiple( + plugin, + _ruby_tar=mock.DEFAULT, + _gem_install=mock.DEFAULT) as mocks: + with mock.patch('subprocess.check_call') as mock_check_call: + plugin.pull() + + ruby_expected_dir = os.path.join( + self.path, 'parts', 'test-part', 'ruby') + mocks['_ruby_tar'].provision.assert_called_with( + ruby_expected_dir, + clean_target=False, keep_tarball=True) + mock_check_call.assert_has_calls([ + mock.call( + [mock.ANY, mock.ANY, + './configure', '--disable-install-rdoc', '--prefix=/'], + cwd=ruby_expected_dir, env=mock.ANY), + mock.call( + [mock.ANY, mock.ANY, + 'make', '-j{}'.format(plugin.parallel_build_count)], + cwd=ruby_expected_dir, env=mock.ANY), + mock.call( + [mock.ANY, mock.ANY, + 'make', 'install', 'DESTDIR={}'.format(plugin.installdir)], + cwd=ruby_expected_dir, env=mock.ANY) + ]) + + def test_pull_installs_gems_without_bundler(self): + self.options.gems = ['test-gem-1', 'test-gem-2'] + plugin = ruby.RubyPlugin( + 'test-part', self.options, self.project_options) + with mock.patch.multiple( + plugin, _ruby_tar=mock.DEFAULT, _ruby_install=mock.DEFAULT): + with mock.patch('subprocess.check_call') as mock_check_call: + plugin.pull() + + test_part_dir = os.path.join(self.path, 'parts', 'test-part') + mock_check_call.assert_called_with( + [mock.ANY, mock.ANY, + os.path.join(test_part_dir, 'install', 'bin', 'ruby'), + os.path.join(test_part_dir, 'install', 'bin', 'gem'), + 'install', '--env-shebang', 'test-gem-1', 'test-gem-2'], + cwd=os.path.join(test_part_dir, 'build'), + env=mock.ANY) + + def test_pull_with_bundler(self): + self.options.gems = ['test-gem-1', 'test-gem-2'] + plugin = ruby.RubyPlugin( + 'test-part', self.options, self.project_options) + self.options.use_bundler = True + + with mock.patch.multiple( + plugin, _ruby_tar=mock.DEFAULT, _ruby_install=mock.DEFAULT): + with mock.patch('subprocess.check_call') as mock_check_call: + plugin.pull() + test_part_dir = os.path.join(self.path, 'parts', 'test-part') + mock_check_call.assert_has_calls([ + mock.call( + [mock.ANY, mock.ANY, + os.path.join(test_part_dir, 'install', 'bin', 'ruby'), + os.path.join(test_part_dir, 'install', 'bin', 'gem'), + 'install', '--env-shebang', + 'test-gem-1', 'test-gem-2', 'bundler'], + cwd=os.path.join(test_part_dir, 'build'), + env=mock.ANY), + mock.call( + [mock.ANY, mock.ANY, + os.path.join(test_part_dir, 'install', 'bin', 'ruby'), + os.path.join(test_part_dir, 'install', 'bin', 'bundle'), + 'install'], + cwd=os.path.join(test_part_dir, 'build'), + env=mock.ANY)]) diff -Nru snapcraft-2.34/snapcraft/tests/plugins/test_rust.py snapcraft-2.35/snapcraft/tests/plugins/test_rust.py --- snapcraft-2.34/snapcraft/tests/plugins/test_rust.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/plugins/test_rust.py 2017-11-01 19:41:33.000000000 +0000 @@ -1,6 +1,7 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # # Copyright (C) 2016-2017 Marius Gripsgard (mariogrip@ubuntu.com) +# Copyright (C) 2016-2017 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 @@ -14,10 +15,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import collections import os - +import subprocess from unittest import mock -from testtools.matchers import DirExists, Equals, FileExists, Not + +from testtools.matchers import ( + Contains, + DirExists, + Equals, + FileExists, + Not +) import snapcraft from snapcraft import tests @@ -58,6 +67,10 @@ self.run_mock = patcher.start() self.addCleanup(patcher.stop) + patcher = mock.patch('snapcraft.internal.common.run_output') + patcher.start() + self.addCleanup(patcher.stop) + patcher = mock.patch('snapcraft.ProjectOptions.is_cross_compiling') patcher.start() self.addCleanup(patcher.stop) @@ -164,25 +177,8 @@ 'but it was "{}"'.format(rust_revision_type)) @mock.patch.object(rust.RustPlugin, 'run') - def test_build(self, run_mock): - plugin = rust.RustPlugin('test-part', self.options, - self.project_options) - os.makedirs(plugin.sourcedir) - - plugin.build() - - self.assertThat(run_mock.call_count, Equals(1)) - run_mock.assert_has_calls([ - mock.call( - [plugin._cargo, 'install', - '-j{}'.format(plugin.project.parallel_build_count), - '--root', plugin.installdir, - '--path', plugin.builddir], - env=plugin._build_env()) - ]) - - @mock.patch.object(rust.RustPlugin, 'run') - def test_build_with_conditional_compilation(self, run_mock): + @mock.patch.object(rust.RustPlugin, 'run_output') + def test_build_with_conditional_compilation(self, _, run_mock): plugin = rust.RustPlugin('test-part', self.options, self.project_options) plugin.options.rust_features = ['conditional-compilation'] @@ -292,3 +288,95 @@ os.makedirs(plugin.sourcedir) self.assertRaises(NotImplementedError, plugin.enable_cross_compilation) + + @mock.patch.object(rust.RustPlugin, 'run') + @mock.patch.object(rust.RustPlugin, 'run_output') + def test_build(self, _, run_mock): + plugin = rust.RustPlugin('test-part', self.options, + self.project_options) + os.makedirs(plugin.sourcedir) + + plugin.build() + + self.assertThat(run_mock.call_count, Equals(1)) + run_mock.assert_has_calls([ + mock.call( + [plugin._cargo, 'install', + '-j{}'.format(plugin.project.parallel_build_count), + '--root', plugin.installdir, + '--path', plugin.builddir], + env=plugin._build_env()) + ]) + + @mock.patch.object(rust.RustPlugin, 'run') + @mock.patch.object(rust.RustPlugin, 'run_output') + def test_get_manifest_with_cargo_lock_file(self, *_): + plugin = rust.RustPlugin('test-part', self.options, + self.project_options) + os.makedirs(plugin.sourcedir) + os.makedirs(plugin.builddir) + + with open(os.path.join( + plugin.builddir, + 'Cargo.lock'), 'w') as cargo_lock_file: + cargo_lock_file.write('test cargo lock contents') + + plugin.build() + + self.assertThat( + plugin.get_manifest()['cargo-lock-contents'], + Equals('test cargo lock contents')) + + @mock.patch.object(rust.RustPlugin, 'run') + @mock.patch.object(rust.RustPlugin, 'run_output') + def test_get_manifest_with_unexisting_cargo_lock(self, *_): + plugin = rust.RustPlugin('test-part', self.options, + self.project_options) + os.makedirs(plugin.sourcedir) + os.makedirs(plugin.builddir) + + plugin.build() + + self.assertThat( + plugin.get_manifest(), Not(Contains('cargo-lock-contents'))) + + @mock.patch.object(rust.RustPlugin, 'run') + @mock.patch.object(rust.RustPlugin, 'run_output') + def test_get_manifest_with_cargo_lock_dir(self, *_): + plugin = rust.RustPlugin('test-part', self.options, + self.project_options) + os.makedirs(plugin.sourcedir) + os.makedirs(plugin.builddir) + + os.mkdir(os.path.join(plugin.builddir, 'Cargo.lock')) + + plugin.build() + + self.assertThat( + plugin.get_manifest(), Not(Contains('cargo-lock-contents'))) + + @mock.patch.object(rust.RustPlugin, 'run') + def test_get_manifest_with_versions(self, _): + plugin = rust.RustPlugin('test-part', self.options, + self.project_options) + os.makedirs(plugin.sourcedir) + + original_check_output = subprocess.check_output + + def side_effect(cmd, *args, **kwargs): + if cmd[-1] == '--version': + binary = os.path.basename(cmd[-2]) + return 'test {} version'.format(binary) + return original_check_output(cmd, *args, **kwargs) + + with mock.patch.object( + rust.RustPlugin, 'run_output') as run_output_mock: + run_output_mock.side_effect = side_effect + plugin.build() + + expected_manifest = collections.OrderedDict() + expected_manifest['rustup-version'] = 'test rustup.sh version' + expected_manifest['rustc-version'] = 'test rustc version' + expected_manifest['cargo-version'] = 'test cargo version' + + self.assertThat(plugin.get_manifest(), Equals(expected_manifest)) diff -Nru snapcraft-2.34/snapcraft/tests/project_loader/test_config.py snapcraft-2.35/snapcraft/tests/project_loader/test_config.py --- snapcraft-2.34/snapcraft/tests/project_loader/test_config.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/project_loader/test_config.py 2017-11-01 19:41:33.000000000 +0000 @@ -69,6 +69,8 @@ self.addCleanup(patcher.stop) def test_yaml_aliases(self,): + fake_logger = fixtures.FakeLogger(level=logging.WARNING) + self.useFixture(fake_logger) self.make_snapcraft_yaml("""name: test version: "1" @@ -94,6 +96,13 @@ self.assertThat( c.data['apps']['test']['aliases'], Equals(['test-it', 'testing'])) + # Verify that aliases are properly deprecated + self.assertThat(fake_logger.output, Contains( + "Aliases are now handled by the store, and shouldn't be declared " + 'in the snap.')) + self.assertThat(fake_logger.output, Contains( + "See http://snapcraft.io/docs/deprecation-notices/dn5")) + def test_yaml_aliases_with_duplicates(self): fake_logger = fixtures.FakeLogger(level=logging.ERROR) self.useFixture(fake_logger) @@ -420,7 +429,10 @@ self.assertThat( raised.message, Equals("The 'name' property does not match the required " - "schema: 1 is not of type 'string'")) + "schema: 1 is not a valid snap name. Snap names " + "consist of lower-case alphanumeric characters and " + "hyphens. They cannot be all numbers. They also cannot " + "start or end with a hyphen.")) def test_invalid_yaml_invalid_icon_extension(self): fake_logger = fixtures.FakeLogger(level=logging.ERROR) @@ -493,7 +505,10 @@ self.assertThat( raised.message, Equals("The 'name' property does not match the required schema: " - "'myapp@me_1.0' does not match '^[a-z0-9][a-z0-9+-]*$'")) + "'myapp@me_1.0' is not a valid snap name. Snap names " + "consist of lower-case alphanumeric characters and " + "hyphens. They cannot be all numbers. They also cannot " + "start or end with a hyphen.")) def test_invalid_yaml_missing_description(self): fake_logger = fixtures.FakeLogger(level=logging.ERROR) @@ -820,7 +835,35 @@ self.assertThat( raised.message, Equals("The 'version' property does not match the required " - "schema: '' does not match '^[a-zA-Z0-9.+~-]+$'")) + "schema: '' is not a valid snap version. Snap versions " + "consist of upper- and lower-case alphanumeric characters, " + "as well as periods, plus signs, tildes, and hyphens.")) + + def test_invalid_yaml_invalid_version(self): + fake_logger = fixtures.FakeLogger(level=logging.ERROR) + self.useFixture(fake_logger) + + self.make_snapcraft_yaml(dedent(""" + name: test + version: '*' + summary: test + description: test + confinement: strict + grade: stable + parts: + part1: + plugin: nil + """)) + raised = self.assertRaises( + errors.YamlValidationError, + _config.Config) + + self.assertThat( + raised.message, + Equals("The 'version' property does not match the required " + "schema: '*' is not a valid snap version. Snap versions " + "consist of upper- and lower-case alphanumeric characters, " + "as well as periods, plus signs, tildes, and hyphens.")) def test_invalid_yaml_version_too_long(self): fake_logger = fixtures.FakeLogger(level=logging.ERROR) @@ -1109,8 +1152,50 @@ self.assertRegex( raised.message, - "The 'apps' property does not match the required " - "schema.*") + "The 'apps' property does not match the required schema: .* is " + 'not a valid app name. App names consist of upper- and lower-case ' + 'alphanumeric characters and hyphens') + + +class InvalidHookNamesYamlTestCase(YamlBaseTestCase): + + scenarios = [ + (name, dict(name=name)) for + name in [ + '', '-', '--', 'a--a', 'a-', 'a ', ' a', 'a a', '日本語', '한글', + 'ру́сский язы́к', 'ໄຂ່​ອີ​ສ​ເຕີ້', ':a', 'a:', 'a:a', '_a', 'a_', + 'a_a', 'Hi', + ] + ] + + def test_invalid_yaml_invalid_hook_names(self): + fake_logger = fixtures.FakeLogger(level=logging.ERROR) + self.useFixture(fake_logger) + + self.make_snapcraft_yaml("""name: test +version: "1" +summary: test +description: nothing +confinement: strict +grade: stable + +hooks: + {!r}: + plugs: [network] + +parts: + part1: + plugin: nil +""".format(self.name)) + raised = self.assertRaises( + errors.YamlValidationError, + _config.Config) + + self.assertRegex( + raised.message, + "The 'hooks' property does not match the required schema: .* is " + 'not a valid hook name. Hook names consist of lower-case ' + 'alphanumeric characters and hyphens') class ValidConfinmentTypesYamlTestCase(YamlBaseTestCase): @@ -1617,6 +1702,7 @@ 'snapcraft._options.ProjectOptions.get_core_dynamic_linker') mock_core_dynamic_linker = patcher.start() mock_core_dynamic_linker.return_value = dynamic_linker + self.addCleanup(patcher.stop) self.make_snapcraft_yaml("""name: test version: "1" @@ -1706,7 +1792,7 @@ stage_dir=self.stage_dir, arch_triplet=self.arch_triplet) in environment, 'Current environment is {!r}'.format(environment)) - self.assertTrue('PERL5LIB={}/usr/share/perl5/'.format( + self.assertTrue('PERL5LIB="{}/usr/share/perl5/"'.format( self.stage_dir) in environment) def test_parts_build_env_ordering_with_deps(self): @@ -1907,19 +1993,6 @@ project_loader.Validator(self.data).validate() - def test_invalid_part_name_plugin_raises_exception(self): - self.data['parts']['plugins'] = {'type': 'go'} - - raised = self.assertRaises( - errors.YamlValidationError, - project_loader.Validator(self.data).validate) - - expected_message = ("The 'parts' property does not match the " - "required schema: Additional properties are not " - "allowed ('plugins' was unexpected)") - self.assertThat(raised.message, Equals(expected_message), - message=self.data) - def test_valid_app_daemons(self): self.data['apps'] = { 'service1': {'command': 'binary1 start', 'daemon': 'simple'}, @@ -2036,7 +2109,10 @@ class InvalidNamesTestCase(ValidationBaseTestCase): scenarios = [(name, dict(name=name)) for - name in ['package@awesome', 'something.another', '_hideme']] + name in [ + 'package@awesome', 'something.another', '_hideme', '-no', + 'a:a', '123' + ]] def test_invalid_names(self): data = self.data.copy() @@ -2046,9 +2122,11 @@ errors.YamlValidationError, project_loader.Validator(data).validate) - expected_message = ("The 'name' property does not match the " - "required schema: '{}' does not match " - "'^[a-z0-9][a-z0-9+-]*$'").format(self.name) + expected_message = ( + "The 'name' property does not match the required schema: '{}' is " + "not a valid snap name. Snap names consist of lower-case " + "alphanumeric characters and hyphens. They cannot be all numbers. " + "They also cannot start or end with a hyphen.").format(self.name) self.assertThat(raised.message, Equals(expected_message), message=data) @@ -2116,9 +2194,57 @@ project_loader.Validator(data).validate) expected_message = ( - "The 'apps' property does not match the required " - "schema: Additional properties are not allowed ('{}' " - "was unexpected)").format(self.name) + "The 'apps' property does not match the required schema: {!r} is " + "not a valid app name. App names consist of upper- and lower-case " + "alphanumeric characters and hyphens. They cannot start or end " + "with a hyphen.").format(self.name) + self.assertThat(raised.message, Equals(expected_message), + message=data) + + +class InvalidHookNamesTestCase(ValidationBaseTestCase): + + scenarios = [(name, dict(name=name)) for + name in ['qwe#rty', 'qwe_rty', 'que rty', 'que rty', 'Hi']] + + def test_invalid_app_names(self): + data = self.data.copy() + data['hooks'] = {self.name: {'plugs': ['network']}} + + raised = self.assertRaises( + errors.YamlValidationError, + project_loader.Validator(data).validate) + + expected_message = ( + "The 'hooks' property does not match the required schema: {!r} is " + "not a valid hook name. Hook names consist of lower-case " + "alphanumeric characters and hyphens. They cannot start or end " + "with a hyphen.").format( + self.name) + self.assertThat(raised.message, Equals(expected_message), + message=data) + + +class InvalidPartNamesTestCase(ValidationBaseTestCase): + + scenarios = [(name, dict(name=name)) for + name in [ + 'plugins', 'qwe#rty', 'qwe_rty', 'que rty', 'que rty']] + + def test_invalid_part_names(self): + data = self.data.copy() + data['parts'] = {self.name: {'plugin': 'nil'}} + + raised = self.assertRaises( + errors.YamlValidationError, + project_loader.Validator(data).validate) + + expected_message = ( + "The 'parts' property does not match the required schema: {!r} is " + "not a valid part name. Part names consist of lower-case " + "alphanumeric characters, hyphens, plus signs, and forward " + "slashes. As a special case, 'plugins' is also not a valid part " + "name.").format(self.name) self.assertThat(raised.message, Equals(expected_message), message=data) diff -Nru snapcraft-2.34/snapcraft/tests/project_loader/test_parts.py snapcraft-2.35/snapcraft/tests/project_loader/test_parts.py --- snapcraft-2.34/snapcraft/tests/project_loader/test_parts.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/project_loader/test_parts.py 2017-11-01 19:41:33.000000000 +0000 @@ -18,6 +18,7 @@ import os import unittest import unittest.mock +from textwrap import dedent import fixtures from testtools.matchers import Contains, Equals @@ -111,3 +112,29 @@ self.assertThat(fake_logger.output, Contains(deprecations._deprecation_message('dn1'))) + + +class PartsWithDummyRepoTestCase(tests.TestCase): + + @unittest.mock.patch( + 'snapcraft.internal.project_loader._parts_config.repo.Repo', + wraps=snapcraft.internal.repo._base.DummyRepo) + def test_load_with_dummy_repo(self, os_release_mock): + self.make_snapcraft_yaml(dedent("""\ + name: test + version: "1" + summary: test + description: test + confinement: strict + + parts: + part1: + source: https://github.com/snapcore/snapcraft.git + plugin: python + stage-packages: [fswebcam] + snap: [foo] + """)) + + # Ensure the dummy repo returns an empty set for required + # build tools. + project_loader.load_config() diff -Nru snapcraft-2.34/snapcraft/tests/repo/test_base.py snapcraft-2.35/snapcraft/tests/repo/test_base.py --- snapcraft-2.34/snapcraft/tests/repo/test_base.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/repo/test_base.py 2017-11-01 19:41:33.000000000 +0000 @@ -132,8 +132,8 @@ }), ('python3 bin dir', { 'file_path': os.path.join('root', 'bin', 'd'), - 'content': '#!/usr/bin/python3\nraise Exception()', - 'expected': '#!/usr/bin/python3\nraise Exception()', + 'content': '#!/usr/bin/python3\nimport this', + 'expected': '#!/usr/bin/env python3\nimport this', }), ('sbin dir', { 'file_path': os.path.join('root', 'sbin', 'b'), diff -Nru snapcraft-2.34/snapcraft/tests/repo/test_deb.py snapcraft-2.35/snapcraft/tests/repo/test_deb.py --- snapcraft-2.34/snapcraft/tests/repo/test_deb.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/repo/test_deb.py 2017-11-01 19:41:33.000000000 +0000 @@ -223,6 +223,27 @@ self.assertFalse(mock_cc.called) +class UbuntuTestCaseWithFakeAptCache(RepoBaseTestCase): + + def setUp(self): + super().setUp() + self.fake_apt_cache = fixture_setup.FakeAptCache() + self.useFixture(self.fake_apt_cache) + + def test_get_installed_packages(self): + for name, version, installed in ( + ('test-installed-package', 'test-installed-package-version', + True), + ('test-not-installed-package', 'dummy', False)): + self.fake_apt_cache.add_package( + fixture_setup.FakeAptCachePackage( + name, version, installed=installed)) + + self.assertThat( + repo.Repo.get_installed_packages(), + Equals(['test-installed-package=test-installed-package-version'])) + + class BuildPackagesTestCase(tests.TestCase): def setUp(self): diff -Nru snapcraft-2.34/snapcraft/tests/repo/test_snaps.py snapcraft-2.35/snapcraft/tests/repo/test_snaps.py --- snapcraft-2.34/snapcraft/tests/repo/test_snaps.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/repo/test_snaps.py 2017-11-01 19:41:33.000000000 +0000 @@ -13,11 +13,8 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import os -import socketserver + import subprocess -import tempfile -from threading import Thread from unittest import mock import fixtures @@ -25,7 +22,7 @@ from snapcraft import tests from snapcraft.internal.repo import errors, snaps -from snapcraft.tests.fake_servers.snapd import FakeSnapdServer +from snapcraft.tests import fixture_setup class FakeSnapCommand(fixtures.Fixture): @@ -92,50 +89,13 @@ return 'email: {}'.format(self._email).encode() -class UnixHTTPServer(socketserver.UnixStreamServer): - - def get_request(self): - request, client_address = self.socket.accept() - # BaseHTTPRequestHandler expects a tuple with the client address at - # index 0, so we fake one - if len(client_address) == 0: - client_address = (self.server_address,) - return (request, client_address) - - -class FakeSnapd(fixtures.Fixture): - - def _setUp(self): - snapd_fake_socket_path = tempfile.mkstemp()[1] - os.unlink(snapd_fake_socket_path) - - socket_path_patcher = mock.patch( - 'snapcraft.internal.repo.snaps.get_snapd_socket_path_template') - mock_socket_path = socket_path_patcher.start() - mock_socket_path.return_value = 'http+unix://{}/v2/{{}}'.format( - snapd_fake_socket_path.replace('/', '%2F')) - self.addCleanup(socket_path_patcher.stop) - - self._start_fake_server(snapd_fake_socket_path) - - def _start_fake_server(self, socket): - self.server = UnixHTTPServer(socket, FakeSnapdServer) - server_thread = Thread(target=self.server.serve_forever) - server_thread.start() - self.addCleanup(self._stop_fake_server, server_thread) - - def _stop_fake_server(self, thread): - self.server.shutdown() - self.server.socket.close() - thread.join() - - class SnapPackageBaseTestCase(tests.TestCase): def setUp(self): super().setUp() - self.useFixture(FakeSnapd()) + self.fake_snapd = fixture_setup.FakeSnapd() + self.useFixture(self.fake_snapd) class SnapPackageCurrentChannelTest(SnapPackageBaseTestCase): @@ -143,25 +103,39 @@ scenarios = [ ('stable', dict(snap='fake-snap-stable/stable', + installed_snaps=[ + {'name': 'fake-snap-stable', 'channel': 'stable'}], expected='latest/stable')), ('latest/stable', dict(snap='fake-snap-stable/latest/stable', + installed_snaps=[ + {'name': 'fake-snap-stable', 'channel': 'stable'}], expected='latest/stable')), ('candidate/branch', dict(snap='fake-snap-branch/candidate/branch', + installed_snaps=[ + {'name': 'fake-snap-branch', 'channel': 'candidate/branch'}], expected='latest/candidate/branch')), ('track/stable/branch', dict(snap='fake-snap-track-stable-branch/track/stable/branch', + installed_snaps=[ + {'name': 'fake-snap-track-stable-branch', + 'channel': 'track/stable/branch'}], expected='track/stable/branch')), ('edge', dict(snap='fake-snap-edge/stable', + installed_snaps=[{'name': 'fake-snap-edge', 'channel': 'edge'}], expected='latest/edge')), ('track/stable', dict(snap='fake-snap-track-stable/track/stable', + installed_snaps=[ + {'name': 'fake-snap-track-stable', + 'channel': 'track/stable'}], expected='track/stable')), ] def test_get_current_channel(self): + self.fake_snapd.snaps_result = self.installed_snaps snap_pkg = snaps.SnapPackage(self.snap) self.assertThat(snap_pkg.get_current_channel(), Equals(self.expected)) @@ -172,23 +146,31 @@ scenarios = [ ('installed stable', dict(snap='fake-snap-stable', + installed_snaps=[ + {'name': 'fake-snap-stable', 'channel': 'stable'}], expected=True)), ('installed stable with channel', dict(snap='fake-snap-stable/latest/stable', + installed_snaps=[ + {'name': 'fake-snap-stable', 'channel': 'stable'}], expected=True)), ('not installed', dict(snap='missing-snap', + installed_snaps=[], expected=False)), ('not installed with channel', dict(snap='missing-snap/latest/stable', + installed_snaps=[], expected=False)), ] def test_is_installed(self): + self.fake_snapd.snaps_result = self.installed_snaps snap_pkg = snaps.SnapPackage(self.snap) self.assertThat(snap_pkg.installed, Is(self.expected)) def test_is_installed_classmethod(self): + self.fake_snapd.snaps_result = self.installed_snaps self.assertThat(snaps.SnapPackage.is_snap_installed(self.snap), Is(self.expected)) @@ -198,19 +180,24 @@ scenarios = [ ('in store', dict(snap='fake-snap', + find_result=[{'fake-snap': 'dummy'}], expected=True)), ('in store with channel', dict(snap='fake-snap/latest/stable', + find_result=[{'fake-snap': 'dummy'}], expected=True)), ('not in store', dict(snap='missing-snap', + find_result=[], expected=False)), ('not in store with channel', dict(snap='missing-snap/latest/stable', + find_result=[], expected=False)), ] def test_is_in_store(self): + self.fake_snapd.find_result = self.find_result snap_pkg = snaps.SnapPackage(self.snap) self.assertThat(snap_pkg.in_store, Is(self.expected)) @@ -218,12 +205,28 @@ class SnapPackageIsClassicTest(SnapPackageBaseTestCase): scenarios = [ - ('classic', dict(snap='fake-snap/classic/stable', expected=True)), - ('strict', dict(snap='fake-snap/strict/stable', expected=False)), - ('devmode', dict(snap='fake-snap/devmode/stable', expected=False)), + ('classic', dict( + snap='fake-snap/classic/stable', + find_result=[{ + 'fake-snap': {'channels': { + 'classic/stable': {'confinement': 'classic'}}}}], + expected=True)), + ('strict', dict( + snap='fake-snap/strict/stable', + find_result=[{ + 'fake-snap': {'channels': { + 'strict/stable': {'confinement': 'strict'}}}}], + expected=False)), + ('devmode', dict( + snap='fake-snap/devmode/stable', + find_result=[{ + 'fake-snap': {'channels': { + 'devmode/stable': {'confinement': 'devmode'}}}}], + expected=False)), ] def test_is_classic(self): + self.fake_snapd.find_result = self.find_result snap_pkg = snaps.SnapPackage(self.snap) self.assertThat(snap_pkg.is_classic(), Is(self.expected)) @@ -231,22 +234,35 @@ class SnapPackageIsValidTest(SnapPackageBaseTestCase): scenarios = [ - ('valid', - dict(snap='fake-snap', expected=True)), - ('valid with channel', - dict(snap='fake-snap/strict/stable', expected=True)), - ('invalid', - dict(snap='missing-snap', expected=False)), - ('invalid with channel', - dict(snap='missing-snap/strict/stable', expected=False)), - + ('valid', dict( + snap='fake-snap', + find_result=[{ + 'fake-snap': {'channels': { + 'latest/stable': {'confinement': 'strict'}}}}], + expected=True)), + ('valid with channel', dict( + snap='fake-snap/strict/stable', + find_result=[{ + 'fake-snap': {'channels': { + 'strict/stable': {'confinement': 'strict'}}}}], + expected=True)), + ('invalid', dict( + snap='missing-snap', + find_result=[], + expected=False)), + ('invalid with channel', dict( + snap='missing-snap/strict/stable', + find_result=[], + expected=False)), ] def test_is_valid(self): + self.fake_snapd.find_result = self.find_result snap_pkg = snaps.SnapPackage(self.snap) self.assertThat(snap_pkg.is_valid(), Is(self.expected)) def test_is_valid_classmethod(self): + self.fake_snapd.find_result = self.find_result self.assertThat(snaps.SnapPackage.is_valid_snap(self.snap), Is(self.expected)) @@ -259,6 +275,10 @@ self.useFixture(self.fake_snap_command) def test_install_classic(self): + self.fake_snapd.find_result = [{ + 'fake-snap': {'channels': { + 'classic/stable': {'confinement': 'classic'}}}}] + snap_pkg = snaps.SnapPackage('fake-snap/classic/stable') snap_pkg.install() self.assertThat(self.fake_snap_command.calls, Equals([ @@ -267,6 +287,10 @@ '--channel', 'classic/stable', '--classic']])) def test_install_non_classic(self): + self.fake_snapd.find_result = [{ + 'fake-snap': {'channels': { + 'strict/stable': {'confinement': 'strict'}}}}] + snap_pkg = snaps.SnapPackage('fake-snap/strict/stable') snap_pkg.install() self.assertThat(self.fake_snap_command.calls, Equals([ @@ -275,6 +299,10 @@ '--channel', 'strict/stable']])) def test_install_logged_in(self): + self.fake_snapd.find_result = [{ + 'fake-snap': {'channels': { + 'strict/stable': {'confinement': 'strict'}}}}] + self.fake_snap_command.login('user@email.com') snap_pkg = snaps.SnapPackage('fake-snap/strict/stable') snap_pkg.install() @@ -284,11 +312,19 @@ '--channel', 'strict/stable']])) def test_install_fails(self): + self.fake_snapd.find_result = [{ + 'fake-snap': {'channels': { + 'strict/stable': {'confinement': 'strict'}}}}] + self.fake_snap_command.install_success = False snap_pkg = snaps.SnapPackage('fake-snap/strict/stable') self.assertRaises(errors.SnapInstallError, snap_pkg.install) def test_refresh(self): + self.fake_snapd.find_result = [{ + 'fake-snap': {'channels': { + 'strict/stable': {'confinement': 'strict'}}}}] + snap_pkg = snaps.SnapPackage('fake-snap/strict/stable') snap_pkg.refresh() self.assertThat(self.fake_snap_command.calls, Equals([ @@ -297,6 +333,10 @@ '--channel', 'strict/stable']])) def test_refresh_to_classic(self): + self.fake_snapd.find_result = [{ + 'fake-snap': {'channels': { + 'classic/stable': {'confinement': 'classic'}}}}] + snap_pkg = snaps.SnapPackage('fake-snap/classic/stable') snap_pkg.refresh() self.assertThat(self.fake_snap_command.calls, Equals([ @@ -305,6 +345,10 @@ '--channel', 'classic/stable', '--classic']])) def test_refresh_logged_in(self): + self.fake_snapd.find_result = [{ + 'fake-snap': {'channels': { + 'strict/stable': {'confinement': 'strict'}}}}] + self.fake_snap_command.login('user@email.com') snap_pkg = snaps.SnapPackage('fake-snap/strict/stable') snap_pkg.refresh() @@ -314,11 +358,48 @@ '--channel', 'strict/stable']])) def test_refresh_fails(self): + self.fake_snapd.find_result = [{ + 'fake-snap': {'channels': { + 'strict/stable': {'confinement': 'strict'}}}}] + snap_pkg = snaps.SnapPackage('fake-snap/strict/stable') self.fake_snap_command.refresh_success = False self.assertRaises(errors.SnapRefreshError, snap_pkg.refresh) + def test_install_snaps_returns_revision(self): + self.fake_snapd.find_result = [{ + 'fake-snap': {'channels': { + 'latest/stable': {'confinement': 'strict'}}}}] + self.fake_snapd.snaps_result = [ + {'name': 'fake-snap', + 'channel': 'stable', + 'revision': 'test-fake-snap-revision'}] + + installed_snaps = snaps.install_snaps(['fake-snap']) + self.assertThat( + installed_snaps, + Equals(['fake-snap=test-fake-snap-revision'])) + def test_install_multiple_snaps(self): + self.fake_snapd.find_result = [{ + 'fake-snap': {'channels': { + 'classic/stable': {'confinement': 'classic'}}}}] + + def snap_details(handler_instance, snap_name): + if snap_name == 'fake-snap': + return (200, {'channel': 'stable', 'revision': 'dummy'}) + # XXX The query for the new-fake-snap details must fail the first + # time, but succeed the second. + elif snap_name == 'new-fake-snap': + if not handler_instance._private_data[ + 'new_fake_snap_installed']: + handler_instance._private_data[ + 'new_fake_snap_installed'] = True + return (404, {}) + else: + return (200, {'channel': 'stable', 'revision': 'dummy'}) + + self.fake_snapd.snap_details_func = snap_details snaps.install_snaps([ 'fake-snap/classic/stable', 'new-fake-snap' @@ -329,3 +410,34 @@ '--channel', 'classic/stable', '--classic'], ['snap', 'whoami'], ['sudo', 'snap', 'install', 'new-fake-snap']])) + + +class InstalledSnapsTestCase(SnapPackageBaseTestCase): + + def test_get_installed_snaps(self): + self.fake_snapd.snaps_result = [ + {'name': 'test-snap-1', + 'revision': 'test-snap-1-revision'}, + {'name': 'test-snap-2', + 'revision': 'test-snap-2-revision'}, + ] + installed_snaps = snaps.get_installed_snaps() + self.assertThat( + installed_snaps, + Equals(['test-snap-1=test-snap-1-revision', + 'test-snap-2=test-snap-2-revision'])) + + +class SnapdNotInstalledTestCase(tests.TestCase): + + def setUp(self): + super().setUp() + socket_path_patcher = mock.patch( + 'snapcraft.internal.repo.snaps.get_snapd_socket_path_template') + mock_socket_path = socket_path_patcher.start() + mock_socket_path.return_value = 'http+unix://nonexisting' + self.addCleanup(socket_path_patcher.stop) + + def test_get_installed_snaps(self): + installed_snaps = snaps.get_installed_snaps() + self.assertThat(installed_snaps, Equals([])) diff -Nru snapcraft-2.34/snapcraft/tests/skip.py snapcraft-2.35/snapcraft/tests/skip.py --- snapcraft-2.34/snapcraft/tests/skip.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/skip.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,40 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2017 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 . + +import contextlib +import functools + +from unittest import skipUnless + +from snapcraft.internal import ( + errors, + os_release, +) + + +def skip_unless_codename(codename, message): + def _wrap(func): + release = os_release.OsRelease() + actual_codename = None + with contextlib.suppress(errors.OsReleaseCodenameError): + actual_codename = release.version_codename() + + @functools.wraps(func) + @skipUnless(actual_codename == codename, message) + def _skip_test(*args, **kwargs): + func(*args, **kwargs) + return _skip_test + return _wrap diff -Nru snapcraft-2.34/snapcraft/tests/sources/test_deb.py snapcraft-2.35/snapcraft/tests/sources/test_deb.py --- snapcraft-2.34/snapcraft/tests/sources/test_deb.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/sources/test_deb.py 2017-11-01 19:41:33.000000000 +0000 @@ -29,8 +29,14 @@ def setUp(self): super().setUp() - patcher = mock.patch('debian.debfile.DebFile') - self.mock_deb = patcher.start() + patcher = mock.patch('debian.arfile.ArFile') + self.mock_ar = patcher.start() + self.mock_ar.return_value.getnames.return_value = [ + 'data.tar.gz', 'control.tar.gz', 'debian-binary'] + self.addCleanup(patcher.stop) + + patcher = mock.patch('tarfile.open') + patcher.start() self.addCleanup(patcher.stop) def test_pull_debfile_must_download_and_extract(self): @@ -43,7 +49,7 @@ deb_source.pull() - self.mock_deb.assert_called_once_with( + self.mock_ar.assert_called_once_with( os.path.join(deb_source.source_dir, deb_file_name)) def test_extract_and_keep_debfile(self): @@ -57,7 +63,7 @@ deb_source.provision(dst=dest_dir, keep_deb=True) deb_download = os.path.join(deb_source.source_dir, deb_file_name) - self.mock_deb.assert_called_once_with( + self.mock_ar.assert_called_once_with( os.path.join(deb_source.source_dir, deb_file_name)) with open(deb_download, 'r') as deb_file: @@ -68,3 +74,18 @@ self.assertTrue(sources._source_handler['deb'] is sources.Deb) else: self.assertRaises(KeyError, sources._source_handler['deb']) + + def test_invalid_deb(self): + self.mock_ar.return_value.getnames.return_value = [ + 'control.tar.gz', 'debian-binary'] + + deb_file_name = 'test.deb' + source = 'http://{}:{}/{file_name}'.format( + *self.server.server_address, file_name=deb_file_name) + dest_dir = os.path.abspath(os.curdir) + deb_source = sources.Deb(source, dest_dir) + + deb_source.download() + + self.assertRaises(sources.errors.InvalidDebError, + deb_source.provision, dst=dest_dir, keep_deb=True) diff -Nru snapcraft-2.34/snapcraft/tests/sources/test_zip.py snapcraft-2.35/snapcraft/tests/sources/test_zip.py --- snapcraft-2.34/snapcraft/tests/sources/test_zip.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/sources/test_zip.py 2017-11-01 19:41:33.000000000 +0000 @@ -37,7 +37,7 @@ zip_source.pull() mock_zip.assert_called_once_with( - os.path.join(zip_source.source_dir, zip_file_name)) + os.path.join(zip_source.source_dir, zip_file_name), 'r') @mock.patch('zipfile.ZipFile') def test_extract_and_keep_zipfile(self, mock_zip): @@ -51,7 +51,7 @@ zip_source.provision(dst=dest_dir, keep_zip=True) zip_download = os.path.join(zip_source.source_dir, zip_file_name) - mock_zip.assert_called_once_with(zip_download) + mock_zip.assert_called_once_with(zip_download, 'r') with open(zip_download, 'r') as zip_file: self.assertThat(zip_file.read(), Equals('Test fake file')) diff -Nru snapcraft-2.34/snapcraft/tests/states/test_build.py snapcraft-2.35/snapcraft/tests/states/test_build.py --- snapcraft-2.34/snapcraft/tests/states/test_build.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/states/test_build.py 2017-11-01 19:41:33.000000000 +0000 @@ -57,11 +57,14 @@ 'build-attributes': ['test-build-attribute'], 'build-packages': 'test-build-packages', 'disable-parallel': 'test-disable-parallel', - 'organize': {'baz': 'qux'} + 'organize': {'baz': 'qux'}, + 'prepare': 'touch prepare', + 'build': 'touch build', + 'install': 'touch install', }) properties = self.state.properties_of_interest(self.part_properties) - self.assertThat(len(properties), Equals(6)) + self.assertThat(len(properties), Equals(9)) self.assertThat(properties['foo'], Equals('bar')) self.assertThat(properties['after'], Equals('test-after')) self.assertThat( @@ -71,6 +74,9 @@ self.assertThat( properties['disable-parallel'], Equals('test-disable-parallel')) self.assertThat(properties['organize'], Equals({'baz': 'qux'})) + self.assertThat(properties['prepare'], Equals('touch prepare')) + self.assertThat(properties['build'], Equals('touch build')) + self.assertThat(properties['install'], Equals('touch install')) def test_project_options_of_interest(self): options = self.state.project_options_of_interest(self.project) diff -Nru snapcraft-2.34/snapcraft/tests/store/test_edit_collaborators.py snapcraft-2.35/snapcraft/tests/store/test_edit_collaborators.py --- snapcraft-2.34/snapcraft/tests/store/test_edit_collaborators.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/store/test_edit_collaborators.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,111 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017 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 . - -from unittest import mock - -import fixtures -from testtools.matchers import Equals - -from snapcraft import tests, _store - - -class EditCollaboratorsTestCase(tests.TestCase): - - def setUp(self): - super().setUp() - patcher = mock.patch('subprocess.check_call') - patcher.start() - self.addCleanup(patcher.stop) - - def test_edit_collaborators_must_write_header_and_developers(self): - developers_from_assertion = [ - {'developer-id': 'test-dev-id1', - 'since': '2017-02-10T08:35:00.000000Z', - 'until': '2018-02-10T08:35:00.000000Z'}, - {'developer-id': 'test-dev-id2', - 'since': '2016-02-10T08:35:00.000000Z', - 'until': '2019-02-10T08:35:00.000000Z'}, - ] - - existing_developers = ("developers:\n" - "- developer-id: test-dev-id1\n" - " since: '2017-02-10 08:35:00'\n" - " until: '2018-02-10 08:35:00'\n" - "- developer-id: test-dev-id2\n" - " since: '2016-02-10 08:35:00'\n" - " until: '2019-02-10 08:35:00'\n") - expected_written = (_store._COLLABORATION_HEADER + '\n' + - existing_developers) - - with mock.patch( - 'builtins.open', - new_callable=mock.mock_open, - read_data=expected_written) as mock_open: - developers_for_assertion = _store._edit_collaborators( - developers_from_assertion) - - written = '' - for call in mock_open().write.call_args_list: - written += str(call.call_list()[0][0][0]) - self.assertThat(written, Equals(expected_written)) - self.assertThat( - developers_for_assertion, Equals(developers_from_assertion)) - - def test_edit_collaborators_must_return_new_developers(self): - developers_from_assertion = [ - {'developer-id': 'test-dev-id1', - 'since': '2017-02-10T08:35:00.000000Z', - 'until': '2018-02-10T08:35:00.000000Z'}, - ] - - new_developers = ("developers:\n" - "- developer-id: test-dev-id1\n" - " since: '2017-02-10 08:35:00'\n" - " until: '2018-02-10 08:35:00'\n" - "- developer-id: test-dev-id2\n" - " since: '2016-02-10 08:35:00'\n" - " until: '2019-02-10 08:35:00'\n") - - with mock.patch('builtins.open', new_callable=mock.mock_open, - read_data=new_developers): - developers_for_assertion = _store._edit_collaborators( - developers_from_assertion) - - expected_developers = developers_from_assertion + [{ - 'developer-id': 'test-dev-id2', - 'since': '2016-02-10T08:35:00.000000Z', - 'until': '2019-02-10T08:35:00.000000Z', - }] - self.assertThat(developers_for_assertion, Equals(expected_developers)) - - -class EditCollaboratorsOpenEditorTestCase(tests.TestCase): - - scenarios = ( - ('default', {'editor': None, 'expected': 'vi'}), - ('non-default', {'editor': 'test-editor', 'expected': 'test-editor'}) - ) - - def setUp(self): - super().setUp() - patcher = mock.patch('subprocess.check_call') - self.check_call_mock = patcher.start() - self.addCleanup(patcher.stop) - - def test_edit_collaborators_must_open_editor(self): - self.useFixture(fixtures.EnvironmentVariable('EDITOR', self.editor)) - _store._edit_collaborators({}) - self.check_call_mock.assert_called_with([self.expected, mock.ANY]) diff -Nru snapcraft-2.34/snapcraft/tests/store/test_store_client.py snapcraft-2.35/snapcraft/tests/store/test_store_client.py --- snapcraft-2.34/snapcraft/tests/store/test_store_client.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/store/test_store_client.py 2017-11-01 19:41:33.000000000 +0000 @@ -318,7 +318,7 @@ 'price': None, 'since': '2016-12-12T01:01:01Z', }, - 'ubuntu-core': { + 'core': { 'snap-id': 'good', 'status': 'Approved', 'private': False, @@ -339,6 +339,20 @@ 'price': None, 'since': '2016-12-12T01:01:01Z', }, + 'no-revoked': { + 'snap-id': 'no-revoked', + 'status': 'Approved', + 'private': False, + 'price': None, + 'since': '2016-12-12T01:01:01Z', + }, + 'revoked': { + 'snap-id': 'revoked', + 'status': 'Approved', + 'private': False, + 'price': None, + 'since': '2016-12-12T01:01:01Z', + }, 'test-snap-with-dev': { 'price': None, 'private': False, @@ -374,7 +388,7 @@ 'price': None, 'since': '2016-12-12T01:01:01Z', }, - 'ubuntu-core': { + 'core': { 'snap-id': 'good', 'status': 'Approved', 'private': False, @@ -395,6 +409,20 @@ 'price': None, 'since': '2016-12-12T01:01:01Z', }, + 'no-revoked': { + 'snap-id': 'no-revoked', + 'status': 'Approved', + 'private': False, + 'price': None, + 'since': '2016-12-12T01:01:01Z', + }, + 'revoked': { + 'snap-id': 'revoked', + 'status': 'Approved', + 'private': False, + 'price': None, + 'since': '2016-12-12T01:01:01Z', + }, 'test-snap-with-dev': { 'price': None, 'private': False, @@ -874,6 +902,18 @@ errors.InvalidCredentialsError, self.client.release, 'test-snap', '10', ['beta']) + def test_release_with_invalid_revision(self): + self.client.login('dummy', 'test correct password') + raised = self.assertRaises( + errors.StoreReleaseError, + self.client.release, + 'test-snap-invalid-data', 'notanumber', ['beta']) + + self.assertThat( + str(raised), + Equals( + 'invalid-field: The \'revision\' field must be an integer\n')) + class CloseChannelsTestCase(StoreTestCase): diff -Nru snapcraft-2.34/snapcraft/tests/test_fixture_setup.py snapcraft-2.35/snapcraft/tests/test_fixture_setup.py --- snapcraft-2.34/snapcraft/tests/test_fixture_setup.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/test_fixture_setup.py 2017-11-01 19:41:33.000000000 +0000 @@ -17,14 +17,30 @@ import http.client import http.server import json +import os +import tempfile import urllib.parse +import fixtures from testtools.matchers import Equals from snapcraft import tests from snapcraft.tests import fixture_setup +class TempCWDTestCase(tests.TestCase): + + def test_with_TEMPDIR_env_var(self): + with tempfile.TemporaryDirectory() as test_tmp_dir: + with fixtures.EnvironmentVariable('TMPDIR', test_tmp_dir): + temp_cwd_fixture = fixture_setup.TempCWD() + self.useFixture(temp_cwd_fixture) + + self.assertThat( + os.path.dirname(temp_cwd_fixture.path), + Equals(test_tmp_dir)) + + class TestFakeServer(http.server.HTTPServer): def __init__(self, server_address): diff -Nru snapcraft-2.34/snapcraft/tests/test_libraries.py snapcraft-2.35/snapcraft/tests/test_libraries.py --- snapcraft-2.34/snapcraft/tests/test_libraries.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/test_libraries.py 2017-11-01 19:41:33.000000000 +0000 @@ -19,11 +19,15 @@ import os import subprocess import tempfile +from textwrap import dedent from testtools.matchers import Equals from unittest import mock -from snapcraft.internal import libraries +from snapcraft.internal import ( + libraries, + os_release, +) from snapcraft import tests @@ -118,19 +122,28 @@ def setUp(self): super().setUp() - patcher = mock.patch('snapcraft.internal.common.get_os_release_info') - distro_mock = patcher.start() - distro_mock.return_value = {'VERSION_CODENAME': 'xenial', - 'HOME_URL': 'http://www.ubuntu.com/', - 'BUG_REPORT_URL': - 'http://bugs.launchpad.net/ubuntu/', - 'VERSION_ID': '16.04', - 'UBUNTU_CODENAME': 'xenial', - 'ID': 'ubuntu', 'NAME': 'Ubuntu', - 'ID_LIKE': 'debian', - 'PRETTY_NAME': 'Ubuntu 16.04.3 LTS', - 'VERSION': '16.04.3 LTS (Xenial Xerus)', - 'SUPPORT_URL': 'http://help.ubuntu.com/'} + with open('os-release', 'w') as f: + f.write(dedent("""\ + NAME="Ubuntu" + VERSION="16.04.3 LTS (Xenial Xerus)" + ID=ubuntu + ID_LIKE=debian + PRETTY_NAME="Ubuntu 16.04.3 LTS" + VERSION_ID="16.04" + HOME_URL="http://www.ubuntu.com/" + SUPPORT_URL="http://help.ubuntu.com/" + BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/" + UBUNTU_CODENAME=xenial + """)) + release = os_release.OsRelease(os_release_file='os-release') + + def _create_os_release(*args, **kwargs): + return release + + patcher = mock.patch( + 'snapcraft.internal.os_release.OsRelease', + wraps=_create_os_release) + self.os_release_mock = patcher.start() self.addCleanup(patcher.stop) patcher = mock.patch('snapcraft.internal.common.run_output') @@ -146,3 +159,37 @@ def test_fail_gracefully_if_system_libs_not_found(self): self.assertThat(libraries.get_dependencies('foo'), Equals([])) + + +class TestSystemLibsOnReleasesWithNoVersionId(tests.TestCase): + + def setUp(self): + super().setUp() + + libraries._libraries = None + + with open('os-release', 'w') as f: + f.write(dedent("""\ + NAME="Gentoo" + ID=gentoo + PRETTY_NAME="Gentoo/Linux" + HOME_URL="http://www.gentoo.org/" + SUPPORT_URL="http://www.gentoo.org/main/en/support.xml" + BUG_REPORT_URL="https://bugs.gentoo.org/" + """)) + release = os_release.OsRelease(os_release_file='os-release') + + def _create_os_release(*args, **kwargs): + return release + + patcher = mock.patch( + 'snapcraft.internal.os_release.OsRelease', + wraps=_create_os_release) + self.os_release_mock = patcher.start() + self.addCleanup(patcher.stop) + + @mock.patch('snapcraft.internal.libraries.repo.Repo.get_package_libraries', + return_value=['/usr/lib/libc.so.6', '/lib/libpthreads.so.6']) + def test_fail_gracefully_if_no_version_id_found(self, mock_package_libs): + self.assertThat(libraries._get_system_libs(), + Equals(frozenset(['libc.so.6', 'libpthreads.so.6']))) diff -Nru snapcraft-2.34/snapcraft/tests/test_lifecycle.py snapcraft-2.35/snapcraft/tests/test_lifecycle.py --- snapcraft-2.34/snapcraft/tests/test_lifecycle.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/test_lifecycle.py 2017-11-01 19:41:33.000000000 +0000 @@ -1,3 +1,4 @@ + # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # # Copyright (C) 2015-2017 Canonical Ltd @@ -20,6 +21,9 @@ import os import re import shutil +import subprocess +import sys +import textwrap from unittest import mock import fixtures @@ -37,6 +41,7 @@ from snapcraft import storeapi from snapcraft.file_utils import calculate_sha3_384 from snapcraft.internal import errors, pluginhandler, lifecycle +from snapcraft.internal.lifecycle._runner import _replace_in_part from snapcraft import tests from snapcraft.tests import fixture_setup @@ -51,16 +56,17 @@ self.project_options = snapcraft.ProjectOptions() def make_snapcraft_yaml(self, parts, snap_type=''): - yaml = """name: test -version: 0 -summary: test -description: test -confinement: strict -grade: stable -{type} + yaml = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + {type} -{parts} -""" + {parts} + """) super().make_snapcraft_yaml(yaml.format(parts=parts, type=snap_type)) @@ -82,20 +88,22 @@ self.plugin = Plugin() part = Part() - new_part = lifecycle._replace_in_part(part) + new_part = _replace_in_part(part) self.assertThat( new_part.plugin.options.source, Equals(part.plugin.installdir)) def test_exception_when_dependency_is_required(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil - part2: - plugin: nil - after: - - part1 -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + part2: + plugin: nil + after: + - part1 + """)) raised = self.assertRaises( RuntimeError, @@ -109,14 +117,16 @@ "prerequisites: 'part1'")) def test_no_exception_when_dependency_is_required_but_already_staged(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil - part2: - plugin: nil - after: - - part1 -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + part2: + plugin: nil + after: + - part1 + """)) def _fake_should_step_run(self, step, force=False): return self.name != 'part1' @@ -137,18 +147,20 @@ 'Pulling part2 \n')) def test_dependency_recursed_correctly(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil - part2: - plugin: nil - after: - - part1 - part3: - plugin: nil - after: - - part2 -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + part2: + plugin: nil + after: + - part1 + part3: + plugin: nil + after: + - part2 + """)) snap_info = lifecycle.execute('pull', self.project_options) @@ -179,14 +191,17 @@ )) def test_os_type_returned_by_lifecycle(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil - part2: - plugin: nil - after: - - part1 -""", 'type: os') + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + part2: + plugin: nil + after: + - part1 + """), + 'type: os') snap_info = lifecycle.execute('pull', self.project_options) @@ -199,12 +214,14 @@ self.assertThat(snap_info, Equals(expected_snap_info)) def test_dirty_prime_reprimes_single_part(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil - part2: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + part2: + plugin: nil + """)) # Strip it. lifecycle.execute('prime', self.project_options) @@ -248,12 +265,14 @@ ])) def test_dirty_prime_reprimes_multiple_part(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil - part2: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + part2: + plugin: nil + """)) # Strip it. lifecycle.execute('prime', self.project_options) @@ -298,12 +317,14 @@ ])) def test_dirty_stage_restages_single_part(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil - part2: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + part2: + plugin: nil + """)) # Stage it. lifecycle.execute('stage', self.project_options) @@ -347,12 +368,14 @@ ])) def test_dirty_stage_restages_multiple_parts(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil - part2: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + part2: + plugin: nil + """)) # Stage it. lifecycle.execute('stage', self.project_options) @@ -399,13 +422,15 @@ ])) def test_dirty_stage_part_with_built_dependent_raises(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil - part2: - plugin: nil - after: [part1] -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + part2: + plugin: nil + after: [part1] + """)) # Stage dependency lifecycle.execute('stage', self.project_options, part_names=['part1']) @@ -449,13 +474,15 @@ "by running:\nsnapcraft clean part2 -s stage\n")) def test_dirty_stage_part_with_unbuilt_dependent(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil - part2: - plugin: nil - after: [part1] -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + part2: + plugin: nil + after: [part1] + """)) # Stage dependency (dependent is unbuilt) lifecycle.execute('stage', self.project_options, part_names=['part1']) @@ -490,10 +517,12 @@ 'Staging part1 \n')) def test_dirty_stage_reprimes(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + """)) # Strip it. lifecycle.execute('prime', self.project_options) @@ -523,10 +552,12 @@ 'Priming part1 \n')) def test_dirty_build_raises(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + """)) # Build it. lifecycle.execute('build', self.project_options) @@ -560,10 +591,12 @@ "by running:\nsnapcraft clean part1 -s build\n")) def test_dirty_pull_raises(self): - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + """)) # Pull it. lifecycle.execute('pull', self.project_options) @@ -599,10 +632,12 @@ def test_pull_is_dirty_if_target_arch_changes( self, mock_install_build_packages, mock_enable_cross_compilation): mock_install_build_packages.return_value = [] - self.make_snapcraft_yaml("""parts: - part1: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + """)) # Pull it with amd64 lifecycle.execute('pull', snapcraft.ProjectOptions( @@ -633,23 +668,79 @@ "by running:\nsnapcraft clean part1 -s pull\n")) def test_prime_excludes_internal_snapcraft_dir(self): - self.make_snapcraft_yaml("""parts: - test-part: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + """)) lifecycle.execute('prime', self.project_options) self.assertThat( os.path.join('prime', 'snap', '.snapcraft'), Not(DirExists())) +class DirtyBuildScriptletTestCase(BaseLifecycleTestCase): + + scenarios = ( + ('prepare scriptlet', {'scriptlet': 'prepare'}), + ('build scriptlet', {'scriptlet': 'build'}), + ('install scriptlet', {'scriptlet': 'install'}), + ) + + @mock.patch.object(snapcraft.BasePlugin, 'enable_cross_compilation') + @mock.patch('snapcraft.repo.Repo.install_build_packages') + def test_build_is_dirty_if_scriptlet_changes( + self, mock_install_build_packages, mock_enable_cross_compilation): + mock_install_build_packages.return_value = [] + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + {}: touch scriptlet + """).format(self.scriptlet)) + + # Build it + lifecycle.execute('build', snapcraft.ProjectOptions()) + + # Reset logging since we only care about the following + self.fake_logger = fixtures.FakeLogger(level=logging.INFO) + self.useFixture(self.fake_logger) + + # Change prepare scriptlet + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + part1: + plugin: nil + {}: touch changed + """).format(self.scriptlet)) + + # Build it again. Should catch that the scriptlet changed and it needs + # to be rebuilt. + raised = self.assertRaises( + errors.StepOutdatedError, + lifecycle.execute, 'build', snapcraft.ProjectOptions()) + + self.assertThat( + str(raised), Equals( + "The 'build' step of 'part1' is out of date:\n" + "The {!r} part property appears to have changed.\n" + "In order to continue, please clean that part's 'build' step " + "by running:\nsnapcraft clean part1 -s build\n".format( + self.scriptlet))) + + class CleanTestCase(BaseLifecycleTestCase): def test_clean_removes_global_state(self): - self.make_snapcraft_yaml("""parts: - test-part: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + """)) lifecycle.execute('pull', self.project_options) lifecycle.clean(self.project_options, parts=None) self.assertThat( @@ -662,10 +753,12 @@ def test_prime_without_build_info_does_not_record(self): self.useFixture(fixtures.EnvironmentVariable( 'SNAPCRAFT_BUILD_INFO', None)) - self.make_snapcraft_yaml("""parts: - test-part: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + """)) lifecycle.execute('prime', self.project_options) for file_name in ('snapcraft.yaml', 'manifest.yaml'): self.assertThat( @@ -675,93 +768,232 @@ def test_prime_with_build_info_records_snapcraft_yaml(self): self.useFixture(fixtures.EnvironmentVariable( 'SNAPCRAFT_BUILD_INFO', '1')) - self.make_snapcraft_yaml("""parts: - test-part: - plugin: nil -""", snap_type='type: app') + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + """), + snap_type='type: app') lifecycle.execute('prime', self.project_options) - expected = ("""name: test -version: 0 -summary: test -description: test -confinement: strict -grade: stable -type: app + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + type: app + + parts: + test-part: + plugin: nil -parts: - test-part: - plugin: nil - -""") + """) self.assertThat( os.path.join('prime', 'snap', 'snapcraft.yaml'), FileContains(expected)) -class RecordManifestTestCase(BaseLifecycleTestCase): +class RecordManifestBaseTestCase(BaseLifecycleTestCase): + + def setUp(self): + super().setUp() + original_check_output = subprocess.check_output + + def fake_uname(cmd, *args, **kwargs): + if 'uname' in cmd: + return 'Linux test uname 4.10 x86_64'.encode( + sys.getfilesystemencoding()) + else: + return original_check_output(cmd, *args, **kwargs) + check_output_patcher = mock.patch( + 'subprocess.check_output', side_effect=fake_uname) + check_output_patcher.start() + self.addCleanup(check_output_patcher.stop) + + self.fake_apt_cache = fixture_setup.FakeAptCache() + self.useFixture(self.fake_apt_cache) + + self.fake_snapd = fixture_setup.FakeSnapd() + self.useFixture(self.fake_snapd) + self.fake_snapd.snaps_result = [] + + +class RecordManifestTestCase(RecordManifestBaseTestCase): def test_prime_with_build_info_records_manifest(self): self.useFixture(fixtures.EnvironmentVariable( 'SNAPCRAFT_BUILD_INFO', '1')) - self.make_snapcraft_yaml("""parts: - test-part: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + """)) lifecycle.execute('prime', self.project_options) - expected = ("""name: test -version: 0 -summary: test -description: test -confinement: strict -grade: stable -parts: - test-part: - build-packages: [] - plugin: nil - prime: [] - stage: [] - stage-packages: [] -architectures: [{}] -build-packages: [] -""".format(self.project_options.deb_arch)) + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + parts: + test-part: + build-packages: [] + installed-packages: [] + installed-snaps: [] + plugin: nil + prime: [] + stage: [] + stage-packages: [] + uname: Linux test uname 4.10 x86_64 + architectures: + - {} + build-packages: [] + build-snaps: [] + """.format(self.project_options.deb_arch)) self.assertThat( os.path.join('prime', 'snap', 'manifest.yaml'), FileContains(expected)) - def test_prime_with_stage_packages(self): + def test_prime_with_installed_snaps(self): self.useFixture(fixtures.EnvironmentVariable( 'SNAPCRAFT_BUILD_INFO', '1')) - self.useFixture(fixture_setup.FakeAptCache([ - ('test-package1', 'test-version1'), - ('test-package2', 'test-version2')])) + self.fake_snapd.snaps_result = [ + {'name': 'test-snap-1', + 'revision': 'test-snap-1-revision'}, + {'name': 'test-snap-2', + 'revision': 'test-snap-2-revision'}, + ] + + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + """)) + lifecycle.execute('prime', self.project_options) - self.make_snapcraft_yaml("""parts: - test-part: - plugin: nil - stage-packages: [test-package1=test-version1, test-package2] -""") + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + parts: + test-part: + build-packages: [] + installed-packages: [] + installed-snaps: + - test-snap-1=test-snap-1-revision + - test-snap-2=test-snap-2-revision + plugin: nil + prime: [] + stage: [] + stage-packages: [] + uname: Linux test uname 4.10 x86_64 + architectures: + - {} + build-packages: [] + build-snaps: [] + """.format(self.project_options.deb_arch)) + self.assertThat( + os.path.join('prime', 'snap', 'manifest.yaml'), + FileContains(expected)) + def test_prime_with_installed_packages(self): + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_BUILD_INFO', '1')) + for name, version in [('test-package1', 'test-version1'), + ('test-package2', 'test-version2')]: + self.fake_apt_cache.add_package( + fixture_setup.FakeAptCachePackage( + name, version, installed=True)) + + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + """)) lifecycle.execute('prime', self.project_options) - expected = ("""name: test -version: 0 -summary: test -description: test -confinement: strict -grade: stable -parts: - test-part: - build-packages: [] - plugin: nil - prime: [] - stage: [] - stage-packages: [test-package1=test-version1, test-package2=test-version2] -architectures: [{}] -build-packages: [] -""".format(self.project_options.deb_arch)) + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + parts: + test-part: + build-packages: [] + installed-packages: + - test-package1=test-version1 + - test-package2=test-version2 + installed-snaps: [] + plugin: nil + prime: [] + stage: [] + stage-packages: [] + uname: Linux test uname 4.10 x86_64 + architectures: + - {} + build-packages: [] + build-snaps: [] + """.format(self.project_options.deb_arch)) + self.assertThat( + os.path.join('prime', 'snap', 'manifest.yaml'), + FileContains(expected)) + + def test_prime_with_stage_packages(self): + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_BUILD_INFO', '1')) + for name, version in [('test-package1', 'test-version1'), + ('test-package2', 'test-version2')]: + self.fake_apt_cache.add_package( + fixture_setup.FakeAptCachePackage(name, version)) + + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + stage-packages: [test-package1=test-version1, test-package2] + """)) # NOQA + + lifecycle.execute('prime', self.project_options) + + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + parts: + test-part: + build-packages: [] + installed-packages: [] + installed-snaps: [] + plugin: nil + prime: [] + stage: [] + stage-packages: + - test-package1=test-version1 + - test-package2=test-version2 + uname: Linux test uname 4.10 x86_64 + architectures: + - {} + build-packages: [] + build-snaps: [] + """.format(self.project_options.deb_arch)) self.assertThat( os.path.join('prime', 'snap', 'manifest.yaml'), FileContains(expected)) @@ -770,34 +1002,45 @@ def test_prime_with_global_build_packages(self, _): self.useFixture(fixtures.EnvironmentVariable( 'SNAPCRAFT_BUILD_INFO', '1')) - self.useFixture(fixture_setup.FakeAptCache([ - ('test-package1', 'test-version1'), - ('test-package2', 'test-version2')])) - - self.make_snapcraft_yaml("""build-packages: [test-package1=test-version1, test-package2] -parts: - test-part: - plugin: nil -""") + for name, version in [('test-package1', 'test-version1'), + ('test-package2', 'test-version2')]: + self.fake_apt_cache.add_package( + fixture_setup.FakeAptCachePackage(name, version)) + + self.make_snapcraft_yaml( + textwrap.dedent("""\ + build-packages: [test-package1=test-version1, test-package2] + parts: + test-part: + plugin: nil + """)) lifecycle.execute('prime', self.project_options) - expected = ("""name: test -version: 0 -summary: test -description: test -confinement: strict -grade: stable -build-packages: [test-package1=test-version1, test-package2=test-version2] -parts: - test-part: - build-packages: [] - plugin: nil - prime: [] - stage: [] - stage-packages: [] -architectures: [{}] -""".format(self.project_options.deb_arch)) + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + build-packages: + - test-package1=test-version1 + - test-package2=test-version2 + parts: + test-part: + build-packages: [] + installed-packages: [] + installed-snaps: [] + plugin: nil + prime: [] + stage: [] + stage-packages: [] + uname: Linux test uname 4.10 x86_64 + architectures: + - {} + build-snaps: [] + """.format(self.project_options.deb_arch)) self.assertThat( os.path.join('prime', 'snap', 'manifest.yaml'), FileContains(expected)) @@ -806,39 +1049,50 @@ def test_prime_with_source_details(self, _): self.useFixture(fixtures.EnvironmentVariable( 'SNAPCRAFT_BUILD_INFO', '1')) + self.fake_apt_cache.add_package( + fixture_setup.FakeAptCachePackage('git', 'testversion')) - self.make_snapcraft_yaml("""parts: - test-part: - plugin: nil - source: test-source - source-type: git - source-commit: test-commit -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + source: test-source + source-type: git + source-commit: test-commit + """)) lifecycle.execute('prime', self.project_options) - expected = ("""name: test -version: 0 -summary: test -description: test -confinement: strict -grade: stable -parts: - test-part: - build-packages: [] - plugin: nil - prime: [] - source: test-source - source-branch: '' - source-checksum: '' - source-commit: test-commit - source-tag: '' - source-type: git - stage: [] - stage-packages: [] -architectures: [{}] -build-packages: [] -""".format(self.project_options.deb_arch)) + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + parts: + test-part: + build-packages: [] + installed-packages: [] + installed-snaps: [] + plugin: nil + prime: [] + source: test-source + source-branch: '' + source-checksum: '' + source-commit: test-commit + source-tag: '' + source-type: git + stage: [] + stage-packages: [] + uname: Linux test uname 4.10 x86_64 + architectures: + - {} + build-packages: + - git=testversion + build-snaps: [] + """.format(self.project_options.deb_arch)) self.assertThat( os.path.join('prime', 'snap', 'manifest.yaml'), FileContains(expected)) @@ -847,33 +1101,43 @@ def test_prime_with_build_package_with_any_architecture(self, _): self.useFixture(fixtures.EnvironmentVariable( 'SNAPCRAFT_BUILD_INFO', '1')) - self.useFixture(fixture_setup.FakeAptCache([ - ('test-package', 'test-version')])) + self.fake_apt_cache.add_package(fixture_setup.FakeAptCachePackage( + 'test-package', 'test-version')) - self.make_snapcraft_yaml("""parts: - test-part: - plugin: nil - build-packages: ['test-package:any'] -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + build-packages: ['test-package:any'] + """)) lifecycle.execute('prime', self.project_options) - expected = ("""name: test -version: 0 -summary: test -description: test -confinement: strict -grade: stable -parts: - test-part: - build-packages: ['test-package:any'] - plugin: nil - prime: [] - stage: [] - stage-packages: [] -architectures: [{}] -build-packages: [test-package=test-version] -""".format(self.project_options.deb_arch)) + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + parts: + test-part: + build-packages: + - test-package:any + installed-packages: [] + installed-snaps: [] + plugin: nil + prime: [] + stage: [] + stage-packages: [] + uname: Linux test uname 4.10 x86_64 + architectures: + - {} + build-packages: + - test-package=test-version + build-snaps: [] + """.format(self.project_options.deb_arch)) self.assertThat( os.path.join('prime', 'snap', 'manifest.yaml'), FileContains(expected)) @@ -882,38 +1146,45 @@ def test_prime_with_virtual_build_package(self, _): self.useFixture(fixtures.EnvironmentVariable( 'SNAPCRAFT_BUILD_INFO', '1')) - fake_apt_cache = fixture_setup.FakeAptCache() - self.useFixture(fake_apt_cache) - fake_apt_cache.cache['test-provider-package'] = ( + self.fake_apt_cache.add_package( fixture_setup.FakeAptCachePackage( - fake_apt_cache.path, 'test-provider-package', 'test-version', provides=['test-virtual-package'])) - self.make_snapcraft_yaml("""parts: - test-part: - plugin: nil - build-packages: ['test-virtual-package'] -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + build-packages: ['test-virtual-package'] + """)) lifecycle.execute('prime', self.project_options) - expected = ("""name: test -version: 0 -summary: test -description: test -confinement: strict -grade: stable -parts: - test-part: - build-packages: [test-virtual-package] - plugin: nil - prime: [] - stage: [] - stage-packages: [] -architectures: [{}] -build-packages: [test-provider-package=test-version] -""".format(self.project_options.deb_arch)) + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + parts: + test-part: + build-packages: + - test-virtual-package + installed-packages: [] + installed-snaps: [] + plugin: nil + prime: [] + stage: [] + stage-packages: [] + uname: Linux test uname 4.10 x86_64 + architectures: + - {} + build-packages: + - test-provider-package=test-version + build-snaps: [] + """.format(self.project_options.deb_arch)) self.assertThat( os.path.join('prime', 'snap', 'manifest.yaml'), FileContains(expected)) @@ -924,35 +1195,107 @@ 'test-plugin-manifest': 'test-value'} self.useFixture(fixtures.EnvironmentVariable( 'SNAPCRAFT_BUILD_INFO', '1')) - self.make_snapcraft_yaml("""parts: - test-part: - plugin: nil -""") + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + """)) lifecycle.execute('prime', self.project_options) - expected = ("""name: test -version: 0 -summary: test -description: test -confinement: strict -grade: stable -parts: - test-part: - build-packages: [] - plugin: nil - prime: [] - stage: [] - stage-packages: [] - test-plugin-manifest: test-value -architectures: [{}] -build-packages: [] -""".format(self.project_options.deb_arch)) + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + parts: + test-part: + build-packages: [] + installed-packages: [] + installed-snaps: [] + plugin: nil + prime: [] + stage: [] + stage-packages: [] + test-plugin-manifest: test-value + uname: Linux test uname 4.10 x86_64 + architectures: + - {} + build-packages: [] + build-snaps: [] + """.format(self.project_options.deb_arch)) self.assertThat( os.path.join('prime', 'snap', 'manifest.yaml'), FileContains(expected)) + def test_prime_with_image_info_records_manifest(self): + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_BUILD_INFO', '1')) + test_image_info = ( + '{"architecture": "test-architecture", ' + '"created_at": "test-created-at", ' + '"fingerprint": "test-fingerprint"}') + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_IMAGE_INFO', test_image_info)) + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + """)) + lifecycle.execute('prime', self.project_options) -class RecordManifestWithDeprecatedSnapKeywordTestCase(BaseLifecycleTestCase): + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + parts: + test-part: + build-packages: [] + installed-packages: [] + installed-snaps: [] + plugin: nil + prime: [] + stage: [] + stage-packages: [] + uname: Linux test uname 4.10 x86_64 + architectures: + - {} + image-info: + architecture: test-architecture + created_at: test-created-at + fingerprint: test-fingerprint + build-packages: [] + build-snaps: [] + """.format(self.project_options.deb_arch)) + self.assertThat( + os.path.join('prime', 'snap', 'manifest.yaml'), + FileContains(expected)) + + def test_prime_with_invalid_image_info_raises_exception(self): + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_BUILD_INFO', '1')) + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_IMAGE_INFO', 'not-json')) + self.make_snapcraft_yaml( + textwrap.dedent("""\ + parts: + test-part: + plugin: nil + """)) + raised = self.assertRaises( + errors.InvalidContainerImageInfoError, + lifecycle.execute, 'prime', self.project_options) + self.assertThat(raised.image_info, Equals('not-json')) + + +class RecordManifestWithDeprecatedSnapKeywordTestCase( + RecordManifestBaseTestCase): scenarios = ( ('using snap keyword', {'keyword': 'snap'}), @@ -970,22 +1313,29 @@ self.make_snapcraft_yaml(parts.format(self.keyword)) lifecycle.execute('prime', self.project_options) - expected = ("""name: test -version: 0 -summary: test -description: test -confinement: strict -grade: stable -parts: - test-part: - build-packages: [] - plugin: nil - prime: [-*] - stage: [] - stage-packages: [] -architectures: [{}] -build-packages: [] -""".format(self.project_options.deb_arch)) + expected = textwrap.dedent("""\ + name: test + version: 0 + summary: test + description: test + confinement: strict + grade: stable + parts: + test-part: + build-packages: [] + installed-packages: [] + installed-snaps: [] + plugin: nil + prime: + - -* + stage: [] + stage-packages: [] + uname: Linux test uname 4.10 x86_64 + architectures: + - {} + build-packages: [] + build-snaps: [] + """.format(self.project_options.deb_arch)) self.assertThat( os.path.join('prime', 'snap', 'manifest.yaml'), FileContains(expected)) @@ -1009,7 +1359,8 @@ self.addCleanup(patcher.stop) self.tempdir = os.path.join(self.path, 'tmpdir') - patcher = mock.patch('snapcraft.internal.lifecycle.TemporaryDirectory') + patcher = mock.patch('snapcraft.internal.lifecycle._runner.' + 'TemporaryDirectory') self.tempdir_mock = patcher.start() self.addCleanup(patcher.stop) @@ -1111,5 +1462,5 @@ which_mock.return_value = None raised = self.assertRaises( errors.MissingCommandError, - lifecycle.snap, self.project_options) + lifecycle.pack, self.project_options) self.assertThat(str(raised), Contains('mksquashfs')) diff -Nru snapcraft-2.34/snapcraft/tests/test_log.py snapcraft-2.35/snapcraft/tests/test_log.py --- snapcraft-2.34/snapcraft/tests/test_log.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/test_log.py 2017-11-01 19:41:33.000000000 +0000 @@ -16,7 +16,7 @@ import logging -from testtools.matchers import Equals +from testtools.matchers import Contains, Equals, Not from snapcraft.internal import log from snapcraft import tests @@ -44,12 +44,12 @@ logger.info('Test info') logger.warning('Test warning') - expected_out = ('Test debug\n' - '{}Test info\033[0m\n' - '{}Test warning\033[0m\n').format( - self.info_color, self.warning_color) - - self.assertThat(self.fake_terminal.getvalue(), Equals(expected_out)) + stdout = self.fake_terminal.getvalue() + self.assertThat(stdout, Contains('Test debug')) + expected_info = '{}Test info\033[0m'.format(self.info_color) + self.assertThat(stdout, Contains(expected_info)) + expected_warning = '{}Test warning\033[0m'.format(self.warning_color) + self.assertThat(stdout, Contains(expected_warning)) self.assertThat(self.fake_terminal.getvalue(stderr=True), Equals('')) def test_configure_must_send_errors_to_stderr(self): @@ -62,13 +62,11 @@ logger.error('Test error') logger.critical('Test critical') - expected_err = ('{}Test error\033[0m\n' - '{}Test critical\033[0m\n').format( - self.error_color, self.critical_color) - - self.assertThat( - self.fake_terminal.getvalue(stderr=True), - Equals(expected_err)) + stderr = self.fake_terminal.getvalue(stderr=True) + expected_error = '{}Test error\033[0m'.format(self.error_color) + self.assertThat(stderr, Contains(expected_error)) + expected_crit = '{}Test critical\033[0m'.format(self.critical_color) + self.assertThat(stderr, Contains(expected_crit)) self.assertThat(self.fake_terminal.getvalue(), Equals('')) def test_configure_must_log_info_and_higher(self): @@ -82,16 +80,18 @@ logger.error('Test error') logger.critical('Test critical') - expected_out = ('{}Test info\033[0m\n' - '{}Test warning\033[0m\n').format( - self.info_color, self.warning_color) - expected_err = ('{}Test error\033[0m\n' - '{}Test critical\033[0m\n').format( - self.error_color, self.critical_color) - - self.assertThat(self.fake_terminal.getvalue(), Equals(expected_out)) - self.assertThat( - self.fake_terminal.getvalue(stderr=True), Equals(expected_err)) + stdout = self.fake_terminal.getvalue() + self.assertThat(stdout, Not(Contains('Test debug'))) + expected_info = '{}Test info\033[0m'.format(self.info_color) + self.assertThat(stdout, Contains(expected_info)) + expected_warning = '{}Test warning\033[0m'.format(self.warning_color) + self.assertThat(stdout, Contains(expected_warning)) + + stderr = self.fake_terminal.getvalue(stderr=True) + expected_error = '{}Test error\033[0m'.format(self.error_color) + self.assertThat(stderr, Contains(expected_error)) + expected_crit = '{}Test critical\033[0m'.format(self.critical_color) + self.assertThat(stderr, Contains(expected_crit)) def test_configure_must_support_debug(self): logger_name = self.id() @@ -104,17 +104,18 @@ logger.error('Test error') logger.critical('Test critical') - expected_out = ('Test debug\n' - '{}Test info\033[0m\n' - '{}Test warning\033[0m\n').format( - self.info_color, self.warning_color) - expected_err = ('{}Test error\033[0m\n' - '{}Test critical\033[0m\n').format( - self.error_color, self.critical_color) - - self.assertThat(self.fake_terminal.getvalue(), Equals(expected_out)) - self.assertThat( - self.fake_terminal.getvalue(stderr=True), Equals(expected_err)) + stdout = self.fake_terminal.getvalue() + self.assertThat(stdout, Contains('Test debug')) + expected_info = '{}Test info\033[0m'.format(self.info_color) + self.assertThat(stdout, Contains(expected_info)) + expected_warning = '{}Test warning\033[0m'.format(self.warning_color) + self.assertThat(stdout, Contains(expected_warning)) + + stderr = self.fake_terminal.getvalue(stderr=True) + expected_error = '{}Test error\033[0m'.format(self.error_color) + self.assertThat(stderr, Contains(expected_error)) + expected_crit = '{}Test critical\033[0m'.format(self.critical_color) + self.assertThat(stderr, Contains(expected_crit)) def test_configure_must_support_no_tty(self): self.fake_terminal = fixture_setup.FakeTerminal(isatty=False) @@ -129,12 +130,16 @@ logger.error('Test error') logger.critical('Test critical') - expected_out = ('Test debug\n' - 'Test info\n' - 'Test warning\n') - expected_err = ('Test error\n' - 'Test critical\n') - - self.assertThat(self.fake_terminal.getvalue(), Equals(expected_out)) - self.assertThat( - self.fake_terminal.getvalue(stderr=True), Equals(expected_err)) + stdout = self.fake_terminal.getvalue() + self.assertThat(stdout, Contains('Test debug')) + self.assertThat(stdout, Contains('Test info')) + self.assertThat(stdout, Not(Contains(self.info_color))) + self.assertThat(stdout, Contains('Test warning')) + self.assertThat(stdout, Not(Contains(self.warning_color))) + self.assertThat(stdout, Not(Contains('\033[0m'))) + + stderr = self.fake_terminal.getvalue(stderr=True) + self.assertThat(stderr, Contains('Test error')) + self.assertThat(stderr, Not(Contains(self.error_color))) + self.assertThat(stderr, Contains('Test critical')) + self.assertThat(stderr, Not(Contains(self.critical_color))) diff -Nru snapcraft-2.34/snapcraft/tests/test_lxd.py snapcraft-2.35/snapcraft/tests/test_lxd.py --- snapcraft-2.34/snapcraft/tests/test_lxd.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/test_lxd.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,6 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import json import logging import os import requests @@ -32,22 +33,20 @@ from snapcraft.internal import lxd from snapcraft.internal.errors import ( ContainerConnectionError, + ContainerRunError, SnapdError, + SnapcraftEnvironmentError, ) +from snapcraft._options import _get_deb_arch -class LXDTestCase(tests.TestCase): - - scenarios = [ - ('local', dict(remote='local', target_arch=None)), - ('remote', dict(remote='my-remote', target_arch=None)), - ('cross', dict(remote='local', target_arch='armhf')), - ] +class LXDBaseTestCase(tests.TestCase): def setUp(self): super().setUp() self.fake_lxd = tests.fixture_setup.FakeLXD() self.useFixture(self.fake_lxd) + self.fake_lxd.kernel_arch = self.server self.fake_filesystem = tests.fixture_setup.FakeFilesystem() self.useFixture(self.fake_filesystem) @@ -55,7 +54,24 @@ self.useFixture(self.fake_logger) self.project_options = ProjectOptions(target_deb_arch=self.target_arch) - def make_cleanbuilder(self): + +class LXDTestCase(LXDBaseTestCase): + + scenarios = [ + ('local', dict(remote='local', target_arch=None, server='x86_64')), + ('remote', dict(remote='myremote', target_arch=None, server='x86_64')), + ('cross', dict(remote='local', target_arch='armhf', server='x86_64', + cross=True)), + ('arm remote', dict(remote='pi', target_arch=None, server='armv7l')), + ('arm same', dict(remote='pi', target_arch='armhf', server='armv7l')), + ('arm cross', dict(remote='pi', target_arch='arm64', server='armv7l', + cross=True)), + ] + + +class CleanbuilderTestCase(LXDTestCase): + + def make_containerbuild(self): return lxd.Cleanbuilder(output='snap.snap', source='project.tar', metadata={'name': 'project'}, project_options=self.project_options, @@ -68,14 +84,16 @@ mock_container_run.side_effect = lambda cmd, **kwargs: cmd mock_pet.return_value = 'my-pet' + project_folder = '/root/build_project' - self.make_cleanbuilder().execute() - expected_arch = 'amd64' + self.make_containerbuild().execute() + expected_arch = _get_deb_arch(self.server) - self.assertIn('Setting up container with project assets\n' - 'Waiting for a network connection...\n' + self.assertIn('Waiting for a network connection...\n' 'Network connection established\n' + 'Setting up container with project assets\n' 'Retrieved snap.snap\n', self.fake_logger.output) + args = [] if self.target_arch: self.assertIn('Setting target machine to \'{}\'\n'.format( @@ -90,21 +108,28 @@ 'environment.SNAPCRAFT_SETUP_CORE', '1']), call(['lxc', 'config', 'set', container_name, 'environment.LC_ALL', 'C.UTF-8']), + call(['lxc', 'config', 'set', container_name, + 'environment.SNAPCRAFT_IMAGE_INFO', + '{"fingerprint": "test-fingerprint", ' + '"architecture": "test-architecture", ' + '"created_at": "test-created-at"}']), call(['lxc', 'file', 'push', os.path.realpath('project.tar'), '{}/root/build_project/project.tar'.format(container_name)]), ]) mock_container_run.assert_has_calls([ - call(['mkdir', project_folder]), - call(['tar', 'xvf', 'project.tar'], - cwd=project_folder), call(['python3', '-c', 'import urllib.request; ' + 'urllib.request.urlopen(' + '"http://start.ubuntu.com/connectivity-check.html"' + ', timeout=5)']), call(['apt-get', 'update']), + call(['mkdir', project_folder]), + call(['tar', 'xvf', 'project.tar'], + cwd=project_folder), call(['snapcraft', 'snap', '--output', 'snap.snap', *args], cwd=project_folder), ]) + # Ensure there's no unexpected calls eg. two network checks + self.assertThat(mock_container_run.call_count, Equals(5)) self.fake_lxd.check_call_mock.assert_has_calls([ call(['lxc', 'file', 'pull', '{}{}/snap.snap'.format(container_name, project_folder), @@ -112,18 +137,6 @@ call(['lxc', 'stop', '-f', container_name]), ]) - def test_wait_for_network_loops(self): - self.fake_lxd.check_call_mock.side_effect = CalledProcessError( - -1, ['my-cmd']) - - builder = self.make_cleanbuilder() - - raised = self.assertRaises( - CalledProcessError, - builder._wait_for_network) - - self.assertThat(str(raised), Contains("Command '['my-cmd']'")) - def test_failed_container_never_created(self): def call_effect(*args, **kwargs): if args[0][:2] == ['lxc', 'launch']: @@ -134,28 +147,65 @@ raised = self.assertRaises( CalledProcessError, - self.make_cleanbuilder().execute) + self.make_containerbuild().execute) self.assertThat(self.fake_lxd.status, Equals(None)) # lxc launch should fail and no further commands should come after that self.assertThat(str(raised), Contains("Command '['lxc', 'launch'")) - @patch('snapcraft.internal.lxd.Cleanbuilder._container_run') - def test_failed_build_with_debug(self, mock_run): - call_list = [] - def run_effect(*args, **kwargs): - call_list.append(args[0]) - if args[0][:4] == ['snapcraft', 'snap', '--output', 'snap.snap']: +class ContainerbuildTestCase(LXDTestCase): + + def make_containerbuild(self): + return lxd.Cleanbuilder(output='snap.snap', source='project.tar', + metadata={'name': 'project'}, + project_options=self.project_options, + remote=self.remote) + + def test_parts_uri_set(self): + self.useFixture( + fixtures.EnvironmentVariable('SNAPCRAFT_PARTS_URI', 'foo')) + self.make_containerbuild().execute() + self.fake_lxd.check_call_mock.assert_has_calls([ + call(['lxc', 'config', 'set', self.fake_lxd.name, + 'environment.SNAPCRAFT_PARTS_URI', 'foo']), + ]) + + def test_build_info_set(self): + self.useFixture( + fixtures.EnvironmentVariable( + 'SNAPCRAFT_BUILD_INFO', 'test_build_info_value')) + self.make_containerbuild().execute() + self.fake_lxd.check_call_mock.assert_has_calls([ + call(['lxc', 'config', 'set', self.fake_lxd.name, + 'environment.SNAPCRAFT_BUILD_INFO', + 'test_build_info_value']), + ]) + + def test_wait_for_network_loops(self): + self.fake_lxd.check_call_mock.side_effect = CalledProcessError( + -1, ['my-cmd']) + + builder = self.make_containerbuild() + + self.assertRaises(ContainerRunError, + builder._wait_for_network) + + def test_failed_build_with_debug(self): + def call_effect(*args, **kwargs): + if 'snapcraft snap --output snap.snap' in ' '.join(args[0]): raise CalledProcessError(returncode=255, cmd=args[0]) + return self.fake_lxd.check_output_side_effect()(*args, **kwargs) - mock_run.side_effect = run_effect + self.fake_lxd.check_call_mock.side_effect = call_effect self.project_options = ProjectOptions(debug=True) - self.make_cleanbuilder().execute() + self.make_containerbuild().execute() - self.assertIn(['bash', '-i'], call_list) + self.fake_lxd.check_call_mock.assert_has_calls([ + call(['lxc', 'exec', self.fake_lxd.name, '--', 'bash', '-i']), + ]) - @patch('snapcraft.internal.lxd.Cleanbuilder._container_run') + @patch('snapcraft.internal.lxd.Containerbuild._container_run') def test_failed_build_without_debug(self, mock_run): call_list = [] @@ -168,11 +218,11 @@ self.assertRaises( CalledProcessError, - self.make_cleanbuilder().execute) + self.make_containerbuild().execute) self.assertNotIn(['bash', '-i'], call_list) - @patch('snapcraft.internal.lxd.Cleanbuilder._container_run') + @patch('snapcraft.internal.lxd.Containerbuild._container_run') def test_lxc_check_fails(self, mock_run): self.fake_lxd.check_output_mock.side_effect = FileNotFoundError('lxc') @@ -181,23 +231,23 @@ 'You must have LXD installed in order to use cleanbuild.\n' 'Refer to the documentation at ' 'https://linuxcontainers.org/lxd/getting-started-cli.'): - self.make_cleanbuilder() + self.make_containerbuild() - @patch('snapcraft.internal.lxd.Cleanbuilder._container_run') + @patch('snapcraft.internal.lxd.Containerbuild._container_run') def test_remote_does_not_exist(self, mock_run): self.fake_lxd.check_output_mock.side_effect = CalledProcessError( 255, ['lxd', 'list', self.remote]) with ExpectedException(ContainerConnectionError, 'There are either.*{}.*'.format(self.remote)): - self.make_cleanbuilder() + self.make_containerbuild() @patch('snapcraft.internal.common.is_snap') def test_parallel_invocation(self, mock_is_snap): mock_is_snap.side_effect = lambda: False - builder1 = self.make_cleanbuilder() - builder2 = self.make_cleanbuilder() + builder1 = self.make_containerbuild() + builder2 = self.make_containerbuild() builder1.execute() # Temporary folder should be removed in the end self.fake_filesystem.rmtree_mock.assert_has_calls([ @@ -212,9 +262,21 @@ fake_snapd = tests.fixture_setup.FakeSnapd() self.useFixture(fake_snapd) + fake_snapd.snaps_result = [ + {'name': 'core', + 'confinement': 'strict', + 'id': '2kkitQurgOkL3foImG4wDwn9CIANuHlt', + 'channel': 'stable', + 'revision': '123'}, + {'name': 'snapcraft', + 'confinement': 'classic', + 'id': '3lljuRvshPlM4gpJnH5xExo0DJBOvImu', + 'channel': 'edge', + 'revision': '345'}, + ] - builder1 = self.make_cleanbuilder() - builder2 = self.make_cleanbuilder() + builder1 = self.make_containerbuild() + builder2 = self.make_containerbuild() builder1.execute() # Temporary folder should be removed in the end self.fake_filesystem.rmtree_mock.assert_has_calls([ @@ -233,7 +295,7 @@ fake_snapd = tests.fixture_setup.FakeSnapd() self.useFixture(fake_snapd) - builder = self.make_cleanbuilder() + builder = self.make_containerbuild() builder.execute() mock_container_run.assert_has_calls([ @@ -245,14 +307,16 @@ mock_is_snap): mock_is_snap.side_effect = lambda: True + def snap_details(handler_instalce, snap_name): + raise requests.exceptions.ConnectionError( + 'Connection aborted.', + FileNotFoundError(2, 'No such file or directory')) + fake_snapd = tests.fixture_setup.FakeSnapd() + fake_snapd.snap_details_func = snap_details self.useFixture(fake_snapd) - fake_snapd.session_request_mock.side_effect = ( - requests.exceptions.ConnectionError( - 'Connection aborted.', - FileNotFoundError(2, 'No such file or directory'))) - builder = self.make_cleanbuilder() + builder = self.make_containerbuild() self.assertIn('Error connecting to', str(self.assertRaises(SnapdError, builder.execute))) @@ -265,10 +329,10 @@ mock_is_snap.side_effect = lambda: True fake_snapd = tests.fixture_setup.FakeSnapd() + fake_snapd.snaps_result = [] self.useFixture(fake_snapd) - fake_snapd.snaps = {} - builder = self.make_cleanbuilder() + builder = self.make_containerbuild() self.assertIn('Error querying \'core\' snap: not found', str(self.assertRaises(SnapdError, builder.execute))) @@ -285,10 +349,33 @@ fake_snapd = tests.fixture_setup.FakeSnapd() self.useFixture(fake_snapd) + fake_snapd.snaps_result = [ + {'name': 'core', + 'confinement': 'strict', + 'id': '2kkitQurgOkL3foImG4wDwn9CIANuHlt', + 'channel': 'stable', + 'revision': '123'}, + {'name': 'snapcraft', + 'confinement': 'classic', + 'id': '3lljuRvshPlM4gpJnH5xExo0DJBOvImu', + 'channel': 'edge', + 'revision': '345'}, + ] - builder = self.make_cleanbuilder() + builder = self.make_containerbuild() builder.execute() + if hasattr(self, 'cross') and self.cross: + mock_container_run.assert_has_calls([ + call(['snap', 'install', 'core', '--channel', 'stable']), + call(['snap', 'refresh', 'core', '--channel', 'stable']), + call(['snap', 'install', 'snapcraft', '--channel', 'edge', + '--classic']), + call(['snap', 'refresh', 'snapcraft', '--channel', 'edge', + '--classic']), + ]) + return + self.fake_lxd.check_call_mock.assert_has_calls([ call(['lxc', 'file', 'push', os.path.join(builder.tmp_dir, 'core_123.assert'), @@ -324,12 +411,33 @@ fake_snapd = tests.fixture_setup.FakeSnapd() self.useFixture(fake_snapd) - fake_snapd.snaps['snapcraft']['revision'] = 'x1' - fake_snapd.snaps['snapcraft']['id'] = '' + fake_snapd.snaps_result = [ + {'name': 'core', + 'confinement': 'strict', + 'id': '2kkitQurgOkL3foImG4wDwn9CIANuHlt', + 'channel': 'stable', + 'revision': '123'}, + {'name': 'snapcraft', + 'confinement': 'classic', + 'id': '', + 'channel': 'edge', + 'revision': 'x1'}, + ] - builder = self.make_cleanbuilder() + builder = self.make_containerbuild() builder.execute() + if hasattr(self, 'cross') and self.cross: + mock_container_run.assert_has_calls([ + call(['snap', 'install', 'core', '--channel', 'stable']), + call(['snap', 'refresh', 'core', '--channel', 'stable']), + call(['snap', 'install', 'snapcraft', '--channel', 'edge', + '--classic']), + call(['snap', 'refresh', 'snapcraft', '--channel', 'edge', + '--classic']), + ]) + return + self.fake_lxd.check_call_mock.assert_has_calls([ call(['sudo', 'cp', '/var/lib/snapd/snaps/snapcraft_x1.snap', os.path.join(builder.tmp_dir, 'snapcraft_x1.snap')]), @@ -343,3 +451,231 @@ call(['snap', 'install', '/run/snapcraft_x1.snap', '--dangerous', '--classic']), ]) + + @patch('snapcraft.internal.lxd.Containerbuild._container_run') + @patch('snapcraft.internal.common.is_snap') + def test_inject_snap_already_installed(self, + mock_is_snap, + mock_container_run): + mock_is_snap.side_effect = lambda: True + mock_container_run.side_effect = lambda cmd, **kwargs: cmd + + def call_effect(*args, **kwargs): + if args[0][:2] == ['lxc', 'exec']: + if 'readlink' in args[0]: + if args[0][-1].endswith('/current'): + return '123\n'.encode('utf-8') + if 'sha384sum' in args[0]: + if args[0][-1].endswith('core_123.snap'): + return 'deadbeef {}'.format(args[0][1]).encode('utf-8') + return 'abcdef {}'.format(args[0][1]).encode('utf-8') + return default_side_effect(*args, **kwargs) + + default_side_effect = self.fake_lxd.check_output_mock.side_effect + self.fake_lxd.check_output_mock.side_effect = call_effect + + fake_snapd = tests.fixture_setup.FakeSnapd() + self.useFixture(fake_snapd) + fake_snapd.snaps_result = [ + {'name': 'core', + 'confinement': 'strict', + 'id': '2kkitQurgOkL3foImG4wDwn9CIANuHlt', + 'channel': 'stable', + 'revision': '123'}, + {'name': 'snapcraft', + 'confinement': 'classic', + 'id': '', + 'channel': 'edge', + 'revision': '123'}, + ] + + builder = self.make_containerbuild() + + builder.execute() + if hasattr(self, 'cross') and self.cross: + mock_container_run.assert_has_calls([ + call(['snap', 'install', 'core', '--channel', 'stable']), + call(['snap', 'refresh', 'core', '--channel', 'stable']), + call(['snap', 'install', 'snapcraft', '--channel', 'edge', + '--classic']), + call(['snap', 'refresh', 'snapcraft', '--channel', 'edge', + '--classic']), + ]) + return + + self.fake_lxd.check_call_mock.assert_has_calls([ + call(['lxc', 'file', 'push', + os.path.join(builder.tmp_dir, 'snapcraft_123.assert'), + '{}/run/snapcraft_123.assert'.format(self.fake_lxd.name)]), + call(['lxc', 'file', 'push', + os.path.join(builder.tmp_dir, 'snapcraft_123.snap'), + '{}/run/snapcraft_123.snap'.format(self.fake_lxd.name)]), + ]) + mock_container_run.assert_has_calls([ + call(['apt-get', 'install', 'squashfuse', '-y']), + call(['snap', 'ack', '/run/snapcraft_123.assert']), + call(['snap', 'install', '/run/snapcraft_123.snap', '--classic']), + ]) + + +class ProjectTestCase(ContainerbuildTestCase): + + scenarios = [ + ('remote', dict(remote='myremote', target_arch=None, server='x86_64')), + ] + + def make_containerbuild(self): + return lxd.Project(output='snap.snap', source='project.tar', + metadata={'name': 'project'}, + project_options=self.project_options, + remote=self.remote) + + @patch('snapcraft.internal.lxd.Containerbuild._container_run') + def test_start_failed(self, mock_container_run): + mock_container_run.side_effect = lambda cmd, **kwargs: cmd + + def call_effect(*args, **kwargs): + if args[0][:2] == ['lxc', 'start']: + raise CalledProcessError( + returncode=255, cmd=args[0]) + return d(*args, **kwargs) + + d = self.fake_lxd.check_call_mock.side_effect + self.fake_lxd.check_call_mock.side_effect = call_effect + + self.assertRaises(ContainerConnectionError, + self.make_containerbuild().execute) + + @patch('snapcraft.internal.lxd.Containerbuild._container_run') + def test_ftp_not_installed(self, mock_container_run): + mock_container_run.side_effect = lambda cmd, **kwargs: cmd + + def call_effect(*args, **kwargs): + if args[0][:1] == ['/usr/lib/sftp-server']: + raise FileNotFoundError( + 2, 'No such file or directory') + + self.fake_lxd.popen_mock.side_effect = call_effect + + self.assertIn( + 'You must have openssh-sftp-server installed to use a LXD ' + 'remote on a different host.\n', + str(self.assertRaises( + SnapcraftEnvironmentError, + self.make_containerbuild().execute))) + + @patch('snapcraft.internal.lxd.Containerbuild._container_run') + def test_ftp_error(self, mock_container_run): + mock_container_run.side_effect = lambda cmd, **kwargs: cmd + + def call_effect(*args, **kwargs): + if args[0][:1] == ['/usr/lib/sftp-server']: + raise CalledProcessError( + returncode=255, cmd=args[0]) + + self.fake_lxd.popen_mock.side_effect = call_effect + + self.assertIn( + 'sftp-server seems to be installed but could not be run.\n', + str(self.assertRaises( + SnapcraftEnvironmentError, + self.make_containerbuild().execute))) + + @patch('snapcraft.internal.lxd.Containerbuild._container_run') + def test_sshfs_failed(self, mock_container_run): + mock_container_run.side_effect = lambda cmd, **kwargs: cmd + + def call_effect(*args, **kwargs): + if args[0][:2] == ['lxc', 'exec'] and args[0][4] == 'ls': + return ''.encode('utf-8') + return self.fake_lxd.check_output_side_effect()(*args, **kwargs) + + self.fake_lxd.check_output_mock.side_effect = call_effect + + self.assertIn( + 'The project folder could not be mounted.\n', + str(self.assertRaises( + ContainerConnectionError, + self.make_containerbuild().execute))) + + +class LocalProjectTestCase(ContainerbuildTestCase): + + scenarios = [ + ('local', dict(remote='local', target_arch=None, server='x86_64')), + ] + + def make_containerbuild(self): + return lxd.Project(output='snap.snap', source='project.tar', + metadata={'name': 'project'}, + project_options=self.project_options, + remote=self.remote) + + @patch('snapcraft.internal.lxd.Containerbuild._container_run') + def test_start_failed(self, mock_container_run): + mock_container_run.side_effect = lambda cmd, **kwargs: cmd + + def call_effect(*args, **kwargs): + if args[0][:2] == ['lxc', 'start']: + raise CalledProcessError( + returncode=255, cmd=args[0]) + return d(*args, **kwargs) + + d = self.fake_lxd.check_call_mock.side_effect + self.fake_lxd.check_call_mock.side_effect = call_effect + + self.assertIn( + 'The container could not be started.\n' + 'The files /etc/subuid and /etc/subgid need to contain this line ', + str(self.assertRaises( + ContainerConnectionError, + self.make_containerbuild().execute))) + # Should not attempt to stop a container that wasn't started + self.assertNotIn(call(['lxc', 'stop', '-f', self.fake_lxd.name]), + self.fake_lxd.check_call_mock.call_args_list) + + +class FailedImageInfoTestCase(LXDBaseTestCase): + + remote = 'local' + server = 'x86_64' + target_arch = None + + scenarios = [ + ('CalledProcessError', dict( + exception=CalledProcessError, + kwargs=dict(cmd='testcmd', returncode=1, output='test output'), + expected_warn=( + "Failed to get container image info: " + "`lxc image list --format=json ubuntu:xenial/amd64` " + "returned with exit code 1, output: test output\n" + "It will not be recorded in manifest.\n"))), + ('JSONDecodeError', dict( + exception=json.decoder.JSONDecodeError, + kwargs=dict(msg='dummy', doc='dummy', pos=1), + expected_warn=( + "Failed to get container image info: Not in JSON format\n" + "It will not be recorded in manifest.\n"))), + ] + + def make_containerbuild(self): + return lxd.Project(output='snap.snap', source='project.tar', + metadata={'name': 'project'}, + project_options=self.project_options, + remote=self.remote) + + def test_failed_image_info_just_warns(self): + self.fake_logger = fixtures.FakeLogger(level=logging.WARN) + self.useFixture(self.fake_logger) + + def call_effect(*args, **kwargs): + if args[0][:3] == ['lxc', 'image', 'list']: + raise self.exception(**self.kwargs) + return d(*args, **kwargs) + + d = self.fake_lxd.check_output_mock.side_effect + + self.fake_lxd.check_output_mock.side_effect = call_effect + + self.make_containerbuild().execute() + self.assertEqual(self.fake_logger.output, self.expected_warn) diff -Nru snapcraft-2.34/snapcraft/tests/test_mangling.py snapcraft-2.35/snapcraft/tests/test_mangling.py --- snapcraft-2.34/snapcraft/tests/test_mangling.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/test_mangling.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,109 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 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 . + +import os +import textwrap + +from testtools.matchers import FileContains + +from snapcraft.internal import mangling + +from snapcraft import tests + + +def _create_file(filename, contents): + os.makedirs('test-dir', exist_ok=True) + file_path = os.path.join('test-dir', filename) + with open(file_path, 'w') as f: + f.write(contents) + + return file_path + + +class ManglingPythonShebangTestCase(tests.TestCase): + + def test_python(self): + file_path = _create_file('file', textwrap.dedent("""\ + #! /usr/bin/python2.7 + + # Larger file + """)) + mangling.rewrite_python_shebangs(os.path.dirname(file_path)) + self.assertThat(file_path, FileContains(textwrap.dedent("""\ + #!/usr/bin/env python2.7 + + # Larger file + """))) + + def test_python3(self): + file_path = _create_file('file', '#!/usr/bin/python3') + mangling.rewrite_python_shebangs(os.path.dirname(file_path)) + self.assertThat(file_path, FileContains('#!/usr/bin/env python3')) + + def test_python_args(self): + file_path1 = _create_file('file1', '#!/usr/bin/python') + file_path2 = _create_file('file2', textwrap.dedent("""\ + #! /usr/bin/python -E + + # Larger file + """)) + mangling.rewrite_python_shebangs(os.path.dirname(file_path1)) + self.assertThat(file_path1, FileContains('#!/usr/bin/env python')) + self.assertThat(file_path2, FileContains( + textwrap.dedent("""\ + #!/bin/sh + ''''exec python -E -- "$0" "$@" # ''' + + # Larger file + """))) + + def test_python3_args(self): + file_path1 = _create_file('file1', '#!/usr/bin/python3') + file_path2 = _create_file('file2', '#!/usr/bin/python3 -E') + mangling.rewrite_python_shebangs(os.path.dirname(file_path1)) + self.assertThat(file_path1, FileContains('#!/usr/bin/env python3')) + self.assertThat(file_path2, FileContains( + textwrap.dedent("""\ + #!/bin/sh + ''''exec python3 -E -- "$0" "$@" # '''"""))) + + def test_python_mixed_args(self): + file_path1 = _create_file('file1', '#!/usr/bin/python') + # Ensure extra spaces are chopped off + file_path2 = _create_file('file2', '#!/usr/bin/python3 -Es') + mangling.rewrite_python_shebangs(os.path.dirname(file_path1)) + self.assertThat(file_path1, FileContains('#!/usr/bin/env python')) + self.assertThat(file_path2, FileContains( + textwrap.dedent("""\ + #!/bin/sh + ''''exec python3 -Es -- "$0" "$@" # '''"""))) + + def test_following_docstring_no_rewrite(self): + file_path = _create_file('file', textwrap.dedent("""\ + #!/usr/bin/env python3.5 + + ''' + This is a test + ======================= + """)) + mangling.rewrite_python_shebangs(os.path.dirname(file_path)) + self.assertThat(file_path, FileContains(textwrap.dedent("""\ + #!/usr/bin/env python3.5 + + ''' + This is a test + ======================= + """))) diff -Nru snapcraft-2.34/snapcraft/tests/test_options.py snapcraft-2.35/snapcraft/tests/test_options.py --- snapcraft-2.34/snapcraft/tests/test_options.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/test_options.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,12 +14,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import os.path from unittest import mock import testtools from testtools.matchers import Equals import snapcraft +from snapcraft.internal import common from snapcraft.internal.errors import SnapcraftEnvironmentError from snapcraft import tests @@ -32,67 +34,78 @@ architecture=('64bit', 'ELF'), expected_arch_triplet='x86_64-linux-gnu', expected_deb_arch='amd64', - expected_kernel_arch='x86')), + expected_kernel_arch='x86', + expected_core_dynamic_linker='lib64/ld-linux-x86-64.so.2')), ('amd64-kernel-i686-userspace', dict( machine='x86_64', architecture=('32bit', 'ELF'), expected_arch_triplet='i386-linux-gnu', expected_deb_arch='i386', - expected_kernel_arch='x86')), + expected_kernel_arch='x86', + expected_core_dynamic_linker='lib/ld-linux.so.2')), ('i686', dict( machine='i686', architecture=('32bit', 'ELF'), expected_arch_triplet='i386-linux-gnu', expected_deb_arch='i386', - expected_kernel_arch='x86')), + expected_kernel_arch='x86', + expected_core_dynamic_linker='lib/ld-linux.so.2')), ('armv7l', dict( machine='armv7l', architecture=('32bit', 'ELF'), expected_arch_triplet='arm-linux-gnueabihf', expected_deb_arch='armhf', - expected_kernel_arch='arm')), + expected_kernel_arch='arm', + expected_core_dynamic_linker='lib/ld-linux-armhf.so.3')), ('aarch64', dict( machine='aarch64', architecture=('64bit', 'ELF'), expected_arch_triplet='aarch64-linux-gnu', expected_deb_arch='arm64', - expected_kernel_arch='arm64')), + expected_kernel_arch='arm64', + expected_core_dynamic_linker='lib/ld-linux-aarch64.so.1')), ('aarch64-kernel-armv7l-userspace', dict( machine='aarch64', architecture=('32bit', 'ELF'), expected_arch_triplet='arm-linux-gnueabihf', expected_deb_arch='armhf', - expected_kernel_arch='arm')), + expected_kernel_arch='arm', + expected_core_dynamic_linker='lib/ld-linux-armhf.so.3')), ('armv8l-kernel-armv7l-userspace', dict( machine='armv8l', architecture=('32bit', 'ELF'), expected_arch_triplet='arm-linux-gnueabihf', expected_deb_arch='armhf', - expected_kernel_arch='arm')), + expected_kernel_arch='arm', + expected_core_dynamic_linker='lib/ld-linux-armhf.so.3')), ('ppc', dict( machine='ppc', architecture=('32bit', 'ELF'), expected_arch_triplet='powerpc-linux-gnu', expected_deb_arch='powerpc', - expected_kernel_arch='powerpc')), + expected_kernel_arch='powerpc', + expected_core_dynamic_linker='lib/ld-linux.so.2')), ('ppc64le', dict( machine='ppc64le', architecture=('64bit', 'ELF'), expected_arch_triplet='powerpc64le-linux-gnu', expected_deb_arch='ppc64el', - expected_kernel_arch='powerpc')), + expected_kernel_arch='powerpc', + expected_core_dynamic_linker='lib64/ld64.so.2')), ('ppc64le-kernel-ppc-userspace', dict( machine='ppc64le', architecture=('32bit', 'ELF'), expected_arch_triplet='powerpc-linux-gnu', expected_deb_arch='powerpc', - expected_kernel_arch='powerpc')), + expected_kernel_arch='powerpc', + expected_core_dynamic_linker='lib/ld-linux.so.2')), ('s390x', dict( machine='s390x', architecture=('64bit', 'ELF'), expected_arch_triplet='s390x-linux-gnu', expected_deb_arch='s390x', - expected_kernel_arch='s390')) + expected_kernel_arch='s390', + expected_core_dynamic_linker='lib/ld64.so.1')) ] @mock.patch('platform.architecture') @@ -109,6 +122,20 @@ self.assertThat( options.kernel_arch, Equals(self.expected_kernel_arch)) + # The core dynamic linker is correct. Guard against stray absolute + # paths, as they cause os.path.join to discard the previous + # argument. + self.assertFalse(os.path.isabs(self.expected_core_dynamic_linker)) + with mock.patch('os.path.lexists') as mock_lexists: + mock_lexists.return_value = True + with mock.patch('os.path.islink') as mock_islink: + mock_islink.return_value = False + self.assertThat( + options.get_core_dynamic_linker(), + Equals(os.path.join( + common.get_core_path(), + self.expected_core_dynamic_linker))) + @mock.patch('platform.architecture') @mock.patch('platform.machine') def test_get_platform_architecture( @@ -136,3 +163,12 @@ SnapcraftEnvironmentError, "Cross compilation not supported for target arch 'x86_64'"): options.cross_compiler_prefix + + @mock.patch('platform.architecture') + @mock.patch('platform.machine') + def test_cross_compiler_prefix_empty( + self, mock_platform_machine, mock_platform_architecture): + mock_platform_machine.return_value = 'x86_64' + mock_platform_architecture.return_value = ('64bit', 'ELF') + options = snapcraft.ProjectOptions(target_deb_arch='i386') + self.assertThat(options.cross_compiler_prefix, Equals('')) diff -Nru snapcraft-2.34/snapcraft/tests/test_os_release.py snapcraft-2.35/snapcraft/tests/test_os_release.py --- snapcraft-2.34/snapcraft/tests/test_os_release.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snapcraft/tests/test_os_release.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,114 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2017 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 . + +from textwrap import dedent + +from testtools.matchers import Equals + +from snapcraft.internal import ( + os_release, + errors +) +from snapcraft import tests + + +class OsReleaseTestCase(tests.TestCase): + + def _write_os_release(self, contents): + path = 'os-release' + with open(path, 'w') as f: + f.write(contents) + return path + + def test_blank_lines(self): + release = os_release.OsRelease(os_release_file=self._write_os_release( + dedent("""\ + NAME="Arch Linux" + + PRETTY_NAME="Arch Linux" + ID=arch + ID_LIKE=archlinux + VERSION_ID="foo" + VERSION_CODENAME="bar" + + """))) + + self.assertThat(release.id(), Equals('arch')) + self.assertThat(release.name(), Equals('Arch Linux')) + self.assertThat(release.version_id(), Equals('foo')) + self.assertThat(release.version_codename(), Equals('bar')) + + def test_no_id(self): + release = os_release.OsRelease(os_release_file=self._write_os_release( + dedent("""\ + NAME="Arch Linux" + PRETTY_NAME="Arch Linux" + ID_LIKE=archlinux + VERSION_ID="foo" + VERSION_CODENAME="bar" + """))) + + self.assertRaises(errors.OsReleaseIdError, release.id) + + def test_no_name(self): + release = os_release.OsRelease(os_release_file=self._write_os_release( + dedent("""\ + ID=arch + PRETTY_NAME="Arch Linux" + ID_LIKE=archlinux + VERSION_ID="foo" + VERSION_CODENAME="bar" + """))) + + self.assertRaises(errors.OsReleaseNameError, release.name) + + def test_no_version_id(self): + release = os_release.OsRelease(os_release_file=self._write_os_release( + dedent("""\ + NAME="Arch Linux" + ID=arch + PRETTY_NAME="Arch Linux" + ID_LIKE=archlinux + VERSION_CODENAME="bar" + """))) + + self.assertRaises(errors.OsReleaseVersionIdError, release.version_id) + + def test_no_version_codename(self): + """Test that version codename can also come from VERSION_ID""" + release = os_release.OsRelease(os_release_file=self._write_os_release( + dedent("""\ + NAME="Ubuntu" + VERSION="14.04.5 LTS, Trusty Tahr" + ID=ubuntu + ID_LIKE=debian + PRETTY_NAME="Ubuntu 14.04.5 LTS" + VERSION_ID="14.04" + """))) + + self.assertThat(release.version_codename(), Equals('trusty')) + + def test_no_version_codename_or_version_id(self): + release = os_release.OsRelease(os_release_file=self._write_os_release( + dedent("""\ + NAME="Ubuntu" + ID=ubuntu + ID_LIKE=debian + PRETTY_NAME="Ubuntu 16.04.3 LTS" + """))) + + self.assertRaises( + errors.OsReleaseCodenameError, release.version_codename) diff -Nru snapcraft-2.34/snaps_tests/demos_tests/test_godd.py snapcraft-2.35/snaps_tests/demos_tests/test_godd.py --- snapcraft-2.34/snaps_tests/demos_tests/test_godd.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snaps_tests/demos_tests/test_godd.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017 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 . - -import snaps_tests - - -class GoddTestCase(snaps_tests.SnapsTestCase): - - snap_content_dir = 'godd' - - def test_godd(self): - # Build snap will raise an exception in case of error. - snap_path = self.build_snap(self.snap_content_dir) - # Install snap will raise an exception in case of error. - self.install_snap(snap_path, 'godd', '1.0') - - self.run_command_in_snappy_testbed( - 'sudo snap connect godd:mount-observe') - self.run_command_in_snappy_testbed('mkdir -p ~/snap/godd/common') - self.run_command_in_snappy_testbed('touch ~/snap/godd/common/test') - self.run_command_in_snappy_testbed( - '/snap/bin/godd ~/snap/godd/common/test /dev/null') diff -Nru snapcraft-2.34/snaps_tests/demos_tests/test_gopaste.py snapcraft-2.35/snaps_tests/demos_tests/test_gopaste.py --- snapcraft-2.34/snaps_tests/demos_tests/test_gopaste.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snaps_tests/demos_tests/test_gopaste.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,28 +0,0 @@ -# -*- 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 . - -import snaps_tests - - -class GopasteTestCase(snaps_tests.SnapsTestCase): - - snap_content_dir = 'gopaste' - - def test_gopaste(self): - snap_path = self.build_snap(self.snap_content_dir) - snap_name = 'gopaste' - self.install_snap(snap_path, snap_name, '1.0') - self.assert_service_running(snap_name, 'gopaste') diff -Nru snapcraft-2.34/snaps_tests/demos_tests/test_libpipeline.py snapcraft-2.35/snaps_tests/demos_tests/test_libpipeline.py --- snapcraft-2.34/snaps_tests/demos_tests/test_libpipeline.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snaps_tests/demos_tests/test_libpipeline.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,32 +0,0 @@ -# -*- 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 . - -import snaps_tests - - -class LibPipelineTestCase(snaps_tests.SnapsTestCase): - - snap_content_dir = 'libpipeline' - - def test_libpipeline(self): - snap_path = self.build_snap(self.snap_content_dir) - self.install_snap(snap_path, 'pipelinetest', '1.0') - expected = ( - 'running echo test | grep s | grep t\n' - 'custom libpipeline called\n' - 'test\n') - self.assert_command_in_snappy_testbed( - '/snap/bin/pipelinetest', expected) diff -Nru snapcraft-2.34/snaps_tests/demos_tests/test_mosquitto.py snapcraft-2.35/snaps_tests/demos_tests/test_mosquitto.py --- snapcraft-2.34/snaps_tests/demos_tests/test_mosquitto.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snaps_tests/demos_tests/test_mosquitto.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2017 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 . - -import time - -import snaps_tests - - -class MosquittoTestCase(snaps_tests.SnapsTestCase): - - snap_content_dir = 'mosquitto' - - def test_mosquitto(self): - snap_path = self.build_snap(self.snap_content_dir) - snap_name = 'mosquitto' - self.install_snap(snap_path, snap_name, '0.1') - self.assert_service_running(snap_name, 'mosquitto') - if not snaps_tests.config.get('skip-install', False): - # The subscriber will exit after the first message. - process = self.snappy_testbed.run_command_in_background( - ['/snap/bin/mosquitto.subscribe', 'test-mosquitto-topic']) - self.addCleanup(process.wait, 30) - time.sleep(5) - self.assert_command_in_snappy_testbed( - ['/snap/bin/mosquitto.publish', 'test-mosquitto-topic', - 'test-message'], '') - self.assert_command_in_snappy_testbed( - ['/snap/bin/mosquitto.publish', 'test-mosquitto-topic', - 'exit'], '') - self.assert_command_in_snappy_testbed( - ['cat', '~/snap/mosquitto/x*/' - 'mosquitto.subscriber.log'], - 'MQTT subscriber connected.\n' - "test-mosquitto-topic b'test-message'\n" - "test-mosquitto-topic b'exit'\n") diff -Nru snapcraft-2.34/snaps_tests/demos_tests/test_plainbox_test_tool.py snapcraft-2.35/snaps_tests/demos_tests/test_plainbox_test_tool.py --- snapcraft-2.34/snaps_tests/demos_tests/test_plainbox_test_tool.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snaps_tests/demos_tests/test_plainbox_test_tool.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,40 +0,0 @@ -# -*- 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 . - -import snaps_tests - - -class PlainboxTestCase(snaps_tests.SnapsTestCase): - - snap_content_dir = 'plainbox-test-tool' - - def test_plainbox(self): - # build and install the example - snap_path = self.build_snap(self.snap_content_dir) - self.install_snap(snap_path, 'plainbox-test-tool', '0.1') - # check that can run plainbox and list the tests available - expected = """2013.com.canonical.plainbox::collect-manifest -2013.com.canonical.plainbox::manifest -2016.com.example::always-fail -2016.com.example::always-pass -""" - self.assert_command_in_snappy_testbed( - '/snap/bin/plainbox-test-tool.plainbox dev special -j', - expected) - # check can run the tests in the example provider - self.run_command_in_snappy_testbed('/snap/bin/plainbox-test-tool.' - 'plainbox run ' - '-i 2016.com.example::.*') diff -Nru snapcraft-2.34/snaps_tests/demos_tests/test_rosinstall.py snapcraft-2.35/snaps_tests/demos_tests/test_rosinstall.py --- snapcraft-2.34/snaps_tests/demos_tests/test_rosinstall.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snaps_tests/demos_tests/test_rosinstall.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,42 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2017 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 . - -import re -from unittest import skipUnless - -from snapcraft.internal.common import get_os_release_info - -import snaps_tests - - -class RosinstallTestCase(snaps_tests.SnapsTestCase): - - snap_content_dir = 'rosinstall' - - @skipUnless(get_os_release_info()['VERSION_CODENAME'] == 'xenial', - 'This test fails on yakkety LP: #1614476') - def test_rosinstall(self): - snap_path = self.build_snap(self.snap_content_dir, timeout=1800) - - self.install_snap(snap_path, 'rosinstall-demo', '1.0') - - # Run the ROS system. By default this will never exit, but the demo - # supports an `exit-after-receive` parameter that, if true, will cause - # the system to shutdown after the listener has successfully received - # a message. - self.assert_command_in_snappy_testbed_with_regex([ - '/snap/bin/rosinstall-demo.run', - 'exit-after-receive:=true'], r'.*I heard hello world.*', re.DOTALL) diff -Nru snapcraft-2.34/snaps_tests/demos_tests/test_ros.py snapcraft-2.35/snaps_tests/demos_tests/test_ros.py --- snapcraft-2.34/snaps_tests/demos_tests/test_ros.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snaps_tests/demos_tests/test_ros.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,70 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2017 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 . - -import os -import re -import subprocess -from unittest import skipUnless - -from testtools.matchers import Equals - -import snapcraft -from snapcraft.internal.common import get_os_release_info -import snaps_tests - - -class ROSTestCase(snaps_tests.SnapsTestCase): - - snap_content_dir = 'ros' - - @skipUnless(get_os_release_info()['VERSION_CODENAME'] == 'xenial', - 'This test fails on yakkety LP: #1614476') - def test_ros(self): - try: - failed = True - snap_path = self.build_snap(self.snap_content_dir, timeout=1800) - failed = False - except snaps_tests.CommandError: - if snapcraft.ProjectOptions().deb_arch == 'arm64': - # https://bugs.launchpad.net/snapcraft/+bug/1662915 - self.expectFailure( - 'There are no arm64 Indigo packages in the ROS archive', - self.assertFalse, failed) - else: - raise - - self.install_snap(snap_path, 'ros-example', '1.0') - # check that the hardcoded /usr/bin/python in rosversion - # is changed to using /usr/bin/env python - expected = b'#!/usr/bin/env python\n' - output = subprocess.check_output( - "sed -n '/env/p;1q' prime/usr/bin/rosversion", - cwd=os.path.join(self.path, self.snap_content_dir), shell=True) - self.assertThat(output, Equals(expected)) - - # Regression test for LP: #1660852. Make sure --help actually gets - # passed to rosaunch instead of being eaten by setup.sh. - self.assert_command_in_snappy_testbed_with_regex([ - '/snap/bin/ros-example.launch-project', '--help'], - r'.*Usage: roslaunch.*') - - # Run the ROS system. By default this will never exit, but the demo - # supports an `exit-after-receive` parameter that, if true, will cause - # the system to shutdown after the listener has successfully received - # a message. - self.assert_command_in_snappy_testbed_with_regex([ - '/snap/bin/ros-example.launch-project', - 'exit-after-receive:=true'], r'.*I heard Hello world.*', re.DOTALL) diff -Nru snapcraft-2.34/snaps_tests/demos_tests/test_shared_ros.py snapcraft-2.35/snaps_tests/demos_tests/test_shared_ros.py --- snapcraft-2.34/snaps_tests/demos_tests/test_shared_ros.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snaps_tests/demos_tests/test_shared_ros.py 2017-11-01 19:41:33.000000000 +0000 @@ -14,27 +14,23 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import snaps_tests - import os import re import subprocess -from unittest import skipUnless -from snapcraft.internal.common import get_os_release_info +from snaps_tests import SnapsTestCase, skip -class SharedROSTestCase(snaps_tests.SnapsTestCase): +class SharedROSTestCase(SnapsTestCase): snap_content_dir = 'shared-ros' - @skipUnless(get_os_release_info()['VERSION_CODENAME'] == 'xenial', - 'This test fails on yakkety LP: #1614476') + @skip.skip_unless_codename('xenial', 'ROS Kinetic only targets Xenial') def test_shared_ros(self): ros_base_path = os.path.join(self.snap_content_dir, 'ros-base') ros_app_path = os.path.join(self.snap_content_dir, 'ros-app') - base_snap_path = self.build_snap(ros_base_path, timeout=1800) + base_snap_path = self.build_snap(ros_base_path, timeout=10000) # Now tar up its staging area to be used to build ros-app subprocess.check_call([ @@ -42,7 +38,7 @@ os.path.dirname(base_snap_path), 'stage'], cwd=self.src_dir) # Now build ros-app - app_snap_path = self.build_snap(ros_app_path, timeout=1800) + app_snap_path = self.build_snap(ros_app_path, timeout=10000) # Install both snaps self.install_snap(base_snap_path, 'ros-base', '1.0') diff -Nru snapcraft-2.34/snaps_tests/demos_tests/test_shout.py snapcraft-2.35/snaps_tests/demos_tests/test_shout.py --- snapcraft-2.34/snaps_tests/demos_tests/test_shout.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snaps_tests/demos_tests/test_shout.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,28 +0,0 @@ -# -*- 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 . - -import snaps_tests - - -class ShoutTestCase(snaps_tests.SnapsTestCase): - - snap_content_dir = 'shout' - - def test_shout(self): - snap_path = self.build_snap(self.snap_content_dir) - snap_name = 'shout' - self.install_snap(snap_path, snap_name, '0.52.0') - self.assert_service_running(snap_name, 'server') diff -Nru snapcraft-2.34/snaps_tests/demos_tests/tests.py snapcraft-2.35/snaps_tests/demos_tests/tests.py --- snapcraft-2.34/snaps_tests/demos_tests/tests.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snaps_tests/demos_tests/tests.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,42 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2017 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 . - -import testscenarios - -import snaps_tests - - -class TestSnapcraftExamples( - testscenarios.WithScenarios, snaps_tests.SnapsTestCase): - - scenarios = [ - ('py2-project', { - 'snap_content_dir': 'py2-project', - 'name': 'spongeshaker', - 'version': '0', - }), - ('py3-project', { - 'snap_content_dir': 'py3-project', - 'name': 'spongeshaker', - 'version': '0', - }), - ] - - def test_demo(self): - # Build snap will raise an exception in case of error. - snap_path = self.build_snap(self.snap_content_dir) - # Install snap will raise an exception in case of error. - self.install_snap(snap_path, self.name, self.version) diff -Nru snapcraft-2.34/snaps_tests/demos_tests/test_webchat.py snapcraft-2.35/snaps_tests/demos_tests/test_webchat.py --- snapcraft-2.34/snaps_tests/demos_tests/test_webchat.py 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/snaps_tests/demos_tests/test_webchat.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,28 +0,0 @@ -# -*- 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 . - -import snaps_tests - - -class WebchatTestCase(snaps_tests.SnapsTestCase): - - snap_content_dir = 'webchat' - - def test_webchat(self): - snap_path = self.build_snap(self.snap_content_dir) - snap_name = 'webchat' - self.install_snap(snap_path, snap_name, '0.0.1') - self.assert_service_running(snap_name, 'webchat') diff -Nru snapcraft-2.34/snaps_tests/skip.py snapcraft-2.35/snaps_tests/skip.py --- snapcraft-2.34/snaps_tests/skip.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.35/snaps_tests/skip.py 2017-11-01 19:41:33.000000000 +0000 @@ -0,0 +1,39 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2017 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 . + +import contextlib +import functools + +from unittest import skipUnless + +from snapcraft.internal import errors, os_release + + +# TODO: remove this duplicated functionality once __init__ for +# the testing packages are cleaned up. (LP: #1729593) +def skip_unless_codename(codename, message): + def _wrap(func): + release = os_release.OsRelease() + actual_codename = None + with contextlib.suppress(errors.OsReleaseCodenameError): + actual_codename = release.version_codename() + + @functools.wraps(func) + @skipUnless(actual_codename == codename, message) + def _skip_test(*args, **kwargs): + func(*args, **kwargs) + return _skip_test + return _wrap diff -Nru snapcraft-2.34/TESTING.md snapcraft-2.35/TESTING.md --- snapcraft-2.34/TESTING.md 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/TESTING.md 2017-11-01 19:41:33.000000000 +0000 @@ -33,10 +33,16 @@ The integration tests are a group of suites that excercise snapcraft as a black box. They are only allowed to set up the environment where snapcraft runs and create files; but for the execution phase of the test they can only run the snapcraft command or one of its subcommands. To verify the results they can check the output printed to the command line, the return value of the snapcraft command, and any files created during the execution. -This suite was split in three: plugins, store and other integration tests. This split is artificial, we made it just because the full suite takes more time than what Travis allows for a single job. +This suite was split in four: plugins, store, snapd and other integration tests. This split is artificial, we made it just because the full suite takes more time than what Travis allows for a single job. These tests are in the `integration_tests` directory, with the `snapcraft.yamls` and other source files for the tests snaps in `integration_tests/snaps`. +### Slow tests + +Some tests take too long. This affects the pull requests because we have to wait for a long time, and they will make Travis CI timeout because we have only 50 minutes per suite in there. The solution is to tag these tests as slow, and don't run them in all pull requests. These tests will only be run in autopkgtests. + +To mark a test case as slow, set the class attribute `slow_test = True`. + ### Snaps tests The snaps tests is a suite of high-level tests that try to simulate real-world scenarios of a user interacting with snapcraft. They cover the call to snapcraft to generate a snap file from the source files of a fully functional project, the installation of the resulting snap, and the execution of the binaries and services of this snap. diff -Nru snapcraft-2.34/tools/staging_env.sh snapcraft-2.35/tools/staging_env.sh --- snapcraft-2.34/tools/staging_env.sh 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/tools/staging_env.sh 2017-11-01 19:41:33.000000000 +0000 @@ -11,8 +11,8 @@ unset deactivate } -export UBUNTU_STORE_API_ROOT_URL="https://myapps.developer.staging.ubuntu.com/dev/api/" -export UBUNTU_STORE_SEARCH_ROOT_URL="https://search.apps.staging.ubuntu.com/" +export UBUNTU_STORE_API_ROOT_URL="https://dashboard.staging.snapcraft.io/dev/api/" +export UBUNTU_STORE_SEARCH_ROOT_URL="https://api.staging.snapcraft.io/" export UBUNTU_STORE_UPLOAD_ROOT_URL="https://upload.apps.staging.ubuntu.com/" export UBUNTU_SSO_API_ROOT_URL="https://login.staging.ubuntu.com/api/v2/" export TEST_STORE="staging" diff -Nru snapcraft-2.34/tools/travis/build_snapcraft_snap.sh snapcraft-2.35/tools/travis/build_snapcraft_snap.sh --- snapcraft-2.34/tools/travis/build_snapcraft_snap.sh 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/tools/travis/build_snapcraft_snap.sh 2017-11-01 19:41:33.000000000 +0000 @@ -31,6 +31,8 @@ $lxc file push --recursive $project_path snap-builder/root/ # TODO use the stable snap once it's published. $lxc exec snap-builder -- sh -c "snap install snapcraft --candidate --classic" -$lxc exec snap-builder -- sh -c "cd snapcraft && /snap/bin/snapcraft" - +$lxc exec snap-builder -- sh -c "cd snapcraft && /snap/bin/snapcraft snap --output snapcraft-pr$TRAVIS_PULL_REQUEST.snap" +# Pull the snap from the container to save it into the cache. +mkdir -p "$TRAVIS_BUILD_DIR/snaps-cache" +$lxc file pull "snap-builder/root/snapcraft/snapcraft-pr$TRAVIS_PULL_REQUEST.snap" "$TRAVIS_BUILD_DIR/snaps-cache/" $lxc stop snap-builder diff -Nru snapcraft-2.34/tools/travis/run_lxd_container.sh snapcraft-2.35/tools/travis/run_lxd_container.sh --- snapcraft-2.34/tools/travis/run_lxd_container.sh 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/tools/travis/run_lxd_container.sh 2017-11-01 19:41:33.000000000 +0000 @@ -33,7 +33,7 @@ echo "Starting the LXD container." # FIXME switch back to unprovileged once LP: #1709536 is fixed -$lxc launch --ephemeral --config security.privileged=true ubuntu:xenial "$name" +$lxc launch --ephemeral --config security.privileged=true --config security.nesting=true ubuntu:xenial "$name" # This is likely needed to wait for systemd in the container to start and get # an IP, configure DNS. First boot is always a bit slow because cloud-init # needs to run too. @@ -52,6 +52,7 @@ $lxc config set "$name" environment.GH_TOKEN "$GH_TOKEN" $lxc config set "$name" environment.CODECOV_TOKEN "$CODECOV_TOKEN" $lxc config set "$name" environment.LC_ALL "C.UTF-8" +$lxc config set "$name" environment.SNAPCRAFT_FROM_INSTALLED "1" $lxc exec "$name" -- apt update diff -Nru snapcraft-2.34/tools/travis/run_tests.sh snapcraft-2.35/tools/travis/run_tests.sh --- snapcraft-2.34/tools/travis/run_tests.sh 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/tools/travis/run_tests.sh 2017-11-01 19:41:33.000000000 +0000 @@ -33,8 +33,11 @@ dependencies="apt install -y python3-pip && python3 -m pip install -r requirements-devel.txt" elif [ "$test" = "unit" ]; then dependencies="apt install -y git bzr subversion mercurial libnacl-dev libsodium-dev libffi-dev libapt-pkg-dev libarchive-dev python3-pip squashfs-tools xdelta3 && python3 -m pip install -r requirements-devel.txt -r requirements.txt codecov && apt install -y python3-coverage" -elif [ "$test" = "integration" ] || [ "$test" = "plugins" ] || [ "$test" = "store" ]; then - dependencies="apt install -y bzr curl git libnacl-dev libsodium-dev libffi-dev libapt-pkg-dev libarchive-dev mercurial python3-pip subversion squashfs-tools sudo snapd xdelta3 && python3 -m pip install -r requirements-devel.txt -r requirements.txt" +elif [ "$test" = "integration" ] || [ "$test" = "plugins" ] || [ "$test" = "store" ] || [ "$test" = "containers" ] || [ "$test" = "snapd" ]; then + # snap install core exits with this error message: + # - Setup snap "core" (2462) security profiles (cannot reload udev rules: exit status 2 + # but the installation succeeds, so we just ingore it. + dependencies="apt install -y bzr curl git libnacl-dev libsodium-dev libffi-dev libapt-pkg-dev libarchive-dev mercurial python3-pip subversion squashfs-tools sudo snapd xdelta3 && python3 -m pip install -r requirements-devel.txt -r requirements.txt && (snap install core || echo 'ignored error') && sudo snap install snaps-cache/snapcraft-pr$TRAVIS_PULL_REQUEST.snap --dangerous --classic" else echo "Unknown test suite: $test" exit 1 @@ -47,11 +50,9 @@ "$script_path/setup_lxd.sh" "$script_path/run_lxd_container.sh" test-runner + $lxc file push --recursive $project_path test-runner/root/ $lxc exec test-runner -- sh -c "cd snapcraft && $dependencies" -# Workaround for -# - Setup snap "core" (2462) security profiles (cannot reload udev rules: exit status 2 -[ "$test" = "integration" ] && $lxc exec test-runner -- sh -c "snap install core" || echo "ignored error" $lxc exec test-runner -- sh -c "cd snapcraft && ./runtests.sh $test $pattern" if [ "$test" = "unit" ]; then diff -Nru snapcraft-2.34/tools/travis/setup_lxd.sh snapcraft-2.35/tools/travis/setup_lxd.sh --- snapcraft-2.34/tools/travis/setup_lxd.sh 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/tools/travis/setup_lxd.sh 2017-11-01 19:41:33.000000000 +0000 @@ -26,6 +26,7 @@ sleep 1 done apt-get install --yes snapd + # Use edge because the feature to copy links to the container has not yet been # released to stable: # https://github.com/lxc/lxd/commit/004e7c361e1d914795d3ba7582654622e32ff193 @@ -36,7 +37,7 @@ # From LXD's CI. # shellcheck disable=SC2034 for i in $(seq 12); do - lxd waitready --timeout=10 >/dev/null 2>&1 && break + /snap/bin/lxd waitready --timeout=10 >/dev/null 2>&1 && break done /snap/bin/lxd init --auto diff -Nru snapcraft-2.34/.travis.yml snapcraft-2.35/.travis.yml --- snapcraft-2.34/.travis.yml 2017-09-06 13:51:35.000000000 +0000 +++ snapcraft-2.35/.travis.yml 2017-11-01 19:41:33.000000000 +0000 @@ -5,25 +5,43 @@ language: bash +cache: + directories: + - $TRAVIS_BUILD_DIR/snaps-cache + jobs: include: # Tests, only when not triggered by the daily cron. - stage: static - script: if [ "$TRAVIS_EVENT_TYPE" != "cron" ]; then sudo ./tools/travis/run_tests.sh static; fi + if: type != cron + script: sudo ./tools/travis/run_tests.sh static - stage: unit - script: if [ "$TRAVIS_EVENT_TYPE" != "cron" ]; then sudo ./tools/travis/run_tests.sh unit; fi - - script: if [ "$TRAVIS_EVENT_TYPE" != "cron" ]; then SNAPCRAFT_TEST_MOCK_MACHINE=armv7l sudo ./tools/travis/run_tests.sh unit; fi + if: type != cron + script: sudo ./tools/travis/run_tests.sh unit + - if: type != cron + script: SNAPCRAFT_TEST_MOCK_MACHINE=armv7l sudo ./tools/travis/run_tests.sh unit + - stage: snap + if: type != cron + script: sudo ./tools/travis/build_snapcraft_snap.sh - stage: integration - script: if [ "$TRAVIS_EVENT_TYPE" != "cron" ]; then sudo ./tools/travis/run_tests.sh integration; fi - - script: if [ "$TRAVIS_EVENT_TYPE" != "cron" ]; then sudo ./tools/travis/run_tests.sh plugins; fi - - script: if [ "$TRAVIS_EVENT_TYPE" != "cron" ]; then sudo ./tools/travis/run_tests.sh store; fi + if: type != cron + script: sudo ./tools/travis/run_tests.sh integration + - if: type != cron + script: sudo ./tools/travis/run_tests.sh plugins + - if: type != cron + script: sudo ./tools/travis/run_tests.sh store + - if: type != cron + script: sudo ./tools/travis/run_tests.sh containers + - if: type != cron + script: sudo ./tools/travis/run_tests.sh snapd # CLA check, only in pull requests, not comming from the bot. - - script: if [ "$TRAVIS_PULL_REQUEST" != "false" ] && [ "$TRAVIS_EVENT_TYPE" != 'cron' ] && [ "TRAVIS_PULL_REQUEST_SLUG" != 'snappy-m-o/snapcraft' ]; then ./tools/travis/run_cla_check.sh; fi - - stage: snap - script: if [ "$TRAVIS_EVENT_TYPE" != "cron" ]; then sudo ./tools/travis/build_snapcraft_snap.sh; fi + - if: type = pull_request AND sender != snappy-m-o + script: ./tools/travis/run_cla_check.sh # Trigger edge tests, only in the daily cron. - stage: edge - script: if [ "$TRAVIS_EVENT_TYPE" = "cron" ]; then ./runtests.sh spread; fi + if: type = cron + script: ./runtests.sh spread # Trigger beta tests, only in the daily cron. - stage: beta - script: if [ "$TRAVIS_EVENT_TYPE" = "cron" ]; then ./tools/travis/make_beta_pr.sh; fi + if: type = cron + script: ./tools/travis/make_beta_pr.sh