diff -Nru snapcraft-2.40/bin/snapcraftctl snapcraft-2.41/bin/snapcraftctl --- snapcraft-2.40/bin/snapcraftctl 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/bin/snapcraftctl 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,55 @@ +#!/bin/sh +# +# This file exists because snapcraftctl must be run using a clean environment +# that is uninfluenced by the environment of the part using it. There are a few +# reasons for this: +# +# 1. snapcraftctl is a python3 utility, but Snapcraft supports building python2 +# parts, where PYTHONPATH et. al. are set for python2. +# 2. snapcraftctl is part of snapcraft, which loads up various libraries that +# can be influenced with LD_LIBRARY_PATH, which is set for many parts. +# +# Not only this, but the only way snapcraftctl works reliably is if it's run +# by exactly the same interpreter as snapcraft itself (otherwise it won't find +# snapcraft). To that end, this script will use the interpreter defined within +# the SNAPCRAFT_INTERPRETER environment variable. + +# Which python3 are we using? By default, the one from the PATH. If +# SNAPCRAFT_INTERPRETER is specified, use that one instead. +python3_command="${SNAPCRAFT_INTERPRETER:-$(which python3)}" + +snapcraftctl_command="""$python3_command"" -c ' +import snapcraft.cli.__main__ + +# Click strips off the first arg by default, so the -c will not be passed +snapcraft.cli.__main__.run_snapcraftctl(prog_name=\"snapcraftctl\") +' ""$@" + + +# We don't actually want a 100% clean environment. Pass on the SNAP variables, +# locale settings, and environment variables required by snapcraftctl itself. +/usr/bin/env -i -- sh -< Sat, 14 Apr 2018 12:13:35 +0000 + +snapcraft (2.40.1) xenial; urgency=medium + + [ Evan Dandrea ] + * readme: polish the landing page (#2022) + + [ Christian Dywan ] + * repo: catch error due to broken build packages (#2023) + + [ Sergio Schvezov ] + * pluginhandler: organize correcly for targets with leading / (#2034) + + -- Sergio Schvezov Tue, 27 Mar 2018 21:09:24 +0000 + snapcraft (2.40) xenial; urgency=medium [ Sergio Schvezov ] diff -Nru snapcraft-2.40/debian/control snapcraft-2.41/debian/control --- snapcraft-2.40/debian/control 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/debian/control 2018-04-14 12:13:35.000000000 +0000 @@ -50,7 +50,8 @@ Package: snapcraft Architecture: all -Depends: execstack, +Depends: binutils, + execstack, patchelf (>= 0.9), python3-apt, python3-debian, diff -Nru snapcraft-2.40/debian/snapcraft.install snapcraft-2.41/debian/snapcraft.install --- snapcraft-2.40/debian/snapcraft.install 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/debian/snapcraft.install 2018-04-14 12:13:35.000000000 +0000 @@ -1,3 +1,4 @@ /usr/bin/snapcraft +/usr/bin/snapcraftctl /usr/lib/python* -/usr/share/snapcraft \ No newline at end of file +/usr/share/snapcraft diff -Nru snapcraft-2.40/debian/tests/control snapcraft-2.41/debian/tests/control --- snapcraft-2.40/debian/tests/control 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/debian/tests/control 2018-04-14 12:13:35.000000000 +0000 @@ -1,4 +1,6 @@ Tests: integrationtests-general, + integrationtests-lifecycle, + integrationtests-sources, integrationtests-store, integrationtests-plugins, integrationtests-plugins-python, diff -Nru snapcraft-2.40/debian/tests/integrationtests-lifecycle snapcraft-2.41/debian/tests/integrationtests-lifecycle --- snapcraft-2.40/debian/tests/integrationtests-lifecycle 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/debian/tests/integrationtests-lifecycle 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e + +script_path="$(dirname "$0")" +SNAPCRAFT_AUTOPKGTEST_SUITES=tests/integration/lifecycle $script_path/integrationtests diff -Nru snapcraft-2.40/debian/tests/integrationtests-sources snapcraft-2.41/debian/tests/integrationtests-sources --- snapcraft-2.40/debian/tests/integrationtests-sources 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/debian/tests/integrationtests-sources 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,6 @@ +#!/bin/sh + +set -e + +script_path="$(dirname "$0")" +SNAPCRAFT_AUTOPKGTEST_SUITES=tests/integration/sources $script_path/integrationtests diff -Nru snapcraft-2.40/MANIFEST.in snapcraft-2.41/MANIFEST.in --- snapcraft-2.40/MANIFEST.in 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/MANIFEST.in 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,2 @@ +include libraries/* +include schema/* diff -Nru snapcraft-2.40/patches/ctypes_init.diff snapcraft-2.41/patches/ctypes_init.diff --- snapcraft-2.40/patches/ctypes_init.diff 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/patches/ctypes_init.diff 2018-04-14 12:13:35.000000000 +0000 @@ -1,20 +1,58 @@ -406a407,417 -> -> _ARCH_TRIPLET = { -> 'arm64': 'aarch64-linux-gnu', -> 'i386': 'i386-linux-gnu', -> 'ppc64el': 'powerpc64le-linux-gnu', -> 'powerpc': 'powerpc-linux-gnu', -> 'amd64': 'x86_64-linux-gnu', -> 's390x': 's390x-linux-gnu', -> } -> -> -421a433,439 -> if _os.getenv('SNAP_NAME', '') == 'snapcraft': -> _name = _os.path.join( -> _os.getenv('SNAP'), 'usr', 'lib', -> _ARCH_TRIPLET.get(_os.getenv('SNAP_ARCH')), -> name) -> if _os.path.exists(_name): -> name = _name +--- __init__.py.orig 2018-04-14 14:46:40.222772831 -0300 ++++ __init__.py 2018-04-14 14:47:29.835488413 -0300 +@@ -311,6 +311,16 @@ + ################################################################ + + ++_ARCH_TRIPLET = { ++ 'arm64': 'aarch64-linux-gnu', ++ 'i386': 'i386-linux-gnu', ++ 'ppc64el': 'powerpc64le-linux-gnu', ++ 'powerpc': 'powerpc-linux-gnu', ++ 'amd64': 'x86_64-linux-gnu', ++ 's390x': 's390x-linux-gnu', ++} ++ ++ + class CDLL(object): + """An instance of this class represents a loaded dll/shared + library, exporting functions using the standard C calling +@@ -344,7 +354,15 @@ + self._FuncPtr = _FuncPtr + + if handle is None: +- self._handle = _dlopen(self._name, mode) ++ name = self._name ++ if name is not None and _os.getenv('SNAP_NAME', '') == 'snapcraft': ++ _name = _os.path.join( ++ _os.getenv('SNAP'), 'usr', 'lib', ++ _ARCH_TRIPLET.get(_os.getenv('SNAP_ARCH')), ++ name) ++ if _os.path.exists(_name): ++ name = _name ++ self._handle = _dlopen(name, mode) + else: + self._handle = handle + +@@ -407,6 +425,7 @@ + _func_flags_ = _FUNCFLAG_STDCALL + _func_restype_ = HRESULT + ++ + class LibraryLoader(object): + def __init__(self, dlltype): + self._dlltype = dlltype +@@ -419,6 +438,13 @@ + return dll + + def __getitem__(self, name): ++ if _os.getenv('SNAP_NAME', '') == 'snapcraft': ++ _name = _os.path.join( ++ _os.getenv('SNAP'), 'usr', 'lib', ++ _ARCH_TRIPLET.get(_os.getenv('SNAP_ARCH')), ++ name) ++ if _os.path.exists(_name): ++ name = _name + return getattr(self, name) + + def LoadLibrary(self, name): diff -Nru snapcraft-2.40/README.md snapcraft-2.41/README.md --- snapcraft-2.40/README.md 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/README.md 2018-04-14 12:13:35.000000000 +0000 @@ -2,38 +2,30 @@ # Snapcraft -Snapcraft is a delightful packaging tool +Package, distribute, and update any app for Linux and IoT. -Snapcraft helps you assemble a whole project in a single tree out of -many pieces. It can drive a very wide range of build and packaging systems, -so that you can simply list all the upstream projects you want and have -them built and installed together as a single tree. +Snaps are containerised software packages that are simple to create and +install. They auto-update and are safe to run. And because they bundle their +dependencies, they work on all major Linux systems without modification. -For example, say you want to make a product that includes PyPI packages, -Node.js packages from NPM, Java, and a bunch of daemons written in C and -C++ that are built with autotools, snapcraft would make assembling the -final tree very easy. +[Build your first snap](https://docs.snapcraft.io/build-snaps/languages) or learn more about how [Snapcraft can help you](https://snapcraft.io). -Snapcraft allows easy crafting of snap packages for the [snappy Ubuntu Core](http://ubuntu.com/snappy) -transactional update system. +## Get support -## More Information +We’re here to help. Ask your questions at the [Snapcraft Forum](https://forum.snapcraft.io). Report bugs on [Launchpad](https://bugs.launchpad.net/snapcraft/+filebug). -* [Introduction](https://snapcraft.io/docs/) to all the details about the concepts behind snapcraft. -* [Hacking guide](HACKING.md) to contribute if you're interested in developing Snapcraft. -* [Launchpad](https://bugs.launchpad.net/snapcraft) to submit bugs or issues. +Learn about the latest features by following Snapcraft on +[Twitter](https://twitter.com/snapcraftio), +[Google+](https://plus.google.com/+SnapcraftIo) or +[Facebook](https://www.facebook.com/snapcraftio). -## Get in touch +## Contribute to Snapcraft -We're friendly! Talk to us on [IRC](https://webchat.freenode.net/?channels=snappy) or on [our forums](https://forum.snapcraft.io/). +We love contributors. Read the [hacking guide](HACKING.md) if you're interested in helping out. -Get news and stay up to date on [Twitter](https://twitter.com/snapcraftio), -[Google+](https://plus.google.com/+SnapcraftIo) or -[Facebook](https://www.facebook.com/snapcraftio). [travis-image]: https://travis-ci.org/snapcore/snapcraft.svg?branch=master [travis-url]: https://travis-ci.org/snapcore/snapcraft [codecov-image]: https://codecov.io/github/snapcore/snapcraft/coverage.svg?branch=master [codecov-url]: https://codecov.io/github/snapcore/snapcraft?branch=master - diff -Nru snapcraft-2.40/requirements.txt snapcraft-2.41/requirements.txt --- snapcraft-2.40/requirements.txt 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/requirements.txt 2018-04-14 12:13:35.000000000 +0000 @@ -17,6 +17,7 @@ pymacaroons==0.10.0; sys_platform == 'win32' pymacaroons-pynacl==0.9.3 pysha3==1.0b1; python_version < '3.6' +raven==6.5.0 simplejson==3.8.2 tabulate==0.7.5 python-debian==0.1.28 diff -Nru snapcraft-2.40/schema/snapcraft.yaml snapcraft-2.41/schema/snapcraft.yaml --- snapcraft-2.40/schema/snapcraft.yaml 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/schema/snapcraft.yaml 2018-04-14 12:13:35.000000000 +0000 @@ -183,6 +183,9 @@ - type: string minLength: 1 - type: number + passthrough: + type: object + description: properties to be passed into snap.yaml as-is apps: type: object additionalProperties: false @@ -296,6 +299,9 @@ usage: "socket path, a string" socket-mode: type: integer + passthrough: + type: object + description: properties to be passed into snap.yaml as-is hooks: type: object additionalProperties: false @@ -314,6 +320,9 @@ uniqueItems: true items: type: string + passthrough: + type: object + description: properties to be passed into snap.yaml as-is parts: type: object minProperties: 1 @@ -324,13 +333,101 @@ 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 allOf: + # Make sure snap/prime are mutually exclusive - not: type: object required: [snap, prime] validation-failure: - "{.instance} cannot contain both 'snap' and 'prime' keywords." + "Parts cannot contain both 'snap' and 'prime' keywords." + + # Make sure prepare/override-pull are mutually exclusive + - not: + type: object + required: [prepare, override-pull] + validation-failure: + "Parts cannot contain both 'prepare' and 'override-*' + keywords. Use 'override-build' instead of 'prepare'." + # Make sure build/override-pull are mutually exclusive + - not: + type: object + required: [build, override-pull] + validation-failure: + "Parts cannot contain both 'build' and 'override-*' + keywords. Use 'override-build' instead of 'build'." + # Make sure install/override-pull are mutually exclusive + - not: + type: object + required: [install, override-pull] + validation-failure: + "Parts cannot contain both 'install' and 'override-*' + keywords. Use 'override-build' instead of 'install'." + + # Make sure prepare/override-build are mutually exclusive + - not: + type: object + required: [prepare, override-build] + validation-failure: + "Parts cannot contain both 'prepare' and 'override-*' + keywords. Use 'override-build' instead of 'prepare'." + # Make sure build/override-build are mutually exclusive + - not: + type: object + required: [build, override-build] + validation-failure: + "Parts cannot contain both 'build' and 'override-*' + keywords. Use 'override-build' instead of 'build'." + # Make sure install/override-build are mutually exclusive + - not: + type: object + required: [install, override-build] + validation-failure: + "Parts cannot contain both 'install' and 'override-*' + keywords. Use 'override-build' instead of 'install'." + + # Make sure prepare/override-stage are mutually exclusive + - not: + type: object + required: [prepare, override-stage] + validation-failure: + "Parts cannot contain both 'prepare' and 'override-*' + keywords. Use 'override-build' instead of 'prepare'." + # Make sure build/override-stage are mutually exclusive + - not: + type: object + required: [build, override-stage] + validation-failure: + "Parts cannot contain both 'build' and 'override-*' + keywords. Use 'override-build' instead of 'build'." + # Make sure install/override-stage are mutually exclusive + - not: + type: object + required: [install, override-stage] + validation-failure: + "Parts cannot contain both 'install' and 'override-*' + keywords. Use 'override-build' instead of 'install'." + + # Make sure prepare/override-prime are mutually exclusive + - not: + type: object + required: [prepare, override-prime] + validation-failure: + "Parts cannot contain both 'prepare' and 'override-*' + keywords. Use 'override-build' instead of 'prepare'." + # Make sure build/override-prime are mutually exclusive + - not: + type: object + required: [build, override-prime] + validation-failure: + "Parts cannot contain both 'build' and 'override-*' + keywords. Use 'override-build' instead of 'build'." + # Make sure install/override-prime are mutually exclusive + - not: + type: object + required: [install, override-prime] + validation-failure: + "Parts cannot contain both 'install' and 'override-*' + keywords. Use 'override-build' instead of 'install'." type: object minProperties: 1 properties: @@ -448,6 +545,18 @@ prepare: type: string default: '' + override-pull: + type: string + default: 'snapcraftctl pull' + override-build: + type: string + default: 'snapcraftctl build' + override-stage: + type: string + default: 'snapcraftctl stage' + override-prime: + type: string + default: 'snapcraftctl prime' parse-info: type: array minitems: 1 @@ -461,15 +570,15 @@ type: object required: - name - - version - parts -# Either summary/description is required, or adopt-info is required to specify -# the part from which this metadata will be retrieved. +# Either summary/description/version is required, or adopt-info is required to +# specify the part from which this metadata will be retrieved. anyOf: - required: - summary - description + - version - required: - adopt-info dependencies: diff -Nru snapcraft-2.40/setup.py snapcraft-2.41/setup.py --- snapcraft-2.40/setup.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/setup.py 2018-04-14 12:13:35.000000000 +0000 @@ -30,6 +30,7 @@ packages = [ 'snapcraft', 'snapcraft.cli', + 'snapcraft.cli.snapcraftctl', 'snapcraft.extractors', 'snapcraft.integrations', 'snapcraft.internal', @@ -45,6 +46,7 @@ 'snapcraft.internal.repo', 'snapcraft.internal.sources', 'snapcraft.internal.states', + 'snapcraft.project', 'snapcraft.plugins', 'snapcraft.plugins._ros', 'snapcraft.plugins._python', @@ -139,6 +141,8 @@ 'snapcraft-parser = snapcraft.internal.parser:main', ], }, + # This is not in console_scripts because we need a clean environment + scripts=['bin/snapcraftctl'], data_files=[ ('share/snapcraft/schema', ['schema/' + x for x in os.listdir('schema')]), diff -Nru snapcraft-2.40/snap/snapcraft.yaml snapcraft-2.41/snap/snapcraft.yaml --- snapcraft-2.40/snap/snapcraft.yaml 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snap/snapcraft.yaml 2018-04-14 12:13:35.000000000 +0000 @@ -20,8 +20,7 @@ source: patches plugin: dump prime: - - -ctypes_init.diff - - -pyyaml-support-high-codepoints.diff + - -*.diff bash-completion: source: debian plugin: dump @@ -36,98 +35,67 @@ - libffi-dev - libsodium-dev - liblzma-dev + - patch stage-packages: + - binutils - execstack + - gpgv - libffi6 - libsodium18 + - patchelf - squashfs-tools - xdelta3 - prime: - - '*' - - '**/*.pyc' install: | TRIPLET_PATH="$SNAPCRAFT_PART_INSTALL/usr/lib/$(gcc -print-multiarch)" LIBSODIUM=$(readlink -n $TRIPLET_PATH/libsodium.so.18) ln -s $LIBSODIUM $TRIPLET_PATH/libsodium.so - patch -d $SNAPCRAFT_PART_INSTALL/lib/python3.6/site-packages -p1 < $SNAPCRAFT_STAGE/pyyaml-support-high-codepoints.diff - after: [python, apt] - patchelf: - source: https://launchpad.net/ubuntu/+archive/primary/+files/patchelf_0.9.orig.tar.gz - source-checksum: sha384/88c97bfc417db32b61991b974055801b904e5a50b362a178a745e92c3d3479463a4470528aee1561139052ee95107ba5 - plugin: autotools - stage: - - bin/patchelf - python: - source: https://www.python.org/ftp/python/3.6.0/Python-3.6.0.tar.xz - plugin: autotools - configflags: [--prefix=/usr] - build-packages: [libssl-dev, patch] - prime: - - -usr/include - install: | - patch $SNAPCRAFT_PART_INSTALL/usr/lib/python3.6/ctypes/__init__.py $SNAPCRAFT_STAGE/ctypes_init.diff - after: [patches] + patch -d $SNAPCRAFT_PART_INSTALL/lib/python3.5/site-packages -p1 < $SNAPCRAFT_STAGE/pyyaml-support-high-codepoints.diff + patch $SNAPCRAFT_PART_INSTALL/usr/lib/python3.5/ctypes/__init__.py $SNAPCRAFT_STAGE/ctypes_init.diff + after: [patches, apt] apt: - source: https://github.com/Debian/apt - source-type: git - source-tag: 1.2.19 - source-depth: 1 - plugin: autotools - prepare: | - make startup - build: | - mkdir apt-build - cd apt-build - ../configure - make - install: | - 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/ - install bin/apt-key $SNAPCRAFT_PART_INSTALL/apt/ - install bin/apt-mark $SNAPCRAFT_PART_INSTALL/apt/ - install bin/apt-internal-solver $SNAPCRAFT_PART_INSTALL/apt/ - install bin/apt-helper $SNAPCRAFT_PART_INSTALL/apt/ - install -d $SNAPCRAFT_PART_INSTALL/usr/lib - install bin/libapt-inst.so.2.0.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ - install bin/libapt-pkg.so $SNAPCRAFT_PART_INSTALL/usr/lib/ - install bin/libapt-pkg-5.0-0.symver $SNAPCRAFT_PART_INSTALL/usr/lib/ - install bin/libapt-private.so $SNAPCRAFT_PART_INSTALL/usr/lib/ - install bin/libapt-private-0.0-0.symver $SNAPCRAFT_PART_INSTALL/usr/lib/ - install bin/libapt-private.so.0.0.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ - install bin/libapt-inst.so.2.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ - install bin/libapt-inst.so $SNAPCRAFT_PART_INSTALL/usr/lib/ - install bin/libapt-pkg.so.5.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ - install bin/libapt-inst-2.0-0.symver $SNAPCRAFT_PART_INSTALL/usr/lib/ - install bin/libapt-pkg.so.5.0.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ - install bin/libapt-private.so.0.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ - install -d $SNAPCRAFT_PART_INSTALL/usr/include - cp -r include/* $SNAPCRAFT_PART_INSTALL/usr/include/ - prime: - - -usr/include - build-packages: - - gettext - - libbz2-dev - - libcurl4-gnutls-dev - - libdb-dev - - liblz4-dev - - liblzma-dev - - zlib1g-dev - after: [python] - gpg: - source: https://gnupg.org/ftp/gcrypt/gnupg/gnupg-1.4.21.tar.bz2 + source: https://github.com/Debian/apt + source-type: git + source-tag: 1.2.19 + source-depth: 1 plugin: autotools - configflags: - - --enable-minimal - - --disable-makeinfo - - --disable-ldap - - --disable-finger - - --disable-nls prepare: | - # This is fragile but we use a fixed tag - sed -i.bak -e 's/\(^ *g10 keyserver\) po doc ${checks}$/\1 ${checks}/' Makefile.am - build-packages: - - texinfo + make startup + build: | + mkdir apt-build + cd apt-build + ../configure + make + install: | + 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/ + install bin/apt-key $SNAPCRAFT_PART_INSTALL/apt/ + install bin/apt-mark $SNAPCRAFT_PART_INSTALL/apt/ + install bin/apt-internal-solver $SNAPCRAFT_PART_INSTALL/apt/ + install bin/apt-helper $SNAPCRAFT_PART_INSTALL/apt/ + install -d $SNAPCRAFT_PART_INSTALL/usr/lib + install bin/libapt-inst.so.2.0.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ + install bin/libapt-pkg.so $SNAPCRAFT_PART_INSTALL/usr/lib/ + install bin/libapt-pkg-5.0-0.symver $SNAPCRAFT_PART_INSTALL/usr/lib/ + install bin/libapt-private.so $SNAPCRAFT_PART_INSTALL/usr/lib/ + install bin/libapt-private-0.0-0.symver $SNAPCRAFT_PART_INSTALL/usr/lib/ + install bin/libapt-private.so.0.0.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ + install bin/libapt-inst.so.2.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ + install bin/libapt-inst.so $SNAPCRAFT_PART_INSTALL/usr/lib/ + install bin/libapt-pkg.so.5.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ + install bin/libapt-inst-2.0-0.symver $SNAPCRAFT_PART_INSTALL/usr/lib/ + install bin/libapt-pkg.so.5.0.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ + install bin/libapt-private.so.0.0 $SNAPCRAFT_PART_INSTALL/usr/lib/ + install -d $SNAPCRAFT_PART_INSTALL/usr/include + cp -r include/* $SNAPCRAFT_PART_INSTALL/usr/include/ prime: - - bin/gpgv + - -usr/include + build-packages: + - gettext + - libbz2-dev + - libcurl4-gnutls-dev + - libdb-dev + - liblz4-dev + - liblzma-dev + - zlib1g-dev diff -Nru snapcraft-2.40/snapcraft/cli/assertions.py snapcraft-2.41/snapcraft/cli/assertions.py --- snapcraft-2.40/snapcraft/cli/assertions.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/cli/assertions.py 2018-04-14 12:13:35.000000000 +0000 @@ -157,7 +157,7 @@ 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') + developers = yaml.safe_load(fr).get('developers') return developers diff -Nru snapcraft-2.40/snapcraft/cli/_errors.py snapcraft-2.41/snapcraft/cli/_errors.py --- snapcraft-2.40/snapcraft/cli/_errors.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/cli/_errors.py 2018-04-14 12:13:35.000000000 +0000 @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright (C) 2017 Canonical Ltd +# Copyright (C) 2017-2018 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,44 +14,116 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import distutils.util +import os import sys import traceback +from textwrap import dedent from . import echo from snapcraft.internal import errors +import click +# raven is not available on 16.04 +try: + from raven import Client as RavenClient + from raven.transport import RequestsHTTPTransport +except ImportError: + RavenClient = None + +# TODO: +# - annotate the part and lifecycle step in the message +# - add link to privacy policy +# - add Always option +_MSG_TRACEBACK = dedent("""\ + Sorry, Snapcraft ran into an error when trying to running through its + lifecycle that generated the following traceback:""") +_MSG_SEND_TO_SENTRY_TRACEBACK_CONFIRM = dedent("""\ + You can anonymously report this issue to the snapcraft developers. + No other data than this traceback and the version of snapcraft in use will + be sent. + Would you like send this error data?""") +_MSG_SEND_TO_SENTRY_ENV = dedent("""\ + Sending error data: SNAPCRAFT_SEND_ERROR_DATA is set to 'y'.""") +_MSG_SEND_TO_SENTRY_THANKS = 'Thank you for sending the report.' + def exception_handler(exception_type, exception, exception_traceback, *, debug=False): """Catch all Snapcraft exceptions unless debugging. This function is the global excepthook, properly handling uncaught - exceptions. "Proper" being defined as: + exceptions and determine if they need to be reported. - When debug=False: - - If exception is a SnapcraftError, just display a nice error and exit - according to the exit code in the exception. - - If exception is NOT a SnapcraftError, show traceback and exit 1 - - When debug=True: - - If exception is a SnapcraftError, show traceback and exit according - to the exit code in the exception. - - If exception is NOT a SnapcraftError, show traceback and exit 1 - """ + These are the rules of engagement: + - a non snapcraft handled error occurs and raven is setup, + so we go over confirmation logic showing the relevant traceback + - a non snapcraft handled error occurs and raven is not setup, + so we just show the traceback + - a snapcraft handled error occurs, debug=True so a traceback + is shown + - a snapcraft handled error occurs, debug=False so only the + exception message is shown + """ exit_code = 1 is_snapcraft_error = issubclass(exception_type, errors.SnapcraftError) + is_raven_setup = RavenClient is not None + is_sentry_enabled = distutils.util.strtobool( + os.getenv('SNAPCRAFT_ENABLE_SENTRY', 'n')) == 1 + is_sentry_flag = distutils.util.strtobool( + os.getenv('SNAPCRAFT_SEND_ERROR_DATA', 'n')) == 1 - if debug or not is_snapcraft_error: + if is_sentry_enabled and not is_snapcraft_error: + click.echo(_MSG_TRACEBACK) traceback.print_exception( exception_type, exception, exception_traceback) - - should_print_error = not debug and ( - exception_type != errors.ContainerSnapcraftCmdError) - - if is_snapcraft_error: + msg = _MSG_SEND_TO_SENTRY_TRACEBACK_CONFIRM + if not is_raven_setup: + echo.warning( + 'raven is not installed on this system, cannot send data ' + 'to sentry') + elif is_sentry_flag or click.confirm(msg): + if is_sentry_flag: + click.echo(_MSG_SEND_TO_SENTRY_ENV) + _submit_trace(exception) + click.echo(_MSG_SEND_TO_SENTRY_THANKS) + elif not is_snapcraft_error: + click.echo(_MSG_TRACEBACK) + traceback.print_exception( + exception_type, exception, exception_traceback) + elif is_snapcraft_error and debug: + exit_code = exception.get_exit_code() + traceback.print_exception( + exception_type, exception, exception_traceback) + elif is_snapcraft_error and not debug: exit_code = exception.get_exit_code() - if should_print_error: + # if the error comes from running snapcraft in the container, it + # has already been displayed so we should avoid that situation + # of a double error print + if exception_type != errors.ContainerSnapcraftCmdError: echo.error(str(exception)) + else: + click.echo('Unhandled error case') + exit_code = -1 sys.exit(exit_code) + + +def _submit_trace(exception): + client = RavenClient( + 'https://b0fef3e0ced2443c92143ae0d038b0a4:' + 'b7c67d7fa4ee46caae12b29a80594c54@sentry.io/277754', + transport=RequestsHTTPTransport, + # Should Raven automatically log frame stacks (including locals) + # for all calls as it would for exceptions. + auto_log_stacks=False, + # Removes all stacktrace context variables. This will cripple the + # functionality of Sentry, as you’ll only get raw tracebacks, + # but it will ensure no local scoped information is available to the + # server. + processors=('raven.processors.RemoveStackLocalsProcessor',)) + try: + raise exception + except Exception: + client.captureException() diff -Nru snapcraft-2.40/snapcraft/cli/__main__.py snapcraft-2.41/snapcraft/cli/__main__.py --- snapcraft-2.40/snapcraft/cli/__main__.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/cli/__main__.py 2018-04-14 12:13:35.000000000 +0000 @@ -21,6 +21,7 @@ import subprocess from ._runner import run +from .snapcraftctl._runner import run as run_snapcraftctl # noqa from .echo import warning # If the locale ends up being ascii, Click will barf. Let's try to prevent that @@ -40,4 +41,5 @@ os.environ['LANG'] = 'C.UTF-8' break -run(prog_name='snapcraft') +if __name__ == '__main__': + run(prog_name='snapcraft') diff -Nru snapcraft-2.40/snapcraft/cli/_options.py snapcraft-2.41/snapcraft/cli/_options.py --- snapcraft-2.40/snapcraft/cli/_options.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/cli/_options.py 2018-04-14 12:13:35.000000000 +0000 @@ -15,7 +15,7 @@ # along with this program. If not, see . import click -from snapcraft import ProjectOptions +from snapcraft.project import Project class HiddenOption(click.Option): @@ -58,11 +58,9 @@ if not kwargs.get(key): kwargs[key] = value - project_args = dict( + project = Project( debug=kwargs.pop('debug'), use_geoip=kwargs.pop('enable_geoip'), parallel_builds=not kwargs.pop('no_parallel_builds'), - target_deb_arch=kwargs.pop('target_arch'), - ) - - return ProjectOptions(**project_args) + target_deb_arch=kwargs.pop('target_arch')) + return project diff -Nru snapcraft-2.40/snapcraft/cli/_runner.py snapcraft-2.41/snapcraft/cli/_runner.py --- snapcraft-2.40/snapcraft/cli/_runner.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/cli/_runner.py 2018-04-14 12:13:35.000000000 +0000 @@ -54,12 +54,16 @@ version=snapcraft.__version__) @click.pass_context @add_build_options(hidden=True) -@click.option('--debug', '-d', is_flag=True) +@click.option('--debug', '-d', is_flag=True, envvar='SNAPCRAFT_DEBUG') def run(ctx, debug, catch_exceptions=False, **kwargs): """Snapcraft is a delightful packaging tool.""" if debug: log_level = logging.DEBUG + + # Setting this here so that tools run within this are also in debug + # mode (e.g. snapcraftctl) + os.environ['SNAPCRAFT_DEBUG'] = 'true' click.echo('Starting snapcraft {} from {}.'.format( snapcraft.__version__, os.path.dirname(__file__))) else: diff -Nru snapcraft-2.40/snapcraft/cli/snapcraftctl/_runner.py snapcraft-2.41/snapcraft/cli/snapcraftctl/_runner.py --- snapcraft-2.40/snapcraft/cli/snapcraftctl/_runner.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/snapcraft/cli/snapcraftctl/_runner.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,116 @@ +# -*- 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 functools +import json +import logging +import os +import sys + +import click + +from snapcraft.internal import errors +from snapcraft.cli._errors import exception_handler +from snapcraft.internal import log + + +@click.group() +@click.option('--debug', '-d', is_flag=True, envvar='SNAPCRAFT_DEBUG') +def run(debug): + """snapcraftctl is how snapcraft.yaml can communicate with snapcraft""" + + if debug: + log_level = logging.DEBUG + else: + log_level = logging.INFO + + # Setup global exception handler (to be called for unhandled exceptions) + sys.excepthook = functools.partial(exception_handler, debug=debug) + + # In an ideal world, this logger setup would be replaced + log.configure(log_level=log_level) + + +@run.command() +def pull(): + """Run the 'pull' step of the calling part's lifecycle""" + _call_function('pull') + + +@run.command() +def build(): + """Run the 'build' step of the calling part's lifecycle""" + _call_function('build') + + +@run.command() +def stage(): + """Run the 'stage' step of the calling part's lifecycle""" + _call_function('stage') + + +@run.command() +def prime(): + """Run the 'prime' step of the calling part's lifecycle""" + _call_function('prime') + + +@run.command('set-version') +@click.argument('version') +def set_version(version): + """Set the version of the snap""" + _call_function('set-version', {'version': version}) + + +@run.command('set-grade') +@click.argument('grade') +def set_grade(grade): + """Set the grade of the snap""" + _call_function('set-grade', {'grade': grade}) + + +def _call_function(function_name, args=None): + if not args: + args = {} + + data = { + 'function': function_name, + 'args': args, + } + + # We could load the FIFOs in `run` and shove them in the context, but + # that's too early to error out if these variables aren't defined. Doing it + # here allows one to run e.g. `snapcraftctl build --help` without needing + # these variables defined, which is a win for usability. + try: + call_fifo = os.environ['SNAPCRAFTCTL_CALL_FIFO'] + feedback_fifo = os.environ['SNAPCRAFTCTL_FEEDBACK_FIFO'] + except KeyError as e: + raise errors.SnapcraftEnvironmentError( + "{!s} environment variable must be defined. Note that this " + "utility is only designed for use within a snapcraft.yaml".format( + e)) from e + + with open(call_fifo, 'w') as f: + f.write(json.dumps(data)) + f.flush() + + with open(feedback_fifo, 'r') as f: + feedback = f.readline().strip() + + # Any feedback is considered a fatal error to be printed + if feedback: + raise errors.SnapcraftctlError(feedback) diff -Nru snapcraft-2.40/snapcraft/cli/store.py snapcraft-2.41/snapcraft/cli/store.py --- snapcraft-2.40/snapcraft/cli/store.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/cli/store.py 2018-04-14 12:13:35.000000000 +0000 @@ -298,7 +298,10 @@ """Save login configuration for a store account in FILE. This file can then be used to log in to the given account with the - specified permissions. + specified permissions. One can also request the login to be exported to + stdout instead of a file: + + snapcraft export-login - For example, to limit access to the edge channel of any snap the account can access: @@ -338,22 +341,37 @@ save=False): sys.exit(1) - # This is sensitive-- it should only be accessible by the owner - private_open = functools.partial(os.open, mode=0o600) - - # mypy doesn't have the opener arg in its stub. Ignore its warning - with open(login_file, 'w', opener=private_open) as f: # type: ignore - store.conf.save(config_fd=f) + # Support a login_file of '-', which indicates a desire to print to stdout + if login_file.strip() == '-': + echo.info("\nExported login starts on next line:") + store.conf.save(config_fd=sys.stdout, encode=True) + print() + + preamble = 'Login successfully exported and printed above' + login_action = 'echo "" | snapcraft login --with -' + else: + # This is sensitive-- it should only be accessible by the owner + private_open = functools.partial(os.open, mode=0o600) + + # mypy doesn't have the opener arg in its stub. Ignore its warning + with open(login_file, 'w', opener=private_open) as f: # type: ignore + store.conf.save(config_fd=f) + + # Now that the file has been written, we can just make it + # owner-readable + os.chmod(login_file, stat.S_IRUSR) - # Now that the file has been written, we can just make it owner-readable - os.chmod(login_file, stat.S_IRUSR) + preamble = 'Login successfully exported to {0!r}'.format(login_file) + login_action = 'snapcraft login --with {0}'.format(login_file) print() echo.info(dedent("""\ - Login successfully exported to {0!r}. This file can now be used with - 'snapcraft login --with {0}' to log in to this account with no password - and have these capabilities:\n""".format( - login_file))) + {}. This can now be used with + + {} + + to log in to this account with no password and have these + capabilities:\n""".format(preamble, login_action))) echo.info(_human_readable_acls(store)) echo.warning( 'This exported login is not encrypted. Do not commit it to version ' diff -Nru snapcraft-2.40/snapcraft/config.py snapcraft-2.41/snapcraft/config.py --- snapcraft-2.40/snapcraft/config.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/config.py 2018-04-14 12:13:35.000000000 +0000 @@ -14,15 +14,21 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import base64 import configparser +import io import logging import os +import sys import urllib.parse from typing import TextIO from xdg import BaseDirectory -from snapcraft.storeapi import constants +from snapcraft.storeapi import ( + constants, + errors, +) LOCAL_CONFIG_FILENAME = '.snapcraft/snapcraft.cfg' @@ -71,8 +77,9 @@ return True def load(self, *, config_fd: TextIO = None) -> None: + config = '' if config_fd: - self.parser.read_file(config_fd) + config = config_fd.read() else: # Local configurations (per project) are supposed to be static. # That's why it's only checked for 'loading' and never written to. @@ -92,19 +99,50 @@ file_path = BaseDirectory.load_first_config( 'snapcraft', 'snapcraft.cfg') if file_path and os.path.exists(file_path): - self.parser.read(file_path) + with open(file_path, 'r') as f: + config = f.read() + + if config: + _load_potentially_base64_config(self.parser, config) @staticmethod def save_path() -> str: return os.path.join(BaseDirectory.save_config_path('snapcraft'), 'snapcraft.cfg') - def save(self, *, config_fd: TextIO = None) -> None: - if config_fd: - self.parser.write(config_fd) - else: - with open(self.save_path(), 'w') as f: - self.parser.write(f) + def save(self, *, config_fd: TextIO=None, encode: bool=False) -> None: + with io.StringIO() as config_buffer: + self.parser.write(config_buffer) + config = config_buffer.getvalue() + if encode: + # Encode config using base64 + config = base64.b64encode( + config.encode(sys.getfilesystemencoding())).decode( + sys.getfilesystemencoding()) + + if config_fd: + config_fd.write(config) + else: + with open(self.save_path(), 'w') as f: + f.write(config) def clear(self) -> None: self.parser.remove_section(self._section_name()) + + +def _load_potentially_base64_config(parser, config): + try: + parser.read_string(config) + except configparser.Error as e: + # The config may be base64-encoded, try decoding it + try: + config = base64.b64decode(config).decode( + sys.getfilesystemencoding()) + except base64.binascii.Error: # type: ignore + # It wasn't base64, so use the original error + raise errors.InvalidLoginConfig(e) from e + + try: + parser.read_string(config) + except configparser.Error as e: + raise errors.InvalidLoginConfig(e) from e diff -Nru snapcraft-2.40/snapcraft/extractors/_errors.py snapcraft-2.41/snapcraft/extractors/_errors.py --- snapcraft-2.40/snapcraft/extractors/_errors.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/extractors/_errors.py 2018-04-14 12:13:35.000000000 +0000 @@ -36,3 +36,26 @@ def __init__(self, path: str) -> None: super().__init__(path=path) + + +class SetupPyFileParseError(MetadataExtractionError): + + fmt = ( + "Failed to extract metadata from {path!r}: " + "the logic in setup.py is currently not handled." + ) + + def __init__(self, path: str) -> None: + super().__init__(path=path) + + +class SetupPyImportError(MetadataExtractionError): + + fmt = ( + "Failed to extract metadata from {path!r}: " + "some packages or modules used could not be imported: " + "{error}" + ) + + def __init__(self, path: str, error: str) -> None: + super().__init__(path=path, error=error) diff -Nru snapcraft-2.40/snapcraft/extractors/_metadata.py snapcraft-2.41/snapcraft/extractors/_metadata.py --- snapcraft-2.40/snapcraft/extractors/_metadata.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/extractors/_metadata.py 2018-04-14 12:13:35.000000000 +0000 @@ -15,7 +15,7 @@ # along with this program. If not, see . import yaml -from typing import Any, Dict, List, Union +from typing import Any, Dict, List, Set, Union class ExtractedMetadata(yaml.YAMLObject): @@ -24,8 +24,8 @@ yaml_tag = u'!ExtractedMetadata' def __init__( - self, *, common_id: str='', summary: str='', - description: str='', icon: str='', + self, *, common_id: str='', summary: str='', description: str='', + version: str='', grade: str='', icon: str='', desktop_file_paths: List[str]=None) -> None: """Create a new ExtractedMetadata instance. @@ -33,6 +33,8 @@ formats :param str summary: Extracted summary :param str description: Extracted description + :param str version: Extracted version + :param str grade: Extracted grade :param str icon: Extracted icon :param list desktop_file_paths: Extracted desktop file paths """ # noqa @@ -45,6 +47,10 @@ self._data['summary'] = summary if description: self._data['description'] = description + if version: + self._data['version'] = version + if grade: + self._data['grade'] = grade if icon: self._data['icon'] = icon if desktop_file_paths: @@ -87,6 +93,24 @@ description = self._data.get('description') return str(description) if description else None + def get_version(self) -> str: + """Return extracted version. + + :returns: Extracted version + :rtype: str + """ + version = self._data.get('version') + return str(version) if version else None + + def get_grade(self) -> str: + """Return extracted grade. + + :returns: Extracted grade + :rtype: str + """ + grade = self._data.get('grade') + return str(grade) if grade else None + def get_icon(self) -> str: """Return extracted icon. @@ -113,8 +137,22 @@ """ return self._data.copy() + def overlap(self, other: 'ExtractedMetadata') -> Set[str]: + """Return all overlapping keys between this and other. + + :returns: All overlapping keys between this and other + :rtype: set + """ + return set(self._data.keys() & other.to_dict().keys()) + + def __str__(self) -> str: + return str(self._data) + def __eq__(self, other: Any) -> bool: if type(other) is type(self): return self._data == other._data return False + + def __len__(self) -> int: + return self._data.__len__() diff -Nru snapcraft-2.40/snapcraft/extractors/setuppy.py snapcraft-2.41/snapcraft/extractors/setuppy.py --- snapcraft-2.40/snapcraft/extractors/setuppy.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/snapcraft/extractors/setuppy.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,60 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 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 importlib.util +import logging +from typing import Dict # noqa: F401 +from unittest.mock import patch + +from ._metadata import ExtractedMetadata +from snapcraft.extractors import _errors + + +logger = logging.getLogger(__name__) + + +def extract(path: str) -> ExtractedMetadata: + if os.path.basename(path) != 'setup.py': + raise _errors.UnhandledFileError(path, 'setup.py') + + spec = importlib.util.spec_from_file_location('setuppy', path) + setuppy = importlib.util.module_from_spec(spec) + + params = dict() # type: Dict[str, str] + + def _fake_setup(*args, **kwargs): + nonlocal params + params = kwargs + + with patch('setuptools.setup') as setuptools_mock: + with patch('distutils.core.setup') as distutils_mock: + setuptools_mock.side_effect = _fake_setup + distutils_mock.side_effect = _fake_setup + # This would really fail during the use of the plugin + # but let's be cautios and add the proper guards. + try: + spec.loader.exec_module(setuppy) + except SystemExit as e: + raise _errors.SetupPyFileParseError(path=path) + except ImportError as e: + raise _errors.SetupPyImportError( + path=path, error=str(e)) from e + + version = params.get('version') + description = params.get('description') + + return ExtractedMetadata(version=version, description=description) diff -Nru snapcraft-2.40/snapcraft/file_utils.py snapcraft-2.41/snapcraft/file_utils.py --- snapcraft-2.40/snapcraft/file_utils.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/file_utils.py 2018-04-14 12:13:35.000000000 +0000 @@ -15,6 +15,7 @@ # along with this program. If not, see . from contextlib import contextmanager, suppress +import errno import hashlib import logging import re @@ -22,7 +23,8 @@ import shutil import subprocess import sys -from typing import Pattern, Callable, Generator +from typing import Pattern, Callable, Generator, List +from typing import Set # noqa F401 from snapcraft.internal import common from snapcraft.internal.errors import ( @@ -117,31 +119,41 @@ # upstream-- we want this function to continue supporting NOT following # symlinks. os.link(source_path, destination, follow_symlinks=False) - except OSError: - # If os.link raised an I/O error, it may have left a file behind. - # Skip on OSError in case it doesn't exist or is a directory. - with suppress(OSError): - os.unlink(destination) - - shutil.copy2(source, destination, follow_symlinks=follow_symlinks) - uid = os.stat(source, follow_symlinks=follow_symlinks).st_uid - gid = os.stat(source, follow_symlinks=follow_symlinks).st_gid - try: - os.chown(destination, uid, gid, follow_symlinks=follow_symlinks) - except PermissionError as e: - logger.debug('Unable to chown {destination}: {error}'.format( - destination=destination, error=e)) + except OSError as e: + if e.errno == errno.EEXIST and not os.path.isdir(destination): + # os.link will fail if the destination already exists, so let's + # remove it and try again. + os.remove(destination) + link_or_copy(source_path, destination, follow_symlinks) + else: + # If os.link raised an I/O error, it may have left a file behind. + # Skip on OSError in case it doesn't exist or is a directory. + with suppress(OSError): + os.unlink(destination) + + shutil.copy2(source, destination, follow_symlinks=follow_symlinks) + uid = os.stat(source, follow_symlinks=follow_symlinks).st_uid + gid = os.stat(source, follow_symlinks=follow_symlinks).st_gid + try: + os.chown(destination, uid, gid, + follow_symlinks=follow_symlinks) + except PermissionError as e: + logger.debug('Unable to chown {destination}: {error}'.format( + destination=destination, error=e)) def link_or_copy_tree(source_tree: str, destination_tree: str, - copy_function: Callable[..., None] - =link_or_copy) -> None: + ignore: Callable[[str, List[str]], List[str]]=None, + copy_function: Callable[..., None]=link_or_copy) -> None: """Copy a source tree into a destination, hard-linking if possible. :param str source_tree: Source directory to be copied. :param str destination_tree: Destination directory. If this directory already exists, the files in `source_tree` will take precedence. + :param callable ignore: If given, called with two params, source dir and + dir contents, for every dir copied. Should return + list of contents to NOT copy. :param callable copy_function: Callable that actually copies. """ @@ -149,14 +161,31 @@ raise NotADirectoryError('{!r} is not a directory'.format(source_tree)) if (not os.path.isdir(destination_tree) and - os.path.exists(destination_tree)): + (os.path.exists(destination_tree) or + os.path.islink(destination_tree))): raise NotADirectoryError( 'Cannot overwrite non-directory {!r} with directory ' '{!r}'.format(destination_tree, source_tree)) create_similar_directory(source_tree, destination_tree) - for root, directories, files in os.walk(source_tree): + destination_basename = os.path.basename(destination_tree) + + for root, directories, files in os.walk(source_tree, topdown=True): + ignored = set() # type: Set[str] + if ignore is not None: + ignored = set(ignore(root, directories + files)) + + # Don't recurse into destination tree if it's a subdirectory of the + # source tree. + if os.path.relpath(destination_tree, root) == destination_basename: + ignored.add(destination_basename) + + if ignored: + # Prune our search appropriately given an ignore list, i.e. don't + # walk into directories that are ignored. + directories[:] = [d for d in directories if d not in ignored] + for directory in directories: source = os.path.join(root, directory) # os.walk doesn't by default follow symlinks (which is good), but @@ -171,7 +200,7 @@ create_similar_directory(source, destination) - for file_name in files: + for file_name in (set(files) - ignored): source = os.path.join(root, file_name) destination = os.path.join( destination_tree, os.path.relpath(source, source_tree)) @@ -295,6 +324,8 @@ if os.path.exists(path): return path + return '' + def get_linker_version_from_file(linker_file: str) -> str: """Returns the version of the linker from linker_file. diff -Nru snapcraft-2.40/snapcraft/__init__.py snapcraft-2.41/snapcraft/__init__.py --- snapcraft-2.40/snapcraft/__init__.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/__init__.py 2018-04-14 12:13:35.000000000 +0000 @@ -382,7 +382,6 @@ from snapcraft._baseplugin import BasePlugin # noqa -from snapcraft._options import ProjectOptions # noqa # FIXME LP: #1662658 from snapcraft._store import ( # noqa create_key, @@ -409,6 +408,9 @@ from snapcraft import file_utils # noqa from snapcraft import shell_utils # noqa from snapcraft.internal import repo # noqa +from snapcraft.project._project_options import ( # noqa + ProjectOptions +) # Setup yaml module globally @@ -435,5 +437,8 @@ yaml.add_representer(str, str_presenter) +yaml.SafeDumper.add_representer(str, str_presenter) yaml.add_representer(OrderedDict, dict_representer) +yaml.SafeDumper.add_representer(OrderedDict, dict_representer) yaml.add_constructor(_mapping_tag, dict_constructor) +yaml.SafeLoader.add_constructor(_mapping_tag, dict_constructor) diff -Nru snapcraft-2.40/snapcraft/integrations/travis.py snapcraft-2.41/snapcraft/integrations/travis.py --- snapcraft-2.40/snapcraft/integrations/travis.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/integrations/travis.py 2018-04-14 12:13:35.000000000 +0000 @@ -216,7 +216,7 @@ 'Configuring "deploy" phase to build and release the snap in the ' 'Store.') with open(TRAVIS_CONFIG_FILENAME, 'r+') as fd: - travis_conf = yaml.load(fd) + travis_conf = yaml.safe_load(fd) # Enable 'sudo' capability and 'docker' service. travis_conf['sudo'] = 'required' services = travis_conf.setdefault('services', []) diff -Nru snapcraft-2.40/snapcraft/internal/cache/_snap.py snapcraft-2.41/snapcraft/internal/cache/_snap.py --- snapcraft-2.40/snapcraft/internal/cache/_snap.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/cache/_snap.py 2018-04-14 12:13:35.000000000 +0000 @@ -50,7 +50,7 @@ with open(os.path.join( temp_dir, 'squashfs-root', 'meta', 'snap.yaml') ) as yaml_file: - snap_yaml = yaml.load(yaml_file) + snap_yaml = yaml.safe_load(yaml_file) # XXX: add multiarch support later try: return snap_yaml['architectures'][0] diff -Nru snapcraft-2.40/snapcraft/internal/deprecations.py snapcraft-2.41/snapcraft/internal/deprecations.py --- snapcraft-2.40/snapcraft/internal/deprecations.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/deprecations.py 2018-04-14 12:13:35.000000000 +0000 @@ -33,6 +33,9 @@ "in the snap.", 'dn6': "Use of the 'snap' command with a directory has been deprecated " "in favour of the 'pack' command.", + 'dn7': "The 'prepare' keyword has been replaced by 'override-build'", + 'dn8': "The 'build' keyword has been replaced by 'override-build'", + 'dn9': "The 'install' keyword has been replaced by 'override-build'", } _DEPRECATION_URL_FMT = 'http://snapcraft.io/docs/deprecation-notices/{id}' diff -Nru snapcraft-2.40/snapcraft/internal/elf.py snapcraft-2.41/snapcraft/internal/elf.py --- snapcraft-2.40/snapcraft/internal/elf.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/elf.py 2018-04-14 12:13:35.000000000 +0000 @@ -21,12 +21,12 @@ import shutil import subprocess import tempfile -from functools import wraps from typing import Dict, FrozenSet, List, Set, Sequence, Tuple, Union # noqa import elftools.elf.elffile from pkg_resources import parse_version +from snapcraft import file_utils from snapcraft.internal import ( common, errors, @@ -345,35 +345,6 @@ return library_paths -def _retry_patch(f): - @wraps(f) - def wrapper(*args, **kwargs): - try: - return f(*args, **kwargs) - except errors.PatcherError as patch_error: - # This is needed for patchelf to properly work with - # go binaries (LP: #1736861). - # We do this here instead of the go plugin for two reasons, the - # first being that we do not want to blindly remove the section, - # only doing it when necessary, and the second, this logic - # should eventually be removed once patchelf catches up. - try: - elf_file_path = kwargs['elf_file_path'] - logger.warning( - 'Failed to update {!r}. Retrying after stripping ' - 'the .note.go.buildid from the elf file.'.format( - elf_file_path)) - subprocess.check_call([ - 'strip', '--remove-section', '.note.go.buildid', - elf_file_path]) - except subprocess.CalledProcessError: - logger.warning('Could not properly strip .note.go.buildid ' - 'from {!r}.'.format(elf_file_path)) - raise patch_error - return f(*args, **kwargs) - return wrapper - - class Patcher: """Patcher holds the necessary logic to patch elf files.""" @@ -391,30 +362,12 @@ self._dynamic_linker = dynamic_linker self._root_path = root_path - # We will first fallback to the preferred_patchelf_path, - # if that is not found we will look for the snap and finally, - # if we are running from the snap we want to use the patchelf - # bundled there as it would have the capability of working - # anywhere given the fixed ld it would have. - # If not found, resort to whatever is on the system brought - # in by packaging dependencies. - # The docker conditional will work if the docker image has the - # snaps unpacked in the corresponding locations. if preferred_patchelf_path: self._patchelf_cmd = preferred_patchelf_path - # We use the full path here as the path may not be set on - # build systems where the path is recently created and added - # to the environment - elif os.path.exists('/snap/bin/patchelf'): - self._patchelf_cmd = '/snap/bin/patchelf' - elif common.is_snap(): - snap_dir = os.getenv('SNAP') - self._patchelf_cmd = os.path.join(snap_dir, 'bin', 'patchelf') - elif (common.is_docker_instance() and - os.path.exists('/snap/snapcraft/current/bin/patchelf')): - self._patchelf_cmd = '/snap/snapcraft/current/bin/patchelf' else: - self._patchelf_cmd = 'patchelf' + self._patchelf_cmd = file_utils.get_tool_path('patchelf') + + self._strip_cmd = file_utils.get_tool_path('strip') def patch(self, *, elf_file: ElfFile) -> None: """Patch elf_file with the Patcher instance configuration. @@ -447,10 +400,35 @@ self._run_patchelf(patchelf_args=patchelf_args, elf_file_path=elf_file.path) - @_retry_patch def _run_patchelf(self, *, patchelf_args: List[str], elf_file_path: str) -> None: + try: + return self._do_run_patchelf( + patchelf_args=patchelf_args, elf_file_path=elf_file_path) + except errors.PatcherError as patch_error: + # This is needed for patchelf to properly work with + # go binaries (LP: #1736861). + # We do this here instead of the go plugin for two reasons, the + # first being that we do not want to blindly remove the section, + # only doing it when necessary, and the second, this logic + # should eventually be removed once patchelf catches up. + try: + logger.warning( + 'Failed to update {!r}. Retrying after stripping ' + 'the .note.go.buildid from the elf file.'.format( + elf_file_path)) + subprocess.check_call([ + self._strip_cmd, '--remove-section', '.note.go.buildid', + elf_file_path]) + except subprocess.CalledProcessError: + logger.warning('Could not properly strip .note.go.buildid ' + 'from {!r}.'.format(elf_file_path)) + raise patch_error + return self._do_run_patchelf( + patchelf_args=patchelf_args, elf_file_path=elf_file_path) + def _do_run_patchelf(self, *, patchelf_args: List[str], + elf_file_path: str) -> None: # Run patchelf on a copy of the primed file and replace it # after it is successful. This allows us to break the potential # hard link created when migrating the file across the steps of diff -Nru snapcraft-2.40/snapcraft/internal/errors.py snapcraft-2.41/snapcraft/internal/errors.py --- snapcraft-2.40/snapcraft/internal/errors.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/errors.py 2018-04-14 12:13:35.000000000 +0000 @@ -438,9 +438,12 @@ class InvalidContainerImageInfoError(SnapcraftError): - fmt = 'Error parsing the container image info: {image_info}' + fmt = ( + 'Failed to parse container image info: ' + 'SNAPCRAFT_IMAGE_INFO is not a valid JSON string: {image_info}' + ) - def __init__(self, image_info): + def __init__(self, image_info: str) -> None: super().__init__(image_info=image_info) @@ -549,3 +552,52 @@ command = ' '.join(command) super().__init__(command=command, part_name=part_name, exit_code=exit_code) + + +class ScriptletBaseError(SnapcraftError): + """Base class for all scriptlet-related exceptions. + + :cvar fmt: A format string that daughter classes override + + """ + + +class ScriptletRunError(ScriptletBaseError): + fmt = ( + 'Failed to run {scriptlet_name!r}: ' + 'Exit code was {code}.' + ) + + def __init__(self, scriptlet_name: str, code: int) -> None: + super().__init__(scriptlet_name=scriptlet_name, code=code) + + +class ScriptletDuplicateDataError(ScriptletBaseError): + fmt = ( + 'Failed to save data from scriptlet into {step!r} state: ' + 'The {humanized_keys} key(s) were already saved in the {other_step!r} ' + 'step.' + ) + + def __init__(self, step: str, other_step: str, keys: List[str]) -> None: + self.keys = keys + super().__init__( + step=step, other_step=other_step, + humanized_keys=formatting_utils.humanize_list(keys, 'and')) + + +class ScriptletDuplicateFieldError(ScriptletBaseError): + fmt = ( + 'Unable to set {field}: ' + 'it was already set in the {step!r} step.' + ) + + def __init__(self, field: str, step: str) -> None: + super().__init__(field=field, step=step) + + +class SnapcraftctlError(ScriptletBaseError): + fmt = '{message}' + + def __init__(self, message: str) -> None: + super().__init__(message=message) diff -Nru snapcraft-2.40/snapcraft/internal/lifecycle/_packer.py snapcraft-2.41/snapcraft/internal/lifecycle/_packer.py --- snapcraft-2.40/snapcraft/internal/lifecycle/_packer.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/lifecycle/_packer.py 2018-04-14 12:13:35.000000000 +0000 @@ -32,7 +32,7 @@ def _snap_data_from_dir(directory): with open(os.path.join(directory, 'meta', 'snap.yaml')) as f: - snap = yaml.load(f) + snap = yaml.safe_load(f) return {'name': snap['name'], 'version': snap['version'], diff -Nru snapcraft-2.40/snapcraft/internal/lifecycle/_runner.py snapcraft-2.41/snapcraft/internal/lifecycle/_runner.py --- snapcraft-2.40/snapcraft/internal/lifecycle/_runner.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/lifecycle/_runner.py 2018-04-14 12:13:35.000000000 +0000 @@ -73,14 +73,14 @@ state_file.write(yaml.dump( states.GlobalState(installed_packages, installed_snaps))) - if _should_get_core(config.data['confinement']): + if _should_get_core(config.data.get('confinement')): _setup_core(project_options.deb_arch, config.data.get('base', 'core')) _Executor(config, project_options).run(step, part_names) return {'name': config.data['name'], - 'version': config.data['version'], + 'version': config.data.get('version'), 'arch': config.data['architectures'], 'type': config.data.get('type', '')} @@ -240,7 +240,9 @@ common.env = self.config.snap_env() meta.create_snap_packaging( self.config.data, self.config.parts, self.project_options, - self.config.snapcraft_yaml_path) + self.config.snapcraft_yaml_path, + self.config.original_snapcraft_yaml, + self.config.validator.schema) def _handle_dirty(self, part, step, dirty_report): if step not in constants.STEPS_TO_AUTOMATICALLY_CLEAN_IF_DIRTY: diff -Nru snapcraft-2.40/snapcraft/internal/lxd/_containerbuild.py snapcraft-2.41/snapcraft/internal/lxd/_containerbuild.py --- snapcraft-2.40/snapcraft/internal/lxd/_containerbuild.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/lxd/_containerbuild.py 2018-04-14 12:13:35.000000000 +0000 @@ -29,6 +29,7 @@ import subprocess import time from urllib import parse +from textwrap import dedent from typing import List from snapcraft.internal import common @@ -37,16 +38,26 @@ ContainerError, ContainerRunError, ContainerSnapcraftCmdError, + InvalidContainerImageInfoError, SnapdError, ) -from snapcraft._options import _get_deb_arch +from snapcraft.project._project_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') +_NETWORK_PROBE_COMMAND = dedent(''' + import urllib.request + import sys + + check_url = "http://start.ubuntu.com/connectivity-check.html" + try: + urllib.request.urlopen(check_url, timeout=5) + except urllib.error.URLError as e: + sys.exit('Failed to open {!r}: {!s}'.format(check_url, e.reason)) + except Exception as e: + sys.exit('Failed to open {!r}: {!s}'.format(check_url, e)) + ''') _PROXY_KEYS = ['http_proxy', 'https_proxy', 'no_proxy', 'ftp_proxy'] # Canonical store account key _STORE_KEY = ( @@ -138,8 +149,12 @@ 'Failed to get container image info: {}\n' 'It will not be recorded in manifest.') try: + # This command takes the same image name as used to create a new + # container. But we must always use the form distro:series/arch + # here so that we get only the image we're actually using! image_info_command = [ - 'lxc', 'image', 'list', '--format=json', self._image] + 'lxc', 'image', 'list', '--format=json', + '{}/{}'.format(self._image, self._get_container_arch())] image_info = json.loads(subprocess.check_output( image_info_command).decode()) except subprocess.CalledProcessError as e: @@ -157,6 +172,15 @@ for field in ('fingerprint', 'architecture', 'created_at'): if field in image_info[0]: edited_image_info[field] = image_info[0][field] + + # Pick up existing image info if set + image_info_str = os.environ.get('SNAPCRAFT_IMAGE_INFO') + if image_info_str: + try: + edited_image_info.update(json.loads(image_info_str)) + except json.decoder.JSONDecodeError as e: + raise InvalidContainerImageInfoError(image_info_str) from e + # Pass the image info to the container so it can be used when recording # information about the build environment. subprocess.check_call([ @@ -220,7 +244,9 @@ except ContainerRunError as e: retry_count -= 1 if retry_count == 0: - raise e + raise ContainerConnectionError( + 'No network connection in the container.\n' + 'If using a proxy, check its configuration.') logger.info('Network connection established') def _inject_snapcraft(self, *, new_container: bool): diff -Nru snapcraft-2.40/snapcraft/internal/meta/_errors.py snapcraft-2.41/snapcraft/internal/meta/_errors.py --- snapcraft-2.40/snapcraft/internal/meta/_errors.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/meta/_errors.py 2018-04-14 12:13:35.000000000 +0000 @@ -16,6 +16,7 @@ from snapcraft import formatting_utils from snapcraft.internal import errors +from typing import List class CommandError(errors.SnapcraftError): @@ -60,3 +61,16 @@ def __init__(self, part: str) -> None: super().__init__(part=part) + + +class AmbiguousPassthroughKeyError(SnapMetaGenerationError): + + fmt = ( + "Failed to generate snap metadata: " + "The following keys are specified in their regular location " + "as well as in passthrough: {keys}. " + "Remove duplicate keys." + ) + + def __init__(self, keys: List[str]) -> None: + super().__init__(keys=formatting_utils.humanize_list(keys, 'and')) diff -Nru snapcraft-2.40/snapcraft/internal/meta/_snap_packaging.py snapcraft-2.41/snapcraft/internal/meta/_snap_packaging.py --- snapcraft-2.40/snapcraft/internal/meta/_snap_packaging.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/meta/_snap_packaging.py 2018-04-14 12:13:35.000000000 +0000 @@ -25,18 +25,18 @@ import shutil import stat import subprocess -from typing import Any, Dict, List # noqa +from typing import Any, Dict, List, Set # noqa import yaml -from snapcraft import file_utils +from snapcraft import file_utils, formatting_utils from snapcraft import shell_utils +from snapcraft.project import Project from snapcraft.internal import ( common, errors, project_loader, ) -from snapcraft import _options from snapcraft.extractors import _metadata from snapcraft.internal.deprecations import handle_deprecation_notice from snapcraft.internal.meta import ( @@ -86,8 +86,10 @@ def create_snap_packaging( config_data: Dict[str, Any], parts_config: project_loader.PartsConfig, - project_options: _options.ProjectOptions, - snapcraft_yaml_path: str) -> str: + project_options: Project, + snapcraft_yaml_path: str, + original_snapcraft_yaml: Dict[str, Any], + snapcraft_schema: Dict[str, Any]) -> str: """Create snap.yaml and related assets in meta. Create the meta directory and provision it with snap.yaml in the snap dir @@ -101,8 +103,21 @@ # Update config_data using metadata extracted from the project _update_yaml_with_extracted_metadata(config_data, parts_config) + # Now that we've updated config_data with random stuff extracted from + # parts, re-validate it to ensure the it still conforms with the schema. + validator = project_loader.Validator(config_data) + validator.validate(source='properties') + + # Update default values + _update_yaml_with_defaults(config_data, snapcraft_schema) + + # Ensure the YAML contains all required keywords before continuing to + # use it to generate the snap.yaml. + _ensure_required_keywords(config_data) + packaging = _SnapPackaging( - config_data, project_options, snapcraft_yaml_path) + config_data, project_options, + snapcraft_yaml_path, original_snapcraft_yaml) packaging.write_snap_yaml() packaging.setup_assets() packaging.generate_hook_wrappers() @@ -120,43 +135,74 @@ if not part: raise meta_errors.AdoptedPartMissingError(part_name) - # This would be caught since metadata would be missing, but we want - # to be clear about the issue here. This really should be caught by the - # schema, but it doesn't seem to support such dynamic behavior. - if 'parse-info' not in config_data['parts'][part_name]: - raise meta_errors.AdoptedPartNotParsingInfo(part_name) - - # Get the metadata from the pull step first, then update it using the - # metadata from the build step (i.e. the data from the build step takes - # precedence over the pull step) - metadata = part.get_pull_state().extracted_metadata['metadata'] - metadata.update(part.get_build_state().extracted_metadata['metadata']) - _adopt_info(config_data, metadata) + pull_state = part.get_pull_state() + build_state = part.get_build_state() + stage_state = part.get_stage_state() + prime_state = part.get_prime_state() + + # Get the metadata from the pull step first. + metadata = pull_state.extracted_metadata['metadata'] + + # Now update it using the metadata from the build step (i.e. the data + # from the build step takes precedence over the pull step). + metadata.update(build_state.extracted_metadata['metadata']) + + # Now make sure any scriptlet data are taken into account. Later steps + # take precedence, and scriptlet data (even in earlier steps) take + # precedence over extracted data. + metadata.update(pull_state.scriptlet_metadata) + metadata.update(build_state.scriptlet_metadata) + metadata.update(stage_state.scriptlet_metadata) + metadata.update(prime_state.scriptlet_metadata) + + if not metadata: + # If we didn't end up with any metadata, let's ensure this part was + # actually supposed to parse info. If not, let's try to be very + # clear about what's happening, here. We do this after checking for + # metadata because metadata could be supplied by scriptlets, too. + if 'parse-info' not in config_data['parts'][part_name]: + raise meta_errors.AdoptedPartNotParsingInfo(part_name) - # Verify that all mandatory keys have been satisfied - missing_keys = [] # type: List[str] - for key in _MANDATORY_PACKAGE_KEYS: - if key not in config_data: - missing_keys.append(key) - - if missing_keys: - raise meta_errors.MissingSnapcraftYamlKeysError(keys=missing_keys) + _adopt_info(config_data, metadata) def _adopt_info( config_data: Dict[str, Any], extracted_metadata: _metadata.ExtractedMetadata): + ignored_keys = _adopt_keys(config_data, extracted_metadata) + if ignored_keys: + logger.warning( + 'The {keys} {plural_property} {plural_is} specified in adopted ' + 'info as well as the YAML: taking the {plural_property} from the ' + 'YAML'.format( + keys=formatting_utils.humanize_list(list(ignored_keys), 'and'), + plural_property=formatting_utils.pluralize( + ignored_keys, 'property', 'properties'), + plural_is=formatting_utils.pluralize( + ignored_keys, 'is', 'are'))) + + +def _adopt_keys(config_data: Dict[str, Any], + extracted_metadata: _metadata.ExtractedMetadata) -> Set[str]: + ignored_keys = set() metadata_dict = extracted_metadata.to_dict() - for key, value in metadata_dict.items(): - # desktop_file_paths are a special case that will be handled - # after all the top level snapcraft.yaml keys. - if key != 'desktop_file_paths' and key not in config_data: + + # desktop_file_paths and common_ids are special cases that will be handled + # after all the top level snapcraft.yaml keys. + ignore = ('desktop_file_paths', 'common_id') + overrides = ((k, v) for k, v in metadata_dict.items() if k not in ignore) + + for key, value in overrides: + if key not in config_data: if key == 'icon': if _icon_file_exists() or not os.path.exists( str(value)): # Do not overwrite the icon file. continue config_data[key] = value + else: + ignored_keys.add(key) + if 'desktop_file_paths' in metadata_dict and 'common_id' in metadata_dict: app_name = _get_app_name_from_common_id( config_data, str(metadata_dict['common_id'])) @@ -167,6 +213,8 @@ desktop_file_path) break + return ignored_keys + def _icon_file_exists() -> bool: """Check if the icon is specified as a file in the assets dir. @@ -222,6 +270,32 @@ return False +def _update_yaml_with_defaults(config_data, schema): + # Ensure that grade and confinement have their defaults applied, if + # necessary. Defaults are taken from the schema. Technically these are the + # only two optional keywords currently WITH defaults, but we don't want to + # risk setting something that we add later on accident. + for key in ('confinement', 'grade'): + if key not in config_data: + with contextlib.suppress(KeyError): + default = schema[key]['default'] + config_data[key] = default + logger.warn( + '{!r} property not specified: defaulting to {!r}'.format( + key, default)) + + +def _ensure_required_keywords(config_data): + # Verify that all mandatory keys have been satisfied + missing_keys = [] # type: List[str] + for key in _MANDATORY_PACKAGE_KEYS: + if key not in config_data: + missing_keys.append(key) + + if missing_keys: + raise meta_errors.MissingSnapcraftYamlKeysError(keys=missing_keys) + + class _SnapPackaging: @property @@ -229,9 +303,10 @@ return self._meta_dir def __init__( - self, config_data, - project_options: _options.ProjectOptions, - snapcraft_yaml_path: str) -> None: + self, config_data: Dict[str, Any], + project_options: Project, + snapcraft_yaml_path: str, + original_snapcraft_yaml: Dict[str, Any]) -> None: self._snapcraft_yaml_path = snapcraft_yaml_path self._prime_dir = project_options.prime_dir self._parts_dir = project_options.parts_dir @@ -240,6 +315,7 @@ project_options.is_host_compatible_with_base) self._meta_dir = os.path.join(self._prime_dir, 'meta') self._config_data = config_data.copy() + self._original_snapcraft_yaml = original_snapcraft_yaml os.makedirs(self._meta_dir, exist_ok=True) @@ -398,7 +474,9 @@ if 'apps' in self._config_data: _verify_app_paths(basedir='prime', apps=self._config_data['apps']) snap_yaml['apps'] = self._wrap_apps(self._config_data['apps']) - snap_yaml['apps'] = self._render_socket_modes(snap_yaml['apps']) + self._render_socket_modes(snap_yaml['apps']) + + self._process_passthrough_properties(snap_yaml) return snap_yaml @@ -482,7 +560,7 @@ return os.path.relpath(wrappath, self._prime_dir) - def _wrap_apps(self, apps): + def _wrap_apps(self, apps: Dict[str, Any]) -> Dict[str, Any]: gui_dir = os.path.join(self.meta_dir, 'gui') if not os.path.exists(gui_dir): os.mkdir(gui_dir) @@ -513,14 +591,47 @@ desktop_file.parse_and_reformat() desktop_file.write(gui_dir=os.path.join(self.meta_dir, 'gui')) - def _render_socket_modes(self, apps): + def _render_socket_modes(self, apps: Dict[str, Any]) -> None: for app in apps.values(): sockets = app.get('sockets', {}) for socket in sockets.values(): mode = socket.get('socket-mode') if mode is not None: socket['socket-mode'] = OctInt(mode) - return apps + + def _process_passthrough_properties( + self, snap_yaml: Dict[str, Any]) -> None: + passthrough_applied = False + + for section in ['apps', 'hooks']: + if section in self._config_data: + for name, value in snap_yaml[section].items(): + if self._apply_passthrough( + value, value.pop('passthrough', {}), + self._original_snapcraft_yaml[section][name]): + passthrough_applied = True + + if self._apply_passthrough(snap_yaml, + self._config_data.get('passthrough', {}), + self._original_snapcraft_yaml): + passthrough_applied = True + + if passthrough_applied: + logger.warn("The 'passthrough' property is being used to " + "propagate experimental properties to snap.yaml " + "that have not been validated. The snap cannot be " + "released to the store.") + + def _apply_passthrough(self, section: Dict[str, Any], + passthrough: Dict[str, Any], + original: Dict[str, Any]) -> bool: + # Any value already in the original dictionary must + # not be specified in passthrough at the same time. + duplicates = list(original.keys() & passthrough.keys()) + if duplicates: + raise meta_errors.AmbiguousPassthroughKeyError(duplicates) + section.update(passthrough) + return bool(passthrough) def _find_bin(binary, basedir): diff -Nru snapcraft-2.40/snapcraft/internal/parser.py snapcraft-2.41/snapcraft/internal/parser.py --- snapcraft-2.40/snapcraft/internal/parser.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/parser.py 2018-04-14 12:13:35.000000000 +0000 @@ -88,7 +88,7 @@ yaml_file = project_loader.get_snapcraft_yaml(base_dir=origin_dir) try: with open(yaml_file) as fp: - origin_data = yaml.load(fp) + origin_data = yaml.safe_load(fp) except ScannerError as e: raise errors.InvalidWikiEntryError(e) from e @@ -217,7 +217,7 @@ """Add valid wiki entries to the master parts list""" # return the number of errors encountered try: - data = yaml.load(entry) + data = yaml.safe_load(entry) except (ScannerError, ParserError) as e: raise errors.InvalidWikiEntryError( 'Bad wiki entry, possibly malformed YAML for entry: {}'.format(e)) diff -Nru snapcraft-2.40/snapcraft/internal/pluginhandler/__init__.py snapcraft-2.41/snapcraft/internal/pluginhandler/__init__.py --- snapcraft-2.40/snapcraft/internal/pluginhandler/__init__.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/pluginhandler/__init__.py 2018-04-14 12:13:35.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 collections import contextlib import copy import filecmp @@ -34,7 +35,7 @@ from ._build_attributes import BuildAttributes from ._metadata_extraction import extract_metadata from ._plugin_loader import load_plugin # noqa -from ._scriptlets import ScriptRunner +from ._runner import Runner from ._patchelf import PartPatcher logger = logging.getLogger(__name__) @@ -98,6 +99,24 @@ self._build_attributes = BuildAttributes( self._part_properties['build-attributes']) + # Scriptlet data is a dict of dicts for each step + self._scriptlet_metadata = collections.defaultdict( + snapcraft.extractors.ExtractedMetadata) + self._runner = Runner( + part_properties=self._part_properties, + sourcedir=self.plugin.sourcedir, + builddir=self.plugin.build_basedir, + stagedir=self.stagedir, + primedir=self.primedir, + builtin_functions={ + 'pull': self._do_pull, + 'build': self.plugin.build, + 'stage': self._do_stage, + 'prime': self._do_prime, + 'set-version': self._set_version, + 'set-grade': self._set_grade, + }) + self._migrate_state_file() def get_pull_state(self) -> states.PullState: @@ -140,6 +159,46 @@ return source_handler + def _set_version(self, *, version): + try: + self._set_scriptlet_metadata( + snapcraft.extractors.ExtractedMetadata(version=version)) + except errors.ScriptletDuplicateDataError as e: + raise errors.ScriptletDuplicateFieldError('version', e.other_step) + + def _set_grade(self, *, grade): + try: + self._set_scriptlet_metadata( + snapcraft.extractors.ExtractedMetadata(grade=grade)) + except errors.ScriptletDuplicateDataError as e: + raise errors.ScriptletDuplicateFieldError('grade', e.other_step) + + def _set_scriptlet_metadata( + self, metadata: snapcraft.extractors.ExtractedMetadata): + step = self.next_step() + + # First, ensure the metadata set here doesn't conflict with metadata + # already set for this step + conflicts = metadata.overlap(self._scriptlet_metadata[step]) + if len(conflicts) > 0: + raise errors.ScriptletDuplicateDataError( + step, step, list(conflicts)) + + last_step = self.last_step() + if last_step: + # Now ensure the metadata from this step doesn't conflict with + # metadata from any other step + index = common.COMMAND_ORDER.index(last_step) + for index in reversed(range(0, index+1)): + other_step = common.COMMAND_ORDER[index] + state = states.get_state(self.plugin.statedir, other_step) + conflicts = metadata.overlap(state.scriptlet_metadata) + if len(conflicts) > 0: + raise errors.ScriptletDuplicateDataError( + step, other_step, list(conflicts)) + + self._scriptlet_metadata[step].update(metadata) + def makedirs(self): dirs = [ self.plugin.sourcedir, self.plugin.builddir, @@ -173,6 +232,16 @@ return None + def next_step(self): + next_step = None + for step in reversed(common.COMMAND_ORDER): + if os.path.exists( + states.get_step_state_file(self.plugin.statedir, step)): + break + next_step = step + + return next_step + def is_clean(self, step): """Return true if the given step hasn't run (or has been cleaned).""" @@ -272,14 +341,25 @@ self._unpack_stage_packages() def pull(self, force=False): + # Ensure any previously-failed pull is cleared out before we try again + if (os.path.islink(self.plugin.sourcedir) or + os.path.isfile(self.plugin.sourcedir)): + os.remove(self.plugin.sourcedir) + elif os.path.isdir(self.plugin.sourcedir): + shutil.rmtree(self.plugin.sourcedir) + self.makedirs() self.notify_part_progress('Pulling') + + self._runner.pull() + + self.mark_pull_done() + + def _do_pull(self): if self.source_handler: self.source_handler.pull() self.plugin.pull() - self.mark_pull_done() - def mark_pull_done(self): pull_properties = self.plugin.get_pull_properties() @@ -303,8 +383,8 @@ build_packages=part_build_packages, source_details=self.source_handler.source_details, metadata=metadata, - metadata_files=metadata_files - )) + metadata_files=metadata_files, + scriptlet_metadata=self._scriptlet_metadata['pull'])) def clean_pull(self, hint=''): if self.is_clean('pull'): @@ -358,15 +438,19 @@ shutil.copytree(self.plugin.sourcedir, self.plugin.build_basedir, symlinks=True, ignore=ignore) - script_runner = ScriptRunner(builddir=self.plugin.build_basedir) - - script_runner.run(scriptlet=self._part_properties.get('prepare')) - build_scriptlet = self._part_properties.get('build') - if build_scriptlet: - script_runner.run(scriptlet=build_scriptlet) - else: - self.plugin.build() - script_runner.run(scriptlet=self._part_properties.get('install')) + self._runner.prepare() + self._runner.build() + self._runner.install() + + # Organize the installed files as requested. We do this in the build + # step for two reasons: + # + # 1. So cleaning and re-running the stage step works even if + # `organize` is used + # 2. So collision detection takes organization into account, i.e. we + # can use organization to get around file collisions between + # parts when staging. + self._organize() self.mark_build_done() @@ -408,7 +492,8 @@ plugin_assets=plugin_manifest, machine_assets=machine_manifest, metadata=metadata, - metadata_files=metadata_files)) + metadata_files=metadata_files, + scriptlet_metadata=self._scriptlet_metadata['build'])) def _get_machine_manifest(self): return { @@ -465,7 +550,14 @@ def stage(self, force=False): self.makedirs() self.notify_part_progress('Staging') - self._organize() + self._runner.stage() + + # Only mark this step done if _do_stage() didn't run, in which case + # we have no directories or files to track. + if self.is_clean('stage'): + self.mark_stage_done(set(), set()) + + def _do_stage(self): snap_files, snap_dirs = self.migratable_fileset_for('stage') def fixup_func(file_path): @@ -486,7 +578,7 @@ def mark_stage_done(self, snap_files, snap_dirs): self.mark_done('stage', states.StageState( snap_files, snap_dirs, self._part_properties, - self._project_options)) + self._project_options, self._scriptlet_metadata['stage'])) def clean_stage(self, project_staged_state, hint=''): if self.is_clean('stage'): @@ -510,6 +602,14 @@ def prime(self, force=False) -> None: self.makedirs() self.notify_part_progress('Priming') + self._runner.prime() + + # Only mark this step done if _do_prime() didn't run, in which case + # we have no files, directories, or dependency paths to track. + if self.is_clean('prime'): + self.mark_prime_done(set(), set(), set()) + + def _do_prime(self) -> None: snap_files, snap_dirs = self.migratable_fileset_for('prime') _migrate_files(snap_files, snap_dirs, self.stagedir, self.primedir) @@ -563,7 +663,7 @@ def mark_prime_done(self, snap_files, snap_dirs, dependency_paths): self.mark_done('prime', states.PrimeState( snap_files, snap_dirs, dependency_paths, self._part_properties, - self._project_options)) + self._project_options, self._scriptlet_metadata['prime'])) def clean_prime(self, project_primed_state, hint=''): if self.is_clean('prime'): @@ -831,7 +931,9 @@ def _organize_filesets(fileset, base_dir): for key in sorted(fileset, key=lambda x: ['*' in x, x]): src = os.path.join(base_dir, key) - dst = os.path.join(base_dir, fileset[key]) + # Remove the leading slash if there so os.path.join + # actually joins + dst = os.path.join(base_dir, fileset[key].lstrip('/')) sources = iglob(src, recursive=True) diff -Nru snapcraft-2.40/snapcraft/internal/pluginhandler/_runner.py snapcraft-2.41/snapcraft/internal/pluginhandler/_runner.py --- snapcraft-2.40/snapcraft/internal/pluginhandler/_runner.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/pluginhandler/_runner.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,245 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016, 2018 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 json +import os +import subprocess +import sys +import tempfile +import textwrap +import time +from typing import Any, Callable, Dict # noqa + +from snapcraft.internal import ( + common, + deprecations, + errors, +) + + +class Runner: + """The Runner class is responsible for orchestrating scriptlets.""" + + # FIXME: Need to quote builtin_functions typing because of + # https://github.com/python/typing/issues/259 which is fixed in Python + # 3.5.3. + def __init__(self, *, part_properties: Dict[str, Any], sourcedir: str, + builddir: str, stagedir: str, primedir: str, + builtin_functions: 'Dict[str, Callable[..., None]]') -> None: + """Create a new Runner. + :param dict part_properties: YAML properties set for this part. + :param str sourcedir: The source directory for this part. + :param str builddir: The build directory for this part. + :param str stagedir: The staging area. + :param str primedir: The priming area. + :param dict builtin_functions: Dict of builtin function names to + actual callables. + """ + self._sourcedir = sourcedir + self._builddir = builddir + self._stagedir = stagedir + self._primedir = primedir + self._builtin_functions = builtin_functions + + self._override_pull_scriptlet = part_properties.get('override-pull') + self._override_build_scriptlet = part_properties.get('override-build') + self._override_stage_scriptlet = part_properties.get('override-stage') + self._override_prime_scriptlet = part_properties.get('override-prime') + + # These are all deprecated + self._prepare_scriptlet = part_properties.get('prepare') + if self._prepare_scriptlet: + deprecations.handle_deprecation_notice('dn7') + + self._build_scriptlet = part_properties.get('build') + if self._build_scriptlet: + deprecations.handle_deprecation_notice('dn8') + + self._install_scriptlet = part_properties.get('install') + if self._install_scriptlet: + deprecations.handle_deprecation_notice('dn9') + + def pull(self) -> None: + """Run override-pull scriptlet.""" + if self._override_pull_scriptlet: + self._run_scriptlet( + 'override-pull', self._override_pull_scriptlet, + self._sourcedir) + + def prepare(self) -> None: + """Run prepare scriptlet.""" + if self._prepare_scriptlet: + self._run_scriptlet( + 'prepare', self._prepare_scriptlet, self._builddir) + + def build(self) -> None: + """Run override-build scriptlet.""" + if self._build_scriptlet: + self._run_scriptlet( + 'build', self._build_scriptlet, self._builddir) + elif self._override_build_scriptlet: + self._run_scriptlet( + 'override-build', self._override_build_scriptlet, + self._builddir) + + def install(self) -> None: + """Run install scriptlet.""" + if self._install_scriptlet: + self._run_scriptlet( + 'install', self._install_scriptlet, self._builddir) + + def stage(self) -> None: + """Run override-stage scriptlet.""" + if self._override_stage_scriptlet: + self._run_scriptlet( + 'override-stage', self._override_stage_scriptlet, + self._stagedir) + + def prime(self) -> None: + """Run override-prime scriptlet.""" + if self._override_prime_scriptlet: + self._run_scriptlet( + 'override-prime', self._override_prime_scriptlet, + self._primedir) + + def _run_scriptlet(self, scriptlet_name: str, scriptlet: str, + workdir: str) -> None: + with tempfile.TemporaryDirectory() as tempdir: + call_fifo = _NonBlockingRWFifo( + os.path.join(tempdir, 'function_call')) + feedback_fifo = _NonBlockingRWFifo( + os.path.join(tempdir, 'call_feedback')) + + env = '' + if common.is_snap(): + # Since the snap is classic, $SNAP/bin is not on the $PATH. + # Let's set an alias to make sure it's found (but only if it + # exists). + snapcraftctl_path = os.path.join( + os.getenv('SNAP'), 'bin', 'snapcraftctl') + if os.path.exists(snapcraftctl_path): + env += 'alias snapcraftctl="$SNAP/bin/snapcraftctl"\n' + env += common.assemble_env() + + # snapcraftctl only works consistently if it's using the exact same + # interpreter as that used by snapcraft itself, thus the definition + # of SNAPCRAFT_INTERPRETER. + script = textwrap.dedent("""\ + export SNAPCRAFTCTL_CALL_FIFO={call_fifo} + export SNAPCRAFTCTL_FEEDBACK_FIFO={feedback_fifo} + export SNAPCRAFT_INTERPRETER={interpreter} + {env} + {scriptlet} + """.format( + interpreter=sys.executable, call_fifo=call_fifo.path, + feedback_fifo=feedback_fifo.path, env=env, + scriptlet=scriptlet)) + + process = subprocess.Popen( + ['/bin/sh', '-e', '-c', script], cwd=workdir) + + status = None + try: + while status is None: + function_call = call_fifo.read() + if function_call: + # Handle the function and let caller know that function + # call has been handled (must contain at least a + # newline, anything beyond is considered an error by + # snapcraftctl) + feedback_fifo.write('{}\n'.format( + self._handle_builtin_function( + scriptlet_name, function_call.strip()))) + status = process.poll() + + # Don't loop TOO busily + time.sleep(0.1) + finally: + call_fifo.close() + feedback_fifo.close() + + if status: + raise errors.ScriptletRunError( + scriptlet_name=scriptlet_name, code=status) + + def _handle_builtin_function(self, scriptlet_name, function_call): + try: + function_json = json.loads(function_call) + except json.decoder.JSONDecodeError as e: + # This means a snapcraft developer messed up adding a new + # snapcraftctl function. Should never be encountered in real life. + raise ValueError( + '{!r} scriptlet called a function with invalid json: ' + '{}'.format(scriptlet_name, function_call)) from e + + try: + function_name = function_json['function'] + function_args = function_json['args'] + except KeyError as e: + # This means a snapcraft developer messed up adding a new + # snapcraftctl function. Should never be encountered in real life. + raise ValueError( + '{!r} scriptlet missing expected json field {!s} in args for ' + 'function call {!r}: {}'.format( + scriptlet_name, e, function_name, function_args)) from e + + try: + function = self._builtin_functions[function_name] + except KeyError as e: + # This means a snapcraft developer messed up adding a new + # snapcraftctl function. Should never be encountered in real life. + raise ValueError( + '{!r} scriptlet called an undefined builtin function: ' + '{}'.format(scriptlet_name, function_name)) from e + + # Return the feedback for this function call. No feedback + # (empty string) is the success case, and feedback is an error case, + # in which case it should be printed and snapcraftctl should print the + # feedback and exit non-zero. + try: + function(**function_args) + except errors.ScriptletBaseError as e: + return e.__str__() + return '' + + +class _NonBlockingRWFifo: + + def __init__(self, path) -> None: + os.mkfifo(path) + self.path = path + + # Using RDWR for every FIFO just so we can open them reliably whenever + # (i.e. write-only FIFOs can't be opened successfully until the reader + # is in place) + self._fd = os.open(self.path, os.O_RDWR | os.O_NONBLOCK) + + def read(self) -> str: + total_read = '' + with contextlib.suppress(BlockingIOError): + value = os.read(self._fd, 1024) + while value: + total_read += value.decode(sys.getfilesystemencoding()) + value = os.read(self._fd, 1024) + return total_read + + def write(self, data: str) -> int: + return os.write(self._fd, data.encode(sys.getfilesystemencoding())) + + def close(self) -> None: + if self._fd is not None: + os.close(self._fd) diff -Nru snapcraft-2.40/snapcraft/internal/pluginhandler/_scriptlets.py snapcraft-2.41/snapcraft/internal/pluginhandler/_scriptlets.py --- snapcraft-2.40/snapcraft/internal/pluginhandler/_scriptlets.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/pluginhandler/_scriptlets.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,46 +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 os -import tempfile -from contextlib import suppress - -from snapcraft.internal.common import run - - -class ScriptRunner: - """Runs /bin/sh scriptlets around the build step for a part.""" - - def __init__(self, *, builddir): - self._builddir = builddir - - def run(self, *, scriptlet): - """Runs the specified scriptlet.""" - if not scriptlet: - return - - try: - with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f: - f.write('#!/bin/sh -e\n') - f.write(scriptlet) - f.flush() - scriptlet_path = f.name - - os.chmod(scriptlet_path, 0o755) - run([scriptlet_path], cwd=self._builddir) - finally: - with suppress(FileNotFoundError): - os.unlink(scriptlet_path) diff -Nru snapcraft-2.40/snapcraft/internal/project_loader/_config.py snapcraft-2.41/snapcraft/internal/project_loader/_config.py --- snapcraft-2.40/snapcraft/internal/project_loader/_config.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/project_loader/_config.py 2018-04-14 12:13:35.000000000 +0000 @@ -23,10 +23,13 @@ import jsonschema import yaml import yaml.reader +from typing import Set # noqa: F401 -import snapcraft +from snapcraft import project +from snapcraft.project._project_info import ProjectInfo from snapcraft.internal import deprecations, remote_parts, states + from ._schema import Validator from ._parts_config import PartsConfig from ._env import ( @@ -84,28 +87,27 @@ self._remote_parts_attr = remote_parts.get_remote_parts() return self._remote_parts_attr - def __init__(self, project_options=None): + def __init__(self, project_options: project.Project=None) -> None: if project_options is None: - project_options = snapcraft.ProjectOptions() + project_options = project.Project() - self.build_snaps = set() - self.build_tools = [] + self.build_snaps = set() # type: Set[str] self._project_options = project_options self.snapcraft_yaml_path = get_snapcraft_yaml() snapcraft_yaml = _snapcraft_yaml_load(self.snapcraft_yaml_path) + self.original_snapcraft_yaml = snapcraft_yaml.copy() - self._validator = Validator(snapcraft_yaml) - self._validator.validate() + self.validator = Validator(snapcraft_yaml) + self.validator.validate() snapcraft_yaml = self._process_remote_parts(snapcraft_yaml) snapcraft_yaml = self._expand_filesets(snapcraft_yaml) - # both confinement type and build quality are optionals - _ensure_confinement_default(snapcraft_yaml, self._validator.schema) - _ensure_grade_default(snapcraft_yaml, self._validator.schema) - self.data = self._expand_env(snapcraft_yaml) + # We need to set the ProjectInfo here because ProjectOptions is + # created in the CLI. + self._project_options.info = ProjectInfo(self.data) self._ensure_no_duplicate_app_aliases() grammar_processor = grammar_processing.GlobalGrammarProcessor( @@ -117,7 +119,7 @@ self.parts = PartsConfig(parts=self.data, project_options=self._project_options, - validator=self._validator, + validator=self.validator, build_snaps=self.build_snaps, build_tools=self.build_tools, snapcraft_yaml=self.snapcraft_yaml_path) @@ -198,8 +200,9 @@ return [ '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']), + 'SNAPCRAFT_PROJECT_VERSION={}'.format( + self.data.get('version', '')), + 'SNAPCRAFT_PROJECT_GRADE={}'.format(self.data.get('grade', '')), ] def _expand_env(self, snapcraft_yaml): @@ -211,8 +214,10 @@ snapcraft_yaml[key], [ ('$SNAPCRAFT_PROJECT_NAME', snapcraft_yaml['name']), - ('$SNAPCRAFT_PROJECT_VERSION', snapcraft_yaml['version']), - ('$SNAPCRAFT_PROJECT_GRADE', snapcraft_yaml['grade']), + ('$SNAPCRAFT_PROJECT_VERSION', snapcraft_yaml.get( + 'version', '')), + ('$SNAPCRAFT_PROJECT_GRADE', snapcraft_yaml.get( + 'grade', '')), ('$SNAPCRAFT_STAGE', self._project_options.stage_dir), ]) return snapcraft_yaml @@ -262,7 +267,7 @@ try: with open(yaml_file, encoding=encoding) as fp: - return yaml.load(fp) + return yaml.safe_load(fp) except yaml.scanner.ScannerError as e: raise errors.YamlValidationError('{} on line {} of {}'.format( e.problem, e.problem_mark.line + 1, yaml_file)) from e @@ -272,24 +277,6 @@ chr(e.character), e.position + 1, yaml_file, e.reason)) from e -def _ensure_confinement_default(yaml_data, schema): - # Provide hint if the confinement property is missing, and add the - # default. We use the schema here so we don't have to hard-code defaults. - if 'confinement' not in yaml_data: - logger.warning('"confinement" property not specified: defaulting ' - 'to "strict"') - yaml_data['confinement'] = schema['confinement']['default'] - - -def _ensure_grade_default(yaml_data, schema): - # Provide hint if the grade property is missing, and add the - # default. We use the schema here so we don't have to hard-code defaults. - if 'grade' not in yaml_data: - logger.warning('"grade" property not specified: defaulting ' - 'to "stable"') - yaml_data['grade'] = schema['grade']['default'] - - def _expand_filesets_for(step, properties): filesets = properties.get('filesets', {}) fileset_for_step = properties.get(step, {}) diff -Nru snapcraft-2.40/snapcraft/internal/project_loader/errors.py snapcraft-2.41/snapcraft/internal/project_loader/errors.py --- snapcraft-2.40/snapcraft/internal/project_loader/errors.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/project_loader/errors.py 2018-04-14 12:13:35.000000000 +0000 @@ -58,10 +58,10 @@ class YamlValidationError(ProjectLoaderError): - fmt = 'Issues while validating {snapcraft_yaml}: {message}' + fmt = 'Issues while validating {source}: {message}' @classmethod - def from_validation_error(cls, error): + def from_validation_error(cls, error, *, source='snapcraft.yaml'): """Take a jsonschema.ValidationError and create a SnapcraftSchemaError. The validation errors coming from jsonschema are a nightmare. This @@ -85,10 +85,10 @@ else: messages.append(error.message) - return cls(' '.join(messages)) + return cls(' '.join(messages), source) - def __init__(self, message, snapcraft_yaml='snapcraft.yaml'): - super().__init__(message=message, snapcraft_yaml=snapcraft_yaml) + def __init__(self, message, source='snapcraft.yaml'): + super().__init__(message=message, source=source) class SnapcraftLogicError(ProjectLoaderError): diff -Nru snapcraft-2.40/snapcraft/internal/project_loader/_parts_config.py snapcraft-2.41/snapcraft/internal/project_loader/_parts_config.py --- snapcraft-2.40/snapcraft/internal/project_loader/_parts_config.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/project_loader/_parts_config.py 2018-04-14 12:13:35.000000000 +0000 @@ -38,7 +38,7 @@ build_snaps, build_tools, snapcraft_yaml): self._snap_name = parts['name'] self._base = parts.get('base', 'core') - self._confinement = parts['confinement'] + self._confinement = parts.get('confinement') self._soname_cache = elf.SonameCache() self._parts_data = parts.get('parts', {}) self._snap_type = parts.get('type', 'app') diff -Nru snapcraft-2.40/snapcraft/internal/project_loader/_schema.py snapcraft-2.41/snapcraft/internal/project_loader/_schema.py --- snapcraft-2.40/snapcraft/internal/project_loader/_schema.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/project_loader/_schema.py 2018-04-14 12:13:35.000000000 +0000 @@ -55,17 +55,18 @@ common.get_schemadir(), 'snapcraft.yaml')) try: with open(schema_file) as fp: - self._schema = yaml.load(fp) + self._schema = yaml.safe_load(fp) except FileNotFoundError: from snapcraft.internal.project_loader import errors raise errors.YamlValidationError( 'snapcraft validation file is missing from installation path') - def validate(self): + def validate(self, *, source=None): format_check = jsonschema.FormatChecker() try: jsonschema.validate( self._snapcraft, self._schema, format_checker=format_check) except jsonschema.ValidationError as e: from snapcraft.internal.project_loader import errors - raise errors.YamlValidationError.from_validation_error(e) + raise errors.YamlValidationError.from_validation_error( + e, source=source) diff -Nru snapcraft-2.40/snapcraft/internal/remote_parts.py snapcraft-2.41/snapcraft/internal/remote_parts.py --- snapcraft-2.40/snapcraft/internal/remote_parts.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/remote_parts.py 2018-04-14 12:13:35.000000000 +0000 @@ -124,7 +124,7 @@ return None with open(self._headers_yaml) as headers_file: - return yaml.load(headers_file) + return yaml.safe_load(headers_file) def _save_headers(self): headers = { @@ -143,7 +143,7 @@ update() with open(self.parts_yaml) as parts_file: - self._parts = yaml.load(parts_file) + self._parts = yaml.safe_load(parts_file) def get_part(self, part_name, full=False): try: diff -Nru snapcraft-2.40/snapcraft/internal/repo/_base.py snapcraft-2.41/snapcraft/internal/repo/_base.py --- snapcraft-2.40/snapcraft/internal/repo/_base.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/repo/_base.py 2018-04-14 12:13:35.000000000 +0000 @@ -22,6 +22,7 @@ import re import shutil import stat +from typing import List from snapcraft import file_utils from snapcraft.internal import mangling @@ -92,13 +93,28 @@ raise errors.NoNativeBackendError() @classmethod - def install_build_packages(cls, package_names): + def install_build_packages(cls, package_names: List[str]) -> List[str]: """Install packages on the host required to build. + This method needs to be implemented by using the appropriate method + to install packages on the system. If possible they should be marked + as automatically installed to allow for easy removal. + The method should return a list of the actually installed packages + in the form "package=version". + + If one of the packages cannot be found + snapcraft.repo.errors.BuildPackageNotFoundError should be raised. + If dependencies for a package cannot be resolved + snapcraft.repo.errors.PackageBrokenError should be raised. + If installing a package on the host failed + snapcraft.repo.errors.BuildPackagesNotInstalledError should be raised. + :param package_names: a list of package names to install. :type package_names: a list of strings. - :raises snapcraft.repo.errors.BuildPackageNotFoundError: - if one of the package_names cannot be installed. + :return: a list with the packages installed and their versions. + :rtype: list of strings. + :raises snapcraft.repo.errors.NoNativeBackendError: + if the method is not implemented in the subclass. """ raise errors.NoNativeBackendError() diff -Nru snapcraft-2.40/snapcraft/internal/repo/_deb.py snapcraft-2.41/snapcraft/internal/repo/_deb.py --- snapcraft-2.40/snapcraft/internal/repo/_deb.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/repo/_deb.py 2018-04-14 12:13:35.000000000 +0000 @@ -26,7 +26,7 @@ import sys import urllib import urllib.request -from typing import Dict, Set, List # noqa +from typing import Dict, Set, List, Tuple # noqa: F401 import apt from xml.etree import ElementTree @@ -218,13 +218,21 @@ return packages @classmethod - def install_build_packages(cls, package_names): - """Install build packages on the building machine. + def install_build_packages(cls, package_names: List[str]) -> List[str]: + """Install packages on the host required to build. + :param package_names: a list of package names to install. + :type package_names: a list of strings. :return: a list with the packages installed and their versions. - + :rtype: list of strings. + :raises snapcraft.repo.errors.BuildPackageNotFoundError: + if one of the packages was not found. + :raises snapcraft.repo.errors.PackageBrokenError: + if dependencies for one of the packages cannot be resolved. + :raises snapcraft.repo.errors.BuildPackagesNotInstalledError: + if installing the packages on the host failed. """ - new_packages = [] + new_packages = [] # type: List[Tuple[str, str]] with apt.Cache() as apt_cache: try: cls._mark_install(apt_cache, package_names) @@ -240,7 +248,8 @@ for package in new_packages] @classmethod - def _mark_install(cls, apt_cache, package_names): + def _mark_install(cls, apt_cache: apt.Cache, + package_names: List[str]) -> None: for name in package_names: if name.endswith(':any'): name = name[:-4] @@ -252,7 +261,11 @@ try: if version: _set_pkg_version(apt_cache[name_arch], version) - apt_cache[name_arch].mark_install() + # Disable automatic resolving of broken packages here + # because if that fails it raises a SystemError and the + # API doesn't expose enough information about he problem. + # Instead we let apt-get show a verbose error message later. + apt_cache[name_arch].mark_install(auto_fix=False) cls._verify_marked_install(apt_cache[name_arch]) except KeyError: raise errors.PackageNotFoundError(name) @@ -268,7 +281,7 @@ raise errors.PackageBrokenError(package.name, broken_deps) @classmethod - def _install_new_build_packages(cls, package_names): + def _install_new_build_packages(cls, package_names: List[str]) -> None: package_names.sort() logger.info( 'Installing build dependencies: %s', ' '.join(package_names)) @@ -284,7 +297,10 @@ apt_command.extend(['-o', 'Dpkg::Progress-Fancy=1']) apt_command.append('install') - subprocess.check_call(apt_command + package_names, env=env) + try: + subprocess.check_call(apt_command + package_names, env=env) + except subprocess.CalledProcessError: + raise errors.BuildPackagesNotInstalledError(packages=package_names) try: subprocess.check_call(['sudo', 'apt-mark', 'auto'] + diff -Nru snapcraft-2.40/snapcraft/internal/repo/errors.py snapcraft-2.41/snapcraft/internal/repo/errors.py --- snapcraft-2.40/snapcraft/internal/repo/errors.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/repo/errors.py 2018-04-14 12:13:35.000000000 +0000 @@ -46,6 +46,14 @@ super().__init__(package=package) +class BuildPackagesNotInstalledError(RepoError): + + fmt = "Could not install all requested build packages: {packages}" + + def __init__(self, *, packages: List[str]) -> None: + super().__init__(packages=' '.join(packages)) + + class PackageBrokenError(RepoError): fmt = "The package {package} has unmet dependencies: {deps}" diff -Nru snapcraft-2.40/snapcraft/internal/sources/_local.py snapcraft-2.41/snapcraft/internal/sources/_local.py --- snapcraft-2.40/snapcraft/internal/sources/_local.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/sources/_local.py 2018-04-14 12:13:35.000000000 +0000 @@ -17,7 +17,6 @@ import copy import glob import os -import shutil from snapcraft import file_utils from snapcraft.internal import common @@ -27,11 +26,6 @@ class Local(Base): def pull(self): - if os.path.islink(self.source_dir) or os.path.isfile(self.source_dir): - os.remove(self.source_dir) - elif os.path.isdir(self.source_dir): - shutil.rmtree(self.source_dir) - current_dir = os.getcwd() source_abspath = os.path.abspath(self.source) @@ -47,5 +41,5 @@ else: return [] - shutil.copytree(source_abspath, self.source_dir, symlinks=True, - copy_function=file_utils.link_or_copy, ignore=ignore) + file_utils.link_or_copy_tree( + source_abspath, self.source_dir, ignore=ignore) diff -Nru snapcraft-2.40/snapcraft/internal/states/_build_state.py snapcraft-2.41/snapcraft/internal/states/_build_state.py --- snapcraft-2.40/snapcraft/internal/states/_build_state.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/states/_build_state.py 2018-04-14 12:13:35.000000000 +0000 @@ -16,7 +16,7 @@ import yaml -from snapcraft import extractors +import snapcraft.extractors from snapcraft.internal.states._state import PartState @@ -37,6 +37,7 @@ 'disable-parallel', 'install', 'organize', + 'override-build', 'prepare', } @@ -47,7 +48,7 @@ def __init__( self, property_names, part_properties=None, project=None, plugin_assets=None, machine_assets=None, metadata=None, - metadata_files=None): + metadata_files=None, scriptlet_metadata=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. @@ -59,8 +60,11 @@ if machine_assets: self.assets.update(machine_assets) + if not scriptlet_metadata: + scriptlet_metadata = snapcraft.extractors.ExtractedMetadata() + if not metadata: - metadata = extractors.ExtractedMetadata() + metadata = snapcraft.extractors.ExtractedMetadata() if not metadata_files: metadata_files = [] @@ -70,6 +74,8 @@ 'files': metadata_files } + self.scriptlet_metadata = scriptlet_metadata + super().__init__(part_properties, project) def properties_of_interest(self, part_properties): diff -Nru snapcraft-2.40/snapcraft/internal/states/_prime_state.py snapcraft-2.41/snapcraft/internal/states/_prime_state.py --- snapcraft-2.40/snapcraft/internal/states/_prime_state.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/states/_prime_state.py 2018-04-14 12:13:35.000000000 +0000 @@ -16,6 +16,7 @@ import yaml +import snapcraft.extractors from snapcraft.internal.states._state import PartState @@ -31,12 +32,16 @@ yaml_tag = u'!PrimeState' def __init__(self, files, directories, dependency_paths=None, - part_properties=None, project=None): + part_properties=None, project=None, scriptlet_metadata=None): super().__init__(part_properties, project) + if not scriptlet_metadata: + scriptlet_metadata = snapcraft.extractors.ExtractedMetadata() + self.files = files self.directories = directories self.dependency_paths = set() + self.scriptlet_metadata = scriptlet_metadata if dependency_paths: self.dependency_paths = dependency_paths @@ -48,7 +53,10 @@ used to filter out files with a white or blacklist. """ - return {'prime': part_properties.get('prime', ['*']) or ['*']} + return { + 'override-prime': part_properties.get('override-prime'), + 'prime': part_properties.get('prime', ['*']) or ['*'], + } def project_options_of_interest(self, project): """Extract the options concerning this step from the project. diff -Nru snapcraft-2.40/snapcraft/internal/states/_pull_state.py snapcraft-2.41/snapcraft/internal/states/_pull_state.py --- snapcraft-2.40/snapcraft/internal/states/_pull_state.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/states/_pull_state.py 2018-04-14 12:13:35.000000000 +0000 @@ -30,6 +30,7 @@ def _schema_properties(): return { + 'override-pull', 'parse-info', 'plugin', 'source', @@ -48,7 +49,8 @@ def __init__(self, property_names, part_properties=None, project=None, stage_packages=None, build_snaps=None, build_packages=None, - source_details=None, metadata=None, metadata_files=None): + source_details=None, metadata=None, metadata_files=None, + scriptlet_metadata=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. @@ -60,6 +62,9 @@ 'source-details': source_details, } + if not scriptlet_metadata: + scriptlet_metadata = snapcraft.extractors.ExtractedMetadata() + if not metadata: metadata = snapcraft.extractors.ExtractedMetadata() @@ -71,6 +76,8 @@ 'files': metadata_files } + self.scriptlet_metadata = scriptlet_metadata + super().__init__(part_properties, project) def properties_of_interest(self, part_properties): diff -Nru snapcraft-2.40/snapcraft/internal/states/_stage_state.py snapcraft-2.41/snapcraft/internal/states/_stage_state.py --- snapcraft-2.40/snapcraft/internal/states/_stage_state.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/internal/states/_stage_state.py 2018-04-14 12:13:35.000000000 +0000 @@ -16,6 +16,7 @@ import yaml +import snapcraft.extractors from snapcraft.internal.states._state import PartState @@ -30,11 +31,16 @@ class StageState(PartState): yaml_tag = u'!StageState' - def __init__(self, files, directories, part_properties=None, project=None): + def __init__(self, files, directories, part_properties=None, project=None, + scriptlet_metadata=None): super().__init__(part_properties, project) + if not scriptlet_metadata: + scriptlet_metadata = snapcraft.extractors.ExtractedMetadata() + self.files = files self.directories = directories + self.scriptlet_metadata = scriptlet_metadata def properties_of_interest(self, part_properties): """Extract the properties concerning this step from part_properties. @@ -44,8 +50,9 @@ """ return { - 'stage': part_properties.get('stage', ['*']) or ['*'], 'filesets': part_properties.get('filesets', {}) or {}, + 'override-stage': part_properties.get('override-stage'), + 'stage': part_properties.get('stage', ['*']) or ['*'], } def project_options_of_interest(self, project): diff -Nru snapcraft-2.40/snapcraft/_options.py snapcraft-2.41/snapcraft/_options.py --- snapcraft-2.40/snapcraft/_options.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/_options.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,350 +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 logging -import multiprocessing -import os -import platform -import sys -from contextlib import suppress -from typing import List, Set # noqa: F401 - -from snapcraft import file_utils -from snapcraft.internal import common, errors, os_release -from snapcraft.internal.deprecations import handle_deprecation_notice - - -logger = logging.getLogger(__name__) - - -_ARCH_TRANSLATIONS = { - 'armv7l': { - 'kernel': 'arm', - 'deb': 'armhf', - 'uts_machine': 'arm', - 'cross-compiler-prefix': 'arm-linux-gnueabihf-', - 'cross-build-packages': ['gcc-arm-linux-gnueabihf', - 'libc6-dev-armhf-cross'], - 'triplet': 'arm-linux-gnueabihf', - 'core-dynamic-linker': 'lib/ld-linux-armhf.so.3', - }, - 'aarch64': { - 'kernel': 'arm64', - 'deb': 'arm64', - 'uts_machine': 'aarch64', - 'cross-compiler-prefix': 'aarch64-linux-gnu-', - 'cross-build-packages': ['gcc-aarch64-linux-gnu', - 'libc6-dev-arm64-cross'], - 'triplet': 'aarch64-linux-gnu', - 'core-dynamic-linker': 'lib/ld-linux-aarch64.so.1', - }, - 'i686': { - 'kernel': 'x86', - 'deb': 'i386', - 'uts_machine': 'i686', - 'triplet': 'i386-linux-gnu', - }, - 'ppc64le': { - 'kernel': 'powerpc', - 'deb': 'ppc64el', - 'uts_machine': 'ppc64el', - 'cross-compiler-prefix': 'powerpc64le-linux-gnu-', - 'cross-build-packages': ['gcc-powerpc64le-linux-gnu', - 'libc6-dev-ppc64el-cross'], - 'triplet': 'powerpc64le-linux-gnu', - 'core-dynamic-linker': 'lib64/ld64.so.2', - }, - 'ppc': { - 'kernel': 'powerpc', - 'deb': 'powerpc', - 'uts_machine': 'powerpc', - 'cross-compiler-prefix': 'powerpc-linux-gnu-', - 'cross-build-packages': ['gcc-powerpc-linux-gnu', - 'libc6-dev-powerpc-cross'], - 'triplet': 'powerpc-linux-gnu', - }, - 'x86_64': { - 'kernel': 'x86', - 'deb': 'amd64', - 'uts_machine': 'x86_64', - 'triplet': 'x86_64-linux-gnu', - 'core-dynamic-linker': 'lib64/ld-linux-x86-64.so.2', - }, - 's390x': { - 'kernel': 's390', - 'deb': 's390x', - 'uts_machine': 's390x', - 'cross-compiler-prefix': 's390x-linux-gnu-', - 'cross-build-packages': ['gcc-s390x-linux-gnu', - 'libc6-dev-s390x-cross'], - 'triplet': 's390x-linux-gnu', - 'core-dynamic-linker': 'lib/ld64.so.1', - } -} - - -_32BIT_USERSPACE_ARCHITECTURE = { - 'aarch64': 'armv7l', - 'armv8l': 'armv7l', - 'ppc64le': 'ppc', - 'x86_64': 'i686', -} - - -_WINDOWS_TRANSLATIONS = { - 'AMD64': 'x86_64' -} - - -_HOST_CODENAME_FOR_BASE = { - 'core18': 'bionic', - 'core': 'xenial', -} -_HOST_COMPATIBILITY = { - 'xenial': ['trusty', 'xenial'], - 'bionic': ['trusty', 'xenial', 'bionic'], -} - - -_LINKER_VERSION_FOR_BASE = { - 'core18': '2.27', - 'core': '2.23', -} - - -def _get_platform_architecture(): - architecture = platform.machine() - - # Translate the windows architectures we know of to architectures - # we can work with. - if sys.platform == 'win32': - architecture = _WINDOWS_TRANSLATIONS.get(architecture) - - if platform.architecture()[0] == '32bit': - userspace = _32BIT_USERSPACE_ARCHITECTURE.get(architecture) - if userspace: - architecture = userspace - - return architecture - - -class ProjectOptions: - - @property - def use_geoip(self): - return self.__use_geoip - - @property - def parallel_builds(self): - return self.__parallel_builds - - @property - def parallel_build_count(self): - build_count = 1 - if self.__parallel_builds: - try: - build_count = multiprocessing.cpu_count() - except NotImplementedError: - logger.warning( - 'Unable to determine CPU count; disabling parallel builds') - - return build_count - - @property - def is_cross_compiling(self): - 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 errors.SnapcraftEnvironmentError( - 'Cross compilation not supported for target arch {!r}'.format( - self.__target_machine)) - - @property - def additional_build_packages(self): - packages = [] - if self.is_cross_compiling: - packages.extend(self.__machine_info.get( - 'cross-build-packages', [])) - return packages - - @property - def arch_triplet(self): - return self.__machine_info['triplet'] - - @property - def deb_arch(self): - return self.__machine_info['deb'] - - @property - def kernel_arch(self): - return self.__machine_info['kernel'] - - @property - def local_plugins_dir(self): - deprecated_plugins_dir = os.path.join(self.parts_dir, 'plugins') - if os.path.exists(deprecated_plugins_dir): - handle_deprecation_notice('dn2') - return deprecated_plugins_dir - return os.path.join(self.__project_dir, 'snap', 'plugins') - - @property - def parts_dir(self): - return os.path.join(self.__project_dir, 'parts') - - @property - def stage_dir(self): - return os.path.join(self.__project_dir, 'stage') - - @property - def prime_dir(self): - return os.path.join(self.__project_dir, 'prime') - - @property - def debug(self): - return self.__debug - - def __init__(self, use_geoip=False, parallel_builds=True, - target_deb_arch=None, debug=False): - # TODO: allow setting a different project dir and check for - # snapcraft.yaml - self.__project_dir = os.getcwd() - self.__use_geoip = use_geoip - self.__parallel_builds = parallel_builds - self._set_machine(target_deb_arch) - self.__debug = debug - - def is_host_compatible_with_base(self, base: str) -> bool: - """Determines if the host is compatible with the GLIBC of the base. - - The system should warn early on when building using a host that does - not match the intended base, this mechanism here enables additional - logic when that is ignored to determine built projects will actually - run. - - :param str base: the base core snap to search for linker. - :returns: True if there are no GLIBC incompatibilities with the chosen - build host, else it returns False. - :rtype: bool - """ - codename = None # type: str - with suppress(errors.OsReleaseCodenameError): - codename = os_release.OsRelease().version_codename() - logger.debug('Running on {!r}'.format(codename)) - - build_host_for_base = _HOST_CODENAME_FOR_BASE.get( - base) # type: str - compatible_hosts = _HOST_COMPATIBILITY.get( - build_host_for_base, []) # type: List[str] - return codename in compatible_hosts - - # This is private to not make the API public given that base - # will be part of the new Project. - def _get_linker_version_for_base(self, base: str) -> str: - """Returns the linker version for base.""" - try: - return _LINKER_VERSION_FOR_BASE[base] - except KeyError: - linker_file = os.path.basename(self.get_core_dynamic_linker(base)) - return file_utils.get_linker_version_from_file(linker_file) - - def get_core_dynamic_linker(self, base: str, expand: bool=True) -> str: - """Returns the dynamic linker used for the targeted core. - - :param str base: the base core snap to search for linker. - :param bool expand: expand the linker to the actual linker if True, - else the main entry point to the linker for the - projects architecture. - :return: the absolute path to the linker - :rtype: str - :raises snapcraft.internal.errors.SnapcraftMissingLinkerInBaseError: - if the linker cannot be found in the base. - :raises snapcraft.internal.errors.SnapcraftEnvironmentError: - if a loop is found while resolving the real path to the linker. - """ - core_path = common.get_core_path(base) - dynamic_linker_path = os.path.join( - core_path, - self.__machine_info.get('core-dynamic-linker', - 'lib/ld-linux.so.2')) - - # return immediately if we do not need to expand - if not expand: - return dynamic_linker_path - - # We can't use os.path.realpath because any absolute symlinks - # have to be interpreted relative to core_path, not the real - # root. - seen_paths = set() # type: Set[str] - while True: - if dynamic_linker_path in seen_paths: - raise errors.SnapcraftEnvironmentError( - "found symlink loop resolving dynamic linker path") - - seen_paths.add(dynamic_linker_path) - if not os.path.lexists(dynamic_linker_path): - raise errors.SnapcraftMissingLinkerInBaseError( - base=base, linker_path=dynamic_linker_path) - if not os.path.islink(dynamic_linker_path): - return dynamic_linker_path - - link_contents = os.readlink(dynamic_linker_path) - if os.path.isabs(link_contents): - dynamic_linker_path = os.path.join( - core_path, link_contents.lstrip('/')) - else: - dynamic_linker_path = os.path.join( - os.path.dirname(dynamic_linker_path), link_contents) - - 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: - self.__target_machine = _find_machine(target_deb_arch) - logger.info('Setting target machine to {!r}'.format( - target_deb_arch)) - self.__machine_info = _ARCH_TRANSLATIONS[self.__target_machine] - - -def _get_deb_arch(machine): - return _ARCH_TRANSLATIONS[machine].get('deb', None) - - -def _find_machine(deb_arch): - for machine in _ARCH_TRANSLATIONS: - if _ARCH_TRANSLATIONS[machine].get('deb', '') == deb_arch: - return machine - elif _ARCH_TRANSLATIONS[machine].get('uts_machine', '') == deb_arch: - return machine - - raise errors.SnapcraftEnvironmentError( - 'Cannot set machine from deb_arch {!r}'.format(deb_arch)) diff -Nru snapcraft-2.40/snapcraft/plugins/catkin.py snapcraft-2.41/snapcraft/plugins/catkin.py --- snapcraft-2.40/snapcraft/plugins/catkin.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/plugins/catkin.py 2018-04-14 12:13:35.000000000 +0000 @@ -345,7 +345,11 @@ env.append('PATH=$PATH:{}/usr/bin'.format(root)) if self.options.underlay: - script = '. {}'.format(os.path.join( + script = textwrap.dedent(""" + if [ -f {snapcraft_setup} ]; then + . {snapcraft_setup} + fi + """).format(snapcraft_setup=os.path.join( self.rosdir, 'snapcraft-setup.sh')) else: script = self._source_setup_sh(root, None) diff -Nru snapcraft-2.40/snapcraft/plugins/dotnet.py snapcraft-2.41/snapcraft/plugins/dotnet.py --- snapcraft-2.40/snapcraft/plugins/dotnet.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/plugins/dotnet.py 2018-04-14 12:13:35.000000000 +0000 @@ -32,24 +32,46 @@ import os import shutil import fnmatch +import urllib.request +import json import snapcraft from snapcraft import sources +from snapcraft import formatting_utils +from typing import List -_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') - } +_DOTNET_RELEASE_METADATA_URL = 'https://raw.githubusercontent.com/dotnet/core/master/release-notes/releases.json' # noqa +_RUNTIME_DEFAULT = '2.0.5' + # TODO extend for other architectures -_SDK_DICT_FOR_ARCH = { - 'amd64': _SDKS_AMD64, -} +_SDK_ARCH = ['amd64'] + + +class DotNetBadArchitectureError(snapcraft.internal.errors.SnapcraftError): + + fmt = ( + 'Failed to prepare the .NET SDK: ' + 'The architecture {architecture!r} is not supported. ' + 'Supported architectures are: {supported}.' + ) + + def __init__(self, *, architecture: str, supported: List[str]) -> None: + super().__init__( + architecture=architecture, + supported=formatting_utils.humanize_list(supported, 'and')) + + +class DotNetBadReleaseDataError(snapcraft.internal.errors.SnapcraftError): + + fmt = ( + 'Failed to prepare the .NET SDK: ' + 'An error occurred while fetching the version details ' + 'for {version!r}. Check that the version is correct.' + ) + + def __init__(self, version): + super().__init__(version=version) class DotNetPlugin(snapcraft.BasePlugin): @@ -58,11 +80,22 @@ def schema(cls): schema = super().schema() + schema['properties']['dotnet-runtime-version'] = { + 'type': 'string', + 'default': _RUNTIME_DEFAULT, + } + if 'required' in schema: del schema['required'] 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 ['dotnet-runtime-version'] + def __init__(self, name, options, project): super().__init__(name, options, project) @@ -87,18 +120,16 @@ 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_arch = self.project.deb_arch + if sdk_arch not in _SDK_ARCH: + raise DotNetBadArchitectureError( + architecture=sdk_arch, supported=_SDK_ARCH) + # TODO: Make this a class that takes care of retrieving the infos + sdk_info = self._get_sdk_info(self.options.dotnet_runtime_version) - sdk_url = sdk_version['url_path'] + sdk_url = sdk_info['package_url'] return sources.Tar(sdk_url, self._dotnet_sdk_dir, - source_checksum=sdk_version['checksum']) + source_checksum=sdk_info['checksum']) def pull(self): super().pull() @@ -139,4 +170,63 @@ for file in os.listdir(self.builddir): if fnmatch.fnmatch(file, '*.??proj'): return os.path.splitext(file)[0] + + def _get_version_metadata(self, version): + jsonData = self._get_dotnet_release_metadata() + package_data = list(filter(lambda x: x.get('version-runtime') + == version, jsonData)) + + if not package_data: + raise DotNetBadReleaseDataError(version) + + return package_data + + def _get_dotnet_release_metadata(self): + package_metadata = [] + + req = urllib.request.Request(_DOTNET_RELEASE_METADATA_URL) + r = urllib.request.urlopen(req).read() + package_metadata = json.loads(r.decode('utf-8')) + + return package_metadata + + def _get_sdk_info(self, version): + metadata = self._get_version_metadata(version) + + if 'sdk-linux-x64' in metadata[0]: + # look for sdk-linux-x64 property, if it doesn't exist + # look for ubuntu.14.04 entry as shipped during 1.1 + sdk_packge_name = metadata[0]['sdk-linux-x64'] + elif 'sdk-ubuntu.14.04' in metadata[0]: + sdk_packge_name = metadata[0]['sdk-ubuntu.14.04'] + else: + raise DotNetBadReleaseDataError(version) + + sdk_package_url = '{}{}'.format(metadata[0]['blob-sdk'], + sdk_packge_name) + sdk_checksum = self._get_package_checksum( + metadata[0]['checksums-sdk'], sdk_packge_name, version) + + return {'package_url': sdk_package_url, 'checksum': sdk_checksum} + + def _get_package_checksum(self, checksum_url: str, + filename: str, version: str) -> str: + req = urllib.request.Request(checksum_url) + r = urllib.request.urlopen(req).read() + data = r.decode('utf-8').split('\n') + + hash = None + checksum = None + for line in data: + text = line.split() + if len(text) == 2 and 'Hash' in text[0]: + hash = text[1] + + if len(text) == 2 and filename in text[1]: + checksum = text[0] break + + if not hash or not checksum: + raise DotNetBadReleaseDataError(version) + + return '{}/{}'.format(hash.lower(), checksum) diff -Nru snapcraft-2.40/snapcraft/plugins/kernel.py snapcraft-2.41/snapcraft/plugins/kernel.py --- snapcraft-2.40/snapcraft/plugins/kernel.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/plugins/kernel.py 2018-04-14 12:13:35.000000000 +0000 @@ -178,6 +178,9 @@ def __init__(self, name, options, project): super().__init__(name, options, project) + # We need to be able to shell out to modprobe + self.build_packages.append('kmod') + self._set_kernel_targets() self.os_snap = os.path.join(self.sourcedir, 'os.snap') @@ -270,10 +273,15 @@ initrd_unpacked_path = self._unpack_generic_initrd() modprobe_outs = [] + + # modprobe is typically in /sbin, which may not always be available on + # the PATH. Add it for this call. + env = os.environ.copy() + env['PATH'] += ':/sbin' for module in self.options.kernel_initrd_modules: modprobe_out = self.run_output([ 'modprobe', '-n', '--show-depends', '-d', self.installdir, - '-S', self.kernel_release, module]) + '-S', self.kernel_release, module], env=env) modprobe_outs.extend(modprobe_out.split(os.linesep)) modprobe_outs = [_ for _ in modprobe_outs if _] diff -Nru snapcraft-2.40/snapcraft/plugins/nodejs.py snapcraft-2.41/snapcraft/plugins/nodejs.py --- snapcraft-2.40/snapcraft/plugins/nodejs.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/plugins/nodejs.py 2018-04-14 12:13:35.000000000 +0000 @@ -35,6 +35,9 @@ (list) A list of targets to `npm run`. These targets will be run in order, after `npm install` + - npm-flags: + (list) + A list of flags for npm. - node-package-manager (string; default: npm) The language package manager to use to drive installation @@ -102,6 +105,15 @@ }, 'default': [] } + schema['properties']['npm-flags'] = { + 'type': 'array', + 'minitems': 1, + 'uniqueItems': False, + 'items': { + 'type': 'string' + }, + 'default': [] + } if 'required' in schema: del schema['required'] @@ -112,7 +124,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 ['node-packages', 'npm-run'] + return ['node-packages', 'npm-run', 'npm-flags'] @classmethod def get_pull_properties(cls): @@ -175,14 +187,15 @@ def _npm_install(self, rootdir): self._nodejs_tar.provision( self.installdir, clean_target=False, keep_tarball=True) - npm_install = ['npm', '--cache-min=Infinity', 'install'] + npm_cmd = ['npm'] + self.options.npm_flags + npm_install = npm_cmd + ['--cache-min=Infinity', 'install'] for pkg in self.options.node_packages: self.run(npm_install + ['--global'] + [pkg], cwd=rootdir) if os.path.exists(os.path.join(rootdir, 'package.json')): self.run(npm_install, cwd=rootdir) self.run(npm_install + ['--global'], cwd=rootdir) for target in self.options.npm_run: - self.run(['npm', 'run', target], cwd=rootdir) + self.run(npm_cmd + ['run', target], cwd=rootdir) return self._get_installed_node_packages('npm', self.installdir) def _yarn_install(self, rootdir): @@ -191,6 +204,7 @@ self._yarn_tar.provision( self._npm_dir, clean_target=False, keep_tarball=True) yarn_cmd = [os.path.join(self._npm_dir, 'bin', 'yarn')] + yarn_cmd.extend(self.options.npm_flags) if 'http_proxy' in os.environ: yarn_cmd.extend(['--proxy', os.environ['http_proxy']]) if 'https_proxy' in os.environ: diff -Nru snapcraft-2.40/snapcraft/plugins/python.py snapcraft-2.41/snapcraft/plugins/python.py --- snapcraft-2.40/snapcraft/plugins/python.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/plugins/python.py 2018-04-14 12:13:35.000000000 +0000 @@ -63,7 +63,7 @@ import snapcraft from snapcraft.common import isurl -from snapcraft.internal import mangling +from snapcraft.internal import mangling, os_release from snapcraft.internal.errors import SnapcraftPluginCommandError from snapcraft.plugins import _python @@ -150,10 +150,20 @@ @property def plugin_stage_packages(self): - if self.options.python_version == 'python3': - return ['python3'] - elif self.options.python_version == 'python2': - return ['python'] + release_codename = os_release.OsRelease().version_codename() + if self.options.python_version == 'python2': + python_base = 'python' + elif self.options.python_version == 'python3': + python_base = 'python3' + else: + return + + stage_packages = [python_base] + if release_codename == 'bionic': + # In bionic, pip started requiring python-distutils to be + # installed. + stage_packages.append('{}-distutils'.format(python_base)) + return stage_packages # ignore mypy error: Read-only property cannot override read-write property @property # type: ignore @@ -317,7 +327,8 @@ constraints=constraints, requirements=requirements, process_dependency_links=self.options.process_dependency_links) - self._install_wheels(wheels) + if wheels: + self._install_wheels(wheels) if setup_py_dir is not None: setup_py_path = os.path.join(setup_py_dir, 'setup.py') diff -Nru snapcraft-2.40/snapcraft/project/__init__.py snapcraft-2.41/snapcraft/project/__init__.py --- snapcraft-2.40/snapcraft/project/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/snapcraft/project/__init__.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,17 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 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 ._project import Project # noqa F401 diff -Nru snapcraft-2.40/snapcraft/project/_project_info.py snapcraft-2.41/snapcraft/project/_project_info.py --- snapcraft-2.40/snapcraft/project/_project_info.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/snapcraft/project/_project_info.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,28 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 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 typing import Any, Dict + + +class ProjectInfo: + """Information gained from the snap's snapcraft.yaml file.""" + + def __init__(self, data: Dict[str, Any]) -> None: + self.name = data['name'] + self.version = data.get('version') + self.summary = data.get('summary') + self.description = data.get('description') + self.confinement = data.get('confinement') diff -Nru snapcraft-2.40/snapcraft/project/_project_options.py snapcraft-2.41/snapcraft/project/_project_options.py --- snapcraft-2.40/snapcraft/project/_project_options.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/snapcraft/project/_project_options.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,350 @@ +# -*- 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 multiprocessing +import os +import platform +import sys +from contextlib import suppress +from typing import List, Set # noqa: F401 + +from snapcraft import file_utils +from snapcraft.internal import common, errors, os_release +from snapcraft.internal.deprecations import handle_deprecation_notice + + +logger = logging.getLogger(__name__) + + +_ARCH_TRANSLATIONS = { + 'armv7l': { + 'kernel': 'arm', + 'deb': 'armhf', + 'uts_machine': 'arm', + 'cross-compiler-prefix': 'arm-linux-gnueabihf-', + 'cross-build-packages': ['gcc-arm-linux-gnueabihf', + 'libc6-dev-armhf-cross'], + 'triplet': 'arm-linux-gnueabihf', + 'core-dynamic-linker': 'lib/ld-linux-armhf.so.3', + }, + 'aarch64': { + 'kernel': 'arm64', + 'deb': 'arm64', + 'uts_machine': 'aarch64', + 'cross-compiler-prefix': 'aarch64-linux-gnu-', + 'cross-build-packages': ['gcc-aarch64-linux-gnu', + 'libc6-dev-arm64-cross'], + 'triplet': 'aarch64-linux-gnu', + 'core-dynamic-linker': 'lib/ld-linux-aarch64.so.1', + }, + 'i686': { + 'kernel': 'x86', + 'deb': 'i386', + 'uts_machine': 'i686', + 'triplet': 'i386-linux-gnu', + }, + 'ppc64le': { + 'kernel': 'powerpc', + 'deb': 'ppc64el', + 'uts_machine': 'ppc64el', + 'cross-compiler-prefix': 'powerpc64le-linux-gnu-', + 'cross-build-packages': ['gcc-powerpc64le-linux-gnu', + 'libc6-dev-ppc64el-cross'], + 'triplet': 'powerpc64le-linux-gnu', + 'core-dynamic-linker': 'lib64/ld64.so.2', + }, + 'ppc': { + 'kernel': 'powerpc', + 'deb': 'powerpc', + 'uts_machine': 'powerpc', + 'cross-compiler-prefix': 'powerpc-linux-gnu-', + 'cross-build-packages': ['gcc-powerpc-linux-gnu', + 'libc6-dev-powerpc-cross'], + 'triplet': 'powerpc-linux-gnu', + }, + 'x86_64': { + 'kernel': 'x86', + 'deb': 'amd64', + 'uts_machine': 'x86_64', + 'triplet': 'x86_64-linux-gnu', + 'core-dynamic-linker': 'lib64/ld-linux-x86-64.so.2', + }, + 's390x': { + 'kernel': 's390', + 'deb': 's390x', + 'uts_machine': 's390x', + 'cross-compiler-prefix': 's390x-linux-gnu-', + 'cross-build-packages': ['gcc-s390x-linux-gnu', + 'libc6-dev-s390x-cross'], + 'triplet': 's390x-linux-gnu', + 'core-dynamic-linker': 'lib/ld64.so.1', + } +} + + +_32BIT_USERSPACE_ARCHITECTURE = { + 'aarch64': 'armv7l', + 'armv8l': 'armv7l', + 'ppc64le': 'ppc', + 'x86_64': 'i686', +} + + +_WINDOWS_TRANSLATIONS = { + 'AMD64': 'x86_64' +} + + +_HOST_CODENAME_FOR_BASE = { + 'core18': 'bionic', + 'core': 'xenial', +} +_HOST_COMPATIBILITY = { + 'xenial': ['trusty', 'xenial'], + 'bionic': ['trusty', 'xenial', 'bionic'], +} + + +_LINKER_VERSION_FOR_BASE = { + 'core18': '2.27', + 'core': '2.23', +} + + +def _get_platform_architecture(): + architecture = platform.machine() + + # Translate the windows architectures we know of to architectures + # we can work with. + if sys.platform == 'win32': + architecture = _WINDOWS_TRANSLATIONS.get(architecture) + + if platform.architecture()[0] == '32bit': + userspace = _32BIT_USERSPACE_ARCHITECTURE.get(architecture) + if userspace: + architecture = userspace + + return architecture + + +class ProjectOptions: + + @property + def use_geoip(self): + return self.__use_geoip + + @property + def parallel_builds(self): + return self.__parallel_builds + + @property + def parallel_build_count(self): + build_count = 1 + if self.__parallel_builds: + try: + build_count = multiprocessing.cpu_count() + except NotImplementedError: + logger.warning( + 'Unable to determine CPU count; disabling parallel builds') + + return build_count + + @property + def is_cross_compiling(self): + 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 errors.SnapcraftEnvironmentError( + 'Cross compilation not supported for target arch {!r}'.format( + self.__target_machine)) + + @property + def additional_build_packages(self): + packages = [] + if self.is_cross_compiling: + packages.extend(self.__machine_info.get( + 'cross-build-packages', [])) + return packages + + @property + def arch_triplet(self): + return self.__machine_info['triplet'] + + @property + def deb_arch(self): + return self.__machine_info['deb'] + + @property + def kernel_arch(self): + return self.__machine_info['kernel'] + + @property + def local_plugins_dir(self): + deprecated_plugins_dir = os.path.join(self.parts_dir, 'plugins') + if os.path.exists(deprecated_plugins_dir): + handle_deprecation_notice('dn2') + return deprecated_plugins_dir + return os.path.join(self.__project_dir, 'snap', 'plugins') + + @property + def parts_dir(self): + return os.path.join(self.__project_dir, 'parts') + + @property + def stage_dir(self): + return os.path.join(self.__project_dir, 'stage') + + @property + def prime_dir(self): + return os.path.join(self.__project_dir, 'prime') + + @property + def debug(self): + return self.__debug + + def __init__(self, use_geoip=False, parallel_builds=True, + target_deb_arch=None, debug=False): + # TODO: allow setting a different project dir and check for + # snapcraft.yaml + self.__project_dir = os.getcwd() + self.__use_geoip = use_geoip + self.__parallel_builds = parallel_builds + self._set_machine(target_deb_arch) + self.__debug = debug + + def is_host_compatible_with_base(self, base: str) -> bool: + """Determines if the host is compatible with the GLIBC of the base. + + The system should warn early on when building using a host that does + not match the intended base, this mechanism here enables additional + logic when that is ignored to determine built projects will actually + run. + + :param str base: the base core snap to search for linker. + :returns: True if there are no GLIBC incompatibilities with the chosen + build host, else it returns False. + :rtype: bool + """ + codename = None # type: str + with suppress(errors.OsReleaseCodenameError): + codename = os_release.OsRelease().version_codename() + logger.debug('Running on {!r}'.format(codename)) + + build_host_for_base = _HOST_CODENAME_FOR_BASE.get( + base) # type: str + compatible_hosts = _HOST_COMPATIBILITY.get( + build_host_for_base, []) # type: List[str] + return codename in compatible_hosts + + # This is private to not make the API public given that base + # will be part of the new Project. + def _get_linker_version_for_base(self, base: str) -> str: + """Returns the linker version for base.""" + try: + return _LINKER_VERSION_FOR_BASE[base] + except KeyError: + linker_file = os.path.basename(self.get_core_dynamic_linker(base)) + return file_utils.get_linker_version_from_file(linker_file) + + def get_core_dynamic_linker(self, base: str, expand: bool=True) -> str: + """Returns the dynamic linker used for the targeted core. + + :param str base: the base core snap to search for linker. + :param bool expand: expand the linker to the actual linker if True, + else the main entry point to the linker for the + projects architecture. + :return: the absolute path to the linker + :rtype: str + :raises snapcraft.internal.errors.SnapcraftMissingLinkerInBaseError: + if the linker cannot be found in the base. + :raises snapcraft.internal.errors.SnapcraftEnvironmentError: + if a loop is found while resolving the real path to the linker. + """ + core_path = common.get_core_path(base) + dynamic_linker_path = os.path.join( + core_path, + self.__machine_info.get('core-dynamic-linker', + 'lib/ld-linux.so.2')) + + # return immediately if we do not need to expand + if not expand: + return dynamic_linker_path + + # We can't use os.path.realpath because any absolute symlinks + # have to be interpreted relative to core_path, not the real + # root. + seen_paths = set() # type: Set[str] + while True: + if dynamic_linker_path in seen_paths: + raise errors.SnapcraftEnvironmentError( + "found symlink loop resolving dynamic linker path") + + seen_paths.add(dynamic_linker_path) + if not os.path.lexists(dynamic_linker_path): + raise errors.SnapcraftMissingLinkerInBaseError( + base=base, linker_path=dynamic_linker_path) + if not os.path.islink(dynamic_linker_path): + return dynamic_linker_path + + link_contents = os.readlink(dynamic_linker_path) + if os.path.isabs(link_contents): + dynamic_linker_path = os.path.join( + core_path, link_contents.lstrip('/')) + else: + dynamic_linker_path = os.path.join( + os.path.dirname(dynamic_linker_path), link_contents) + + 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: + self.__target_machine = _find_machine(target_deb_arch) + logger.info('Setting target machine to {!r}'.format( + target_deb_arch)) + self.__machine_info = _ARCH_TRANSLATIONS[self.__target_machine] + + +def _get_deb_arch(machine): + return _ARCH_TRANSLATIONS[machine].get('deb', None) + + +def _find_machine(deb_arch): + for machine in _ARCH_TRANSLATIONS: + if _ARCH_TRANSLATIONS[machine].get('deb', '') == deb_arch: + return machine + elif _ARCH_TRANSLATIONS[machine].get('uts_machine', '') == deb_arch: + return machine + + raise errors.SnapcraftEnvironmentError( + 'Cannot set machine from deb_arch {!r}'.format(deb_arch)) diff -Nru snapcraft-2.40/snapcraft/project/_project.py snapcraft-2.41/snapcraft/project/_project.py --- snapcraft-2.40/snapcraft/project/_project.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/snapcraft/project/_project.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,29 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 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 ._project_options import ProjectOptions +from ._project_info import ProjectInfo # noqa: F401 + + +class Project(ProjectOptions): + """All details around building a project concerning the build environment + and the snap being built.""" + + def __init__(self, *, use_geoip=False, parallel_builds=True, + target_deb_arch: str=None, debug=False) -> None: + self.info = None # type: ProjectInfo + + super().__init__(use_geoip, parallel_builds, target_deb_arch, debug) diff -Nru snapcraft-2.40/snapcraft/storeapi/errors.py snapcraft-2.41/snapcraft/storeapi/errors.py --- snapcraft-2.40/snapcraft/storeapi/errors.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/storeapi/errors.py 2018-04-14 12:13:35.000000000 +0000 @@ -15,10 +15,15 @@ # along with this program. If not, see . import contextlib +import logging from simplejson.scanner import JSONDecodeError from typing import List # noqa from snapcraft.internal.errors import SnapcraftError +from snapcraft import formatting_utils + + +logger = logging.getLogger(__name__) class StoreError(SnapcraftError): @@ -28,6 +33,12 @@ """ + def __init__(self, **kwargs): + with contextlib.suppress(KeyError, AttributeError): + logger.debug('Store error response: {}'.format( + kwargs['response'].__dict__)) + super().__init__(**kwargs) + class InvalidCredentialsError(StoreError): @@ -108,7 +119,7 @@ if extra_error_message: message += ': {}'.format(extra_error_message) - super().__init__(message=message) + super().__init__(response=response, message=message) class StoreTwoFactorAuthenticationRequired(StoreAuthenticationError): @@ -130,7 +141,8 @@ 'Text: {text!r}') def __init__(self, response): - super().__init__(reason=response.reason, text=response.text) + super().__init__( + response=response, reason=response.reason, text=response.text) class NeedTermsSignedError(StoreError): @@ -160,7 +172,7 @@ 'error_list'] if 'extra' in error] except JSONDecodeError: pass - super().__init__(error=error, extra=extra) + super().__init__(response=response, error=error, extra=extra) class StoreKeyRegistrationError(StoreError): @@ -176,7 +188,7 @@ error['message'] for error in response_json['error_list']) except JSONDecodeError: pass - super().__init__(error=error) + super().__init__(response=response, error=error) class StoreRegistrationError(StoreError): @@ -261,7 +273,8 @@ 'Text: {text!r}') def __init__(self, response): - super().__init__(reason=response.reason, text=response.text) + super().__init__( + response=response, reason=response.reason, text=response.text) class StorePushError(StoreError): @@ -287,8 +300,9 @@ except AttributeError: response_json['text'] = 'error while pushing' - super().__init__(snap_name=snap_name, status_code=response.status_code, - **response_json) + super().__init__( + response=response, snap_name=snap_name, + status_code=response.status_code, **response_json) class StoreReviewError(StoreError): @@ -317,19 +331,24 @@ def __init__(self, result): self.fmt = self.__messages[result['code']] + additional = [] errors = result.get('errors') if errors: for error in errors: message = error.get('message') if message: - self.fmt = '{}\n - {message}'.format( - self.fmt, message=message) + additional.append(' - {message}'.format(message=message)) + if additional: + self.additional = '\n'.join(additional) + self.fmt += '\n{additional}' self.code = result['code'] super().__init__() class StoreReleaseError(StoreError): + fmt = '{message}' + __FMT_NOT_REGISTERED = ( 'Sorry, try `snapcraft register {snap_name}` before trying to ' 'release or choose an existing revision.') @@ -338,7 +357,7 @@ '{code}: {message}\n') __FMT_UNAUTHORIZED_OR_FORBIDDEN = ( - 'Received {status_code!r}: {text!r}') + 'Received {status_code!r}: {text}') def __init__(self, snap_name, response): self.fmt_errors = { @@ -346,14 +365,14 @@ 401: self.__fmt_error_401_or_403, 403: self.__fmt_error_401_or_403, 404: self.__fmt_error_404, + 500: self.__fmt_error_500, } fmt_error = self.fmt_errors.get( response.status_code, self.__fmt_error_unknown) - self.fmt = fmt_error(response) - - super().__init__(snap_name=snap_name) + super().__init__(response=response, message=fmt_error(response).format( + snap_name=snap_name)) def __to_json(self, response): try: @@ -380,6 +399,11 @@ try: text = response.text + with contextlib.suppress(AttributeError, JSONDecodeError): + response_json = response.json() + if 'error_list' in response_json: + text = _error_list_to_message(response_json) + except AttributeError: text = 'error while releasing' @@ -389,6 +413,17 @@ def __fmt_error_404(self, response): return self.__FMT_NOT_REGISTERED + def __fmt_error_500(self, response): + response_json = self.__to_json(response) + message = ('The store encountered an internal error. The status of ' + 'store and associated services can be checked at:\n' + 'https://status.snapcraft.io/') + + if 'error_list' in response_json: + message = _error_list_to_message(response_json) + + return message + def __fmt_error_unknown(self, response): response_json = self.__to_json(response) @@ -434,12 +469,14 @@ parts.append( "You can repeat the push-metadata command with " "--force to force the local values into the Store") - self.fmt = "\n".join(parts) + self.parts = "\n".join(parts) + self.fmt = '{parts}' elif 'error_list' in response_json: response_json['text'] = response_json['error_list'][0]['message'] - super().__init__(snap_name=snap_name, status_code=response.status_code, - **response_json) + super().__init__( + response=response, snap_name=snap_name, + status_code=response.status_code, **response_json) class StoreValidationError(StoreError): @@ -455,7 +492,7 @@ except (AttributeError, JSONDecodeError): response_json = {'text': message or response} - super().__init__(status_code=response.status_code, + super().__init__(response=response, status_code=response.status_code, **response_json) @@ -473,7 +510,7 @@ except JSONDecodeError: pass - super().__init__(error=error) + super().__init__(response=response, error=error) class StoreSnapRevisionsError(StoreError): @@ -493,7 +530,7 @@ pass super().__init__( - snap_id=snap_id, arch=arch or 'any arch', + response=response, snap_id=snap_id, arch=arch or 'any arch', series=series or 'any', error=error) @@ -524,7 +561,7 @@ error = '{} {}'.format( response.status_code, response.reason) - super().__init__(error=error) + super().__init__(response=response, error=error) class StoreChannelClosingPermissionError(StoreError): @@ -624,3 +661,52 @@ def __init__(self, snap_name): super().__init__(snap_name=snap_name) + + +class InvalidLoginConfig(StoreError): + + fmt = 'Invalid login config: {error}' + + def __init__(self, error): + super().__init__(error=error) + + +def _error_list_to_message(response_json): + """Handle error list. + + The error format is given here: + https://dashboard.snapcraft.io/docs/api/snap.html#format + """ + messages = [] + for error_list_item in response_json['error_list']: + messages.append(_error_list_item_to_message( + error_list_item, response_json)) + + return ', and '.join(messages) + + +def _error_list_item_to_message(error_list_item, response_json): + """Handle error list item according to code. + + The list of codes are here: + https://dashboard.snapcraft.io/docs/api/snap.html#codes + """ + code = error_list_item['code'] + message = '' + if code == 'macaroon-permission-required': + message = _handle_macaroon_permission_required(response_json) + + if message: + return message + else: + return error_list_item['message'] + + +def _handle_macaroon_permission_required(response_json): + if 'permission' in response_json and 'channels' in response_json: + if response_json['permission'] == 'channel': + channels = response_json['channels'] + return 'Lacking permission to release to channel(s) {}'.format( + formatting_utils.humanize_list(channels, 'and')) + + return '' diff -Nru snapcraft-2.40/snapcraft/_store.py snapcraft-2.41/snapcraft/_store.py --- snapcraft-2.40/snapcraft/_store.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snapcraft/_store.py 2018-04-14 12:13:35.000000000 +0000 @@ -61,7 +61,7 @@ with open(os.path.join( temp_dir, 'squashfs-root', 'meta', 'snap.yaml') ) as yaml_file: - snap_yaml = yaml.load(yaml_file) + snap_yaml = yaml.safe_load(yaml_file) return snap_yaml diff -Nru snapcraft-2.40/snaps_tests/__init__.py snapcraft-2.41/snaps_tests/__init__.py --- snapcraft-2.40/snaps_tests/__init__.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/snaps_tests/__init__.py 2018-04-14 12:13:35.000000000 +0000 @@ -115,7 +115,7 @@ self.patchelf_command = 'patchelf' if os.getenv('SNAPCRAFT_FROM_SNAP', False): self.snapcraft_command = '/snap/bin/snapcraft' - self.patchelf_command = '/snap/snapcraft/current/bin/patchelf' + self.patchelf_command = '/snap/snapcraft/current/usr/bin/patchelf' elif os.getenv('SNAPCRAFT_FROM_DEB', False): self.snapcraft_command = '/usr/bin/snapcraft' elif os.getenv('VIRTUAL_ENV'): diff -Nru snapcraft-2.40/spread_tests/lifecycle/task.yaml snapcraft-2.41/spread_tests/lifecycle/task.yaml --- snapcraft-2.40/spread_tests/lifecycle/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/spread_tests/lifecycle/task.yaml 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,10 @@ +summary: sources integration tests + +kill-timeout: 30m +execute: | + echo "Check that we are using the snapcraft snap" + snapcraft_path="$(which snapcraft)" + [ "$snapcraft_path" = "/snap/bin/snapcraft" ] + source /snapcraft/venv/bin/activate + cd /snapcraft + ./runtests.sh tests/integration/lifecycle diff -Nru snapcraft-2.40/spread_tests/sources/task.yaml snapcraft-2.41/spread_tests/sources/task.yaml --- snapcraft-2.40/spread_tests/sources/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/spread_tests/sources/task.yaml 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,10 @@ +summary: sources integration tests + +kill-timeout: 30m +execute: | + echo "Check that we are using the snapcraft snap" + snapcraft_path="$(which snapcraft)" + [ "$snapcraft_path" = "/snap/bin/snapcraft" ] + source /snapcraft/venv/bin/activate + cd /snapcraft + ./runtests.sh tests/integration/sources diff -Nru snapcraft-2.40/tests/fake_servers/api.py snapcraft-2.41/tests/fake_servers/api.py --- snapcraft-2.40/tests/fake_servers/api.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/fake_servers/api.py 2018-04-14 12:13:35.000000000 +0000 @@ -435,6 +435,8 @@ details_path = 'details/upload-id/review-snap' elif name == 'test-duplicate-snap': details_path = 'details/upload-id/duplicate-snap' + elif name == 'test-scan-error-with-braces': + details_path = 'details/upload-id/scan-error-with-braces' else: details_path = 'details/upload-id/good-snap' if not request.json_body.get('dry_run', False): @@ -469,6 +471,19 @@ payload = json.dumps({ 'errors': 'Not a valid channel: alpha', }).encode() + elif 'no-permission' in channels: + response_code = 403 + payload = json.dumps({ + 'error_list': [{ + 'code': 'macaroon-permission-required', + 'message': 'Permission is required: channel' + }], + 'permission': 'channel', + 'channels': ['no-permission'], + }).encode() + elif 'bad-channel' in channels: + response_code = 500 + payload = json.dumps({}).encode() elif ( name == 'test-snap' or name.startswith('test-snapcraft')): @@ -702,8 +717,12 @@ # POST, return error error_list = [] for name, value in request.json_body.items(): + if name == 'test-conflict-with-braces': + message = 'value with {braces}' + else: + message = value + '-changed' error_list.append({ - 'message': value + '-changed', + 'message': message, 'code': 'conflict', 'extra': {'name': name}, }) @@ -739,6 +758,9 @@ for e in info]) conflict = any([e.get('filename', '').endswith('conflict') for e in info]) + conflict_with_braces = any( + [e.get('filename', '').endswith('conflict-with-braces') + for e in info]) if invalid: err = {'error_list': [{ 'message': 'Invalid field: icon', @@ -755,6 +777,15 @@ }] payload = json.dumps({'error_list': error_list}).encode('utf8') response_code = 409 + elif conflict_with_braces and request.method == 'POST': + # POST, return error + error_list = [{ + 'message': 'original icon with {braces}', + 'code': 'conflict', + 'extra': {'name': 'icon'}, + }] + payload = json.dumps({'error_list': error_list}).encode('utf8') + response_code = 409 else: updated_info = [] for entry in info: @@ -783,6 +814,18 @@ {'message': 'Duplicate snap already uploaded'}, ] }).encode() + elif snap == 'scan-error-with-braces': + logger.debug('Handling request for scan error with braces') + payload = json.dumps({ + 'code': 'processing_error', + 'url': '/dev/click-apps/5349/rev/1', + 'can_release': False, + 'revision': '1', + 'processed': True, + 'errors': [ + {'message': 'Error message with {braces}'}, + ] + }).encode() else: logger.debug('Handling scan complete request') if snap == 'good-snap': diff -Nru snapcraft-2.40/tests/fixture_setup.py snapcraft-2.41/tests/fixture_setup.py --- snapcraft-2.40/tests/fixture_setup.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/fixture_setup.py 2018-04-14 12:13:35.000000000 +0000 @@ -27,6 +27,7 @@ import subprocess import sys import tempfile +import textwrap import threading import urllib.parse import uuid @@ -134,13 +135,14 @@ def setUp(self): super().setUp() - patcher = mock.patch('snapcraft.ProjectOptions') + patcher = mock.patch('snapcraft.project.Project') patcher.start() self.addCleanup(patcher.stop) # Special handling is required as ProjectOptions attributes are # handled with the @property decorator. - project_options_t = type(snapcraft.ProjectOptions.return_value) + project_options_t = type( + snapcraft.project.Project.return_value) for key in self._kwargs: setattr(project_options_t, key, self._kwargs[key]) @@ -582,9 +584,23 @@ self.check_output_mock.side_effect = self.check_output_side_effect() self.addCleanup(patcher.stop) + self._real_popen = subprocess.Popen + + # Don't over-mock Popen, more things use it than just LXD. + def _fake_popen(*args, **kwargs): + if '/usr/lib/sftp-server' in args[0]: + return self._popen(args[0]) + elif (args[0][:2] == ['lxc', 'exec'] and self.status and + args[0][2] == self.name and args[0][8] == 'sshfs'): + self.files = ['foo', 'bar'] + return self._popen(args[0]) + + # Fall back to the real deal + return self._real_popen(*args, **kwargs) + patcher = mock.patch('subprocess.Popen') self.popen_mock = patcher.start() - self.popen_mock.side_effect = self.check_output_side_effect() + self.popen_mock.side_effect = _fake_popen self.addCleanup(patcher.stop) patcher = mock.patch('time.sleep', lambda _: None) @@ -631,8 +647,6 @@ '"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') @@ -662,9 +676,6 @@ 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) @@ -954,7 +965,7 @@ if version is not None: self.versions.update({version: self}) - def mark_install(self): + def mark_install(self, *, auto_fix=True): if not self.installed: if len(self.dependencies): return @@ -1014,11 +1025,12 @@ self.path = path self.data = { 'name': name, - 'version': version, 'confinement': confinement, 'parts': {}, 'apps': {} } + if version is not None: + self.data['version'] = version if summary is not None: self.data['summary'] = summary if description is not None: @@ -1337,3 +1349,49 @@ open(real_linker, 'w').close() os.symlink(os.path.relpath( real_linker, os.path.dirname(linker_path)), linker_path) + + +class FakeSnapcraftctl(fixtures.Fixture): + + def _setUp(self): + super()._setUp() + + snapcraft_path = os.path.realpath( + os.path.join(os.path.dirname(__file__), '..')) + + tempdir = self.useFixture(fixtures.TempDir()).path + altered_path = '{}:{}'.format(tempdir, os.environ.get('PATH')) + self.useFixture(fixtures.EnvironmentVariable('PATH', altered_path)) + + snapcraftctl_path = os.path.join(tempdir, 'snapcraftctl') + with open(snapcraftctl_path, 'w') as f: + f.write(textwrap.dedent("""\ + #!/usr/bin/env python3 + + # Make sure we can find snapcraft, even if it's not installed + # (like in CI). + import sys + sys.path.append('{snapcraft_path!s}') + + import snapcraft.cli.__main__ + + if __name__ == '__main__': + snapcraft.cli.__main__.run_snapcraftctl( + prog_name='snapcraftctl') + """.format(snapcraft_path=snapcraft_path))) + f.flush() + + os.chmod(snapcraftctl_path, 0o755) + + +class FakeSnapcraftIsASnap(fixtures.Fixture): + + def _setUp(self): + super()._setUp() + + self.useFixture(fixtures.EnvironmentVariable( + 'SNAP', '/snap/snapcraft/current')) + self.useFixture(fixtures.EnvironmentVariable( + 'SNAP_NAME', 'snapcraft')) + self.useFixture(fixtures.EnvironmentVariable( + 'SNAP_VERSION', 'devel')) diff -Nru snapcraft-2.40/tests/integration/general/test_7z_source.py snapcraft-2.41/tests/integration/general/test_7z_source.py --- snapcraft-2.40/tests/integration/general/test_7z_source.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_7z_source.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,36 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017 Tim Süberkrüb -# Copyright (C) 2017-2018 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 FileExists - -from tests import integration - - -class SevenZipTestCase(integration.TestCase): - - _7z_test_files = {'test1.txt', 'test2.txt', 'test3.txt'} - - def test_stage_7z(self): - self.run_snapcraft('stage', '7z-hello') - - for filename in self._7z_test_files: - self.assertThat( - os.path.join(self.stage_dir, filename), - FileExists() - ) diff -Nru snapcraft-2.40/tests/integration/general/test_build_properties.py snapcraft-2.41/tests/integration/general/test_build_properties.py --- snapcraft-2.40/tests/integration/general/test_build_properties.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_build_properties.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,75 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2018 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 yaml - -from testtools.matchers import Equals, FileExists - -from tests import integration - - -class BuildPropertiesTestCase(integration.TestCase): - - def test_build(self): - self.assert_expected_build_state('local-plugin-build-properties') - - def test_build_legacy_build_properties(self): - self.assert_expected_build_state( - 'local-plugin-legacy-build-properties') - - def assert_expected_build_state(self, project_dir): - self.run_snapcraft('build', project_dir) - - state_file = os.path.join( - self.parts_dir, 'x-local-plugin', 'state', 'build') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - - # Verify that the correct schema dependencies made it into the state. - self.assertTrue('foo' in state.schema_properties) - self.assertTrue('stage-packages' in state.schema_properties) - - # Verify that the contents of the dependencies made it in as well. - self.assertTrue('foo' in state.properties) - self.assertTrue('stage-packages' in state.properties) - self.assertThat(state.properties['foo'], Equals('bar')) - self.assertThat(state.properties['stage-packages'], Equals(['curl'])) - - def test_build_with_arch(self): - if self.deb_arch == 'armhf': - self.skipTest('For now, we just support crosscompile from amd64') - self.run_snapcraft(['build', '--target-arch=i386', 'go-hello'], - 'go-hello') - state_file = os.path.join(self.parts_dir, - 'go-hello', 'state', 'build') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - self.assertThat(state.project_options['deb_arch'], Equals('i386')) - - def test_arch_with_build(self): - if self.deb_arch == 'armhf': - self.skipTest('For now, we just support crosscompile from amd64') - self.run_snapcraft(['--target-arch=i386', 'build', 'go-hello'], - 'go-hello') - state_file = os.path.join(self.parts_dir, - 'go-hello', 'state', 'build') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - self.assertThat(state.project_options['deb_arch'], Equals('i386')) diff -Nru snapcraft-2.40/tests/integration/general/test_build.py snapcraft-2.41/tests/integration/general/test_build.py --- snapcraft-2.40/tests/integration/general/test_build.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_build.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017-2018 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 ( - Contains, - Equals, - Not, -) - -from tests import integration - - -class BuildTestCase(integration.TestCase): - - def _build_invalid_part(self, debug): - exception = self.assertRaises( - subprocess.CalledProcessError, - self.run_snapcraft, ['build', 'invalid-part-name'], - 'go-hello', debug=debug) - - self.assertThat(exception.returncode, Equals(2)) - self.assertThat(exception.output, Contains( - "part named 'invalid-part-name' is not defined")) - - return exception.output - - def test_build_invalid_part_no_traceback_without_debug(self): - self.assertThat( - self._build_invalid_part(False), Not(Contains("Traceback"))) - - def test_build_invalid_part_does_traceback_with_debug(self): - self.assertThat( - self._build_invalid_part(True), Contains("Traceback")) diff -Nru snapcraft-2.40/tests/integration/general/test_bzr_source.py snapcraft-2.41/tests/integration/general/test_bzr_source.py --- snapcraft-2.40/tests/integration/general/test_bzr_source.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_bzr_source.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,76 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2018 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 - -from tests import integration - - -class BzrSourceTestCase(integration.BzrSourceBaseTestCase): - - def test_pull_bzr_head(self): - self.copy_project_to_cwd('bzr-head') - - self.init_source_control() - self.commit('"1"', unchanged=True) - self.commit('"2"', unchanged=True) - # test initial branch - self.run_snapcraft('pull') - revno = subprocess.check_output( - ['bzr', 'revno', '-r', '-1', 'parts/bzr/src'], - universal_newlines=True).strip() - self.assertThat(revno, Equals('2')) - # test pull doesn't fail - self.run_snapcraft('pull') - revno = subprocess.check_output( - ['bzr', 'revno', '-r', '-1', 'parts/bzr/src'], - universal_newlines=True).strip() - self.assertThat(revno, Equals('2')) - - def test_pull_bzr_tag(self): - self.copy_project_to_cwd('bzr-tag') - - self.init_source_control() - self.commit('"1"', unchanged=True) - self.commit('"2"', unchanged=True) - subprocess.check_call( - ['bzr', 'tag', '-r', '1', 'initial'], - stderr=subprocess.DEVNULL) - # test initial branch - self.run_snapcraft('pull') - revno = self.get_revno('parts/bzr/src') - self.assertThat(revno, Equals('1')) - # test pull doesn't fail - self.run_snapcraft('pull') - revno = self.get_revno('parts/bzr/src') - self.assertThat(revno, Equals('1')) - - def test_pull_bzr_commit(self): - self.copy_project_to_cwd('bzr-commit') - - self.init_source_control() - self.commit('"1"', unchanged=True) - self.commit('"2"', unchanged=True) - # test initial branch - self.run_snapcraft('pull') - revno = self.get_revno('parts/bzr/src') - self.assertThat(revno, Equals('1')) - # test pull doesn't fail - self.run_snapcraft('pull') - revno = self.get_revno('parts/bzr/src') - self.assertThat(revno, Equals('1')) diff -Nru snapcraft-2.40/tests/integration/general/test_clean_build_step.py snapcraft-2.41/tests/integration/general/test_clean_build_step.py --- snapcraft-2.40/tests/integration/general/test_clean_build_step.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_clean_build_step.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,182 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2018 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, - DirExists, - Equals, - FileExists, - Not -) - -from tests import integration - - -class CleanBuildStepBuiltTestCase(integration.TestCase): - - def setUp(self): - super().setUp() - - self.copy_project_to_cwd('independent-parts') - self.run_snapcraft('build') - self.parts = {} - for part in ['part1', 'part2']: - partdir = os.path.join(self.parts_dir, part) - self.parts[part] = { - 'partdir': partdir, - 'sourcedir': os.path.join(partdir, 'src'), - 'builddir': os.path.join(partdir, 'build'), - 'installdir': os.path.join(partdir, 'install'), - 'bindir': os.path.join(partdir, 'install', 'bin'), - } - - def assert_files_exist(self): - for d in ['builddir', 'bindir']: - self.assertThat(os.path.join(self.parts['part1'][d], 'file1'), - FileExists()) - self.assertThat(os.path.join(self.parts['part2'][d], 'file2'), - FileExists()) - - def test_clean_build_step(self): - self.assert_files_exist() - - output = self.run_snapcraft( - ['clean', '--step=build'], debug=False) - - for part_name, part in self.parts.items(): - self.assertThat(part['builddir'], Not(DirExists())) - self.assertThat(part['installdir'], Not(DirExists())) - self.assertThat(part['sourcedir'], DirExists()) - - # Assert that the priming and staging areas were removed wholesale, not - # a part at a time (since we didn't specify any parts). - self.assertThat(output, Contains("Cleaning up priming area")) - self.assertThat(output, Contains("Cleaning up staging area")) - - output = output.split('\n') - part1_output = [line.strip() for line in output if 'part1' in line] - part2_output = [line.strip() for line in output if 'part2' in line] - self.expectThat(part1_output, Equals([ - 'Skipping cleaning priming area for part1 (already clean)', - 'Skipping cleaning staging area for part1 (already clean)', - 'Cleaning build for part1' - ])) - self.expectThat(part2_output, Equals([ - 'Skipping cleaning priming area for part2 (already clean)', - 'Skipping cleaning staging area for part2 (already clean)', - 'Cleaning build for part2' - ])) - - # Now try to build again - self.run_snapcraft('build') - self.assert_files_exist() - - def test_clean_build_step_single_part(self): - self.assert_files_exist() - - self.run_snapcraft(['clean', 'part1', '--step=build']) - self.assertThat(self.parts['part1']['builddir'], Not(DirExists())) - self.assertThat(self.parts['part1']['installdir'], Not(DirExists())) - self.assertThat(self.parts['part1']['sourcedir'], DirExists()) - - self.assertThat( - os.path.join(self.parts['part2']['builddir'], 'file2'), - FileExists()) - self.assertThat( - os.path.join(self.parts['part2']['bindir'], 'file2'), - FileExists()) - - # Now try to build again - self.run_snapcraft('build') - self.assert_files_exist() - - -class CleanBuildStepPrimedTestCase(integration.TestCase): - - def setUp(self): - super().setUp() - - self.copy_project_to_cwd('independent-parts') - self.run_snapcraft('prime') - - self.snap_bindir = os.path.join(self.prime_dir, 'bin') - self.stage_bindir = os.path.join(self.stage_dir, 'bin') - self.parts = {} - for part in ['part1', 'part2']: - partdir = os.path.join(self.parts_dir, part) - self.parts[part] = { - 'partdir': partdir, - 'sourcedir': os.path.join(partdir, 'src'), - 'builddir': os.path.join(partdir, 'build'), - 'installdir': os.path.join(partdir, 'install'), - 'bindir': os.path.join(partdir, 'install', 'bin'), - } - - def assert_files_exist(self): - for d in ['builddir', 'bindir']: - self.assertThat(os.path.join(self.parts['part1'][d], 'file1'), - FileExists()) - self.assertThat(os.path.join(self.parts['part2'][d], 'file2'), - FileExists()) - - self.assertThat(os.path.join(self.snap_bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) - self.assertThat(os.path.join(self.stage_bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) - - def test_clean_build_step(self): - self.assert_files_exist() - - self.run_snapcraft(['clean', '--step=build']) - self.assertThat(self.stage_dir, Not(DirExists())) - self.assertThat(self.prime_dir, Not(DirExists())) - - for part_name, part in self.parts.items(): - self.assertThat(part['builddir'], Not(DirExists())) - self.assertThat(part['installdir'], Not(DirExists())) - self.assertThat(part['sourcedir'], DirExists()) - - # Now try to prime again - self.run_snapcraft('prime') - self.assert_files_exist() - - def test_clean_build_step_single_part(self): - self.assert_files_exist() - - self.run_snapcraft(['clean', 'part1', '--step=build']) - self.assertThat(os.path.join(self.stage_bindir, 'file1'), - Not(FileExists())) - self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) - self.assertThat(os.path.join(self.snap_bindir, 'file1'), - Not(FileExists())) - self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) - - self.assertThat(self.parts['part1']['builddir'], Not(DirExists())) - self.assertThat(self.parts['part1']['installdir'], Not(DirExists())) - self.assertThat(self.parts['part1']['sourcedir'], DirExists()) - - self.assertThat( - os.path.join(self.parts['part2']['builddir'], 'file2'), - FileExists()) - self.assertThat( - os.path.join(self.parts['part2']['bindir'], 'file2'), - FileExists()) - - # Now try to prime again - self.run_snapcraft('prime') - self.assert_files_exist() diff -Nru snapcraft-2.40/tests/integration/general/test_clean_dependents.py snapcraft-2.41/tests/integration/general/test_clean_dependents.py --- snapcraft-2.40/tests/integration/general/test_clean_dependents.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_clean_dependents.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,136 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2018 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 ( - DirExists, - Not -) - -from tests import integration - - -class CleanDependentsTestCase(integration.TestCase): - - def setUp(self): - super().setUp() - - self.copy_project_to_cwd('dependencies') - self.run_snapcraft('prime') - - # Need to use the state directory here instead of partdir due to - # bug #1567054. - self.part_dirs = { - 'p1': os.path.join(self.parts_dir, 'p1', 'state'), - 'p2': os.path.join(self.parts_dir, 'p2', 'state'), - 'p3': os.path.join(self.parts_dir, 'p3', 'state'), - 'p4': os.path.join(self.parts_dir, 'p4', 'state'), - } - - def assert_clean(self, parts, common=False): - for part in parts: - self.expectThat( - self.part_dirs[part], Not(DirExists()), - 'Expected part directory for {!r} to be cleaned'.format(part)) - - if common: - self.expectThat(self.parts_dir, Not(DirExists()), - 'Expected parts/ directory to be cleaned') - self.expectThat(self.stage_dir, Not(DirExists()), - 'Expected stage/ directory to be cleaned') - self.expectThat(self.prime_dir, Not(DirExists()), - 'Expected snap/ directory to be cleaned') - - def assert_not_clean(self, parts, common=False): - for part in parts: - self.expectThat( - self.part_dirs[part], DirExists(), - 'Expected part directory for {!r} to be uncleaned'.format( - part)) - - if common: - self.expectThat(self.parts_dir, DirExists(), - 'Expected parts/ directory to be uncleaned') - self.expectThat(self.stage_dir, DirExists(), - 'Expected stage/ directory to be uncleaned') - self.expectThat(self.prime_dir, DirExists(), - 'Expected snap/ directory to be uncleaned') - - def test_clean_nested_dependent(self): - # Test that p3 (which has dependencies but no dependents) cleans with - # no extra parameters. - self.run_snapcraft(['clean', 'p3']) - self.assert_clean(['p3']) - self.assert_not_clean(['p1', 'p2', 'p4'], True) - - # Now run prime again - self.run_snapcraft('prime') - self.assert_not_clean(['p1', 'p2', 'p3', 'p4'], True) - - def test_clean_dependent(self): - # Test that p2 (which has both dependencies and dependents) cleans with - # its dependents (p3 and p4) also specified. - self.run_snapcraft(['clean', 'p2', 'p3', 'p4']) - self.assert_clean(['p2', 'p3', 'p4']) - self.assert_not_clean(['p1'], True) - - # Now run prime again - self.run_snapcraft('prime') - self.assert_not_clean(['p1', 'p2', 'p3', 'p4'], True) - - def test_clean_main(self): - # Test that p1 (which has no dependencies but dependents) cleans with - # its dependents (p2 and, as an extension, p3 and p4) also specified. - self.run_snapcraft(['clean', 'p1', 'p2', 'p3', 'p4']) - self.assert_clean(['p1', 'p2', 'p3', 'p4'], True) - - # Now run prime again - self.run_snapcraft('prime') - self.assert_not_clean(['p1', 'p2', 'p3', 'p4'], True) - - def test_clean_dependent_without_nested_dependents(self): - # Test that p2 (which has both dependencies and dependents) - # cleans its dependents (p3 and p4) are not specified - self.run_snapcraft(['clean', 'p2']) - self.assert_not_clean(['p1'], False) - self.assert_clean(['p2', 'p3', 'p4'], False) - - def test_clean_dependent_without_nested_dependent(self): - # Test that p2 (which has both dependencies and dependents) - # cleans its dependents (p4) is not specified - self.run_snapcraft(['clean', 'p2', 'p3']) - self.assert_not_clean(['p1'], False) - self.assert_clean(['p2', 'p3', 'p4'], False) - - def test_clean_main_without_any_dependent(self): - # Test that p1 (which has no dependencies but dependents) - # cleans if none of its dependents are also specified. - self.run_snapcraft(['clean', 'p1']) - self.assert_clean(['p1', 'p2', 'p3', 'p4'], True) - - def test_clean_main_without_dependent(self): - # Test that p1 (which has no dependencies but dependents) - # cleans if its dependent (p2) is not specified - self.run_snapcraft(['clean', 'p1', 'p3', 'p4']) - self.assert_clean(['p1', 'p2', 'p3', 'p4'], True) - - def test_clean_main_without_nested_dependent(self): - # Test that p1 (which has no dependencies but dependents) - # cleans if its nested dependent (p3, by way of p2) is not - # specified - self.run_snapcraft(['clean', 'p1', 'p2']) - self.assert_clean(['p1', 'p2', 'p3', 'p4'], True) diff -Nru snapcraft-2.40/tests/integration/general/test_clean_prime_step.py snapcraft-2.41/tests/integration/general/test_clean_prime_step.py --- snapcraft-2.40/tests/integration/general/test_clean_prime_step.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_clean_prime_step.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,88 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2018 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, - DirExists, - FileExists, - Not -) - -from tests import integration - - -class CleanPrimeStepTestCase(integration.TestCase): - - def setUp(self): - super().setUp() - - self.copy_project_to_cwd('independent-parts') - self.run_snapcraft('prime') - - def test_clean_prime_step(self): - bindir = os.path.join(self.prime_dir, 'bin') - self.assertThat(os.path.join(bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(bindir, 'file2'), FileExists()) - - output = self.run_snapcraft( - ['clean', '--step=prime'], debug=False) - self.assertThat(self.prime_dir, Not(DirExists())) - self.assertThat(self.stage_dir, DirExists()) - self.assertThat(self.parts_dir, DirExists()) - - # Assert that the priming area was removed wholesale, not a part at a - # time (since we didn't specify any parts). - self.assertThat(output, Contains("Cleaning up priming area")) - self.expectThat(output, Not(Contains('part1'))) - self.expectThat(output, Not(Contains('part2'))) - - # Now try to prime again - self.run_snapcraft('prime') - self.assertThat(os.path.join(bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(bindir, 'file2'), FileExists()) - - def test_clean_prime_step_single_part(self): - bindir = os.path.join(self.prime_dir, 'bin') - self.assertThat(os.path.join(bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(bindir, 'file2'), FileExists()) - - self.run_snapcraft(['clean', 'part1', '--step=prime']) - self.assertThat(os.path.join(bindir, 'file1'), Not(FileExists())) - self.assertThat(os.path.join(bindir, 'file2'), FileExists()) - self.assertThat(self.stage_dir, DirExists()) - self.assertThat(self.parts_dir, DirExists()) - - # Now try to prime again - self.run_snapcraft('prime') - self.assertThat(os.path.join(bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(bindir, 'file2'), FileExists()) - - def test_clean_with_deprecated_strip_step(self): - bindir = os.path.join(self.prime_dir, 'bin') - self.assertThat(os.path.join(bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(bindir, 'file2'), FileExists()) - - self.run_snapcraft(['clean', '--step=strip']) - self.assertThat(self.prime_dir, Not(DirExists())) - self.assertThat(self.stage_dir, DirExists()) - self.assertThat(self.parts_dir, DirExists()) - - # Now try to prime again - self.run_snapcraft('prime') - self.assertThat(os.path.join(bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(bindir, 'file2'), FileExists()) diff -Nru snapcraft-2.40/tests/integration/general/test_clean_pull_step.py snapcraft-2.41/tests/integration/general/test_clean_pull_step.py --- snapcraft-2.40/tests/integration/general/test_clean_pull_step.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_clean_pull_step.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,154 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2018 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 ( - DirExists, - Equals, - FileExists, - Not -) - -from tests import integration - - -class CleanPullStepPulledTestCase(integration.TestCase): - - def setUp(self): - super().setUp() - - self.copy_project_to_cwd('independent-parts') - self.run_snapcraft('pull') - self.part1_sourcedir = os.path.join(self.parts_dir, 'part1', 'src') - self.part2_sourcedir = os.path.join(self.parts_dir, 'part2', 'src') - - def assert_files_exist(self): - self.assertThat(os.path.join(self.part1_sourcedir, 'file1'), - FileExists()) - self.assertThat(os.path.join(self.part2_sourcedir, 'file2'), - FileExists()) - - def test_clean_pull_step(self): - self.assert_files_exist() - - self.run_snapcraft(['clean', '--step=pull']) - self.assertThat(self.part1_sourcedir, Not(DirExists())) - self.assertThat(self.part2_sourcedir, Not(DirExists())) - - # Now try to pull again - self.run_snapcraft('pull') - self.assert_files_exist() - - def test_clean_pull_step_single_part(self): - self.assert_files_exist() - - self.run_snapcraft(['clean', 'part1', '--step=pull']) - self.assertThat(self.part1_sourcedir, Not(DirExists())) - self.assertThat(os.path.join(self.part2_sourcedir, 'file2'), - FileExists()) - - # Now try to pull again - self.run_snapcraft('pull') - self.assert_files_exist() - - -class CleanPullStepPrimedTestCase(integration.TestCase): - - def setUp(self): - super().setUp() - - self.copy_project_to_cwd('independent-parts') - self.run_snapcraft('prime') - - self.snap_bindir = os.path.join(self.prime_dir, 'bin') - self.stage_bindir = os.path.join(self.stage_dir, 'bin') - self.parts = {} - for part in ['part1', 'part2']: - partdir = os.path.join(self.parts_dir, part) - self.parts[part] = { - 'partdir': partdir, - 'sourcedir': os.path.join(partdir, 'src'), - 'builddir': os.path.join(partdir, 'build'), - 'installdir': os.path.join(partdir, 'install'), - 'bindir': os.path.join(partdir, 'install', 'bin'), - } - - def assert_files_exist(self): - for d in ['builddir', 'bindir', 'sourcedir']: - self.assertThat(os.path.join(self.parts['part1'][d], 'file1'), - FileExists()) - self.assertThat(os.path.join(self.parts['part2'][d], 'file2'), - FileExists()) - - self.assertThat(os.path.join(self.snap_bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) - self.assertThat(os.path.join(self.stage_bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) - - def test_clean_pull_step(self): - self.assert_files_exist() - - output = self.run_snapcraft(['clean', '--step=pull'], debug=False) - self.assertThat(self.stage_dir, Not(DirExists())) - self.assertThat(self.prime_dir, Not(DirExists())) - - for part_name, part in self.parts.items(): - self.assertThat(part['builddir'], Not(DirExists())) - self.assertThat(part['installdir'], Not(DirExists())) - self.assertThat(part['sourcedir'], Not(DirExists())) - - # Assert that the priming and staging areas were removed wholesale, not - # a part at a time (since we didn't specify any parts). - output = output.strip().split('\n') - self.expectThat(output, Equals([ - 'Cleaning up priming area', - 'Cleaning up staging area', - 'Cleaning up parts directory' - ])) - - # Now try to prime again - self.run_snapcraft('prime') - self.assert_files_exist() - - def test_clean_pull_step_single_part(self): - self.assert_files_exist() - - self.run_snapcraft(['clean', 'part1', '--step=pull']) - self.assertThat(os.path.join(self.stage_bindir, 'file1'), - Not(FileExists())) - self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) - self.assertThat(os.path.join(self.snap_bindir, 'file1'), - Not(FileExists())) - self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) - - self.assertThat(self.parts['part1']['builddir'], Not(DirExists())) - self.assertThat(self.parts['part1']['installdir'], Not(DirExists())) - self.assertThat(self.parts['part1']['sourcedir'], Not(DirExists())) - - self.assertThat( - os.path.join(self.parts['part2']['builddir'], 'file2'), - FileExists()) - self.assertThat( - os.path.join(self.parts['part2']['bindir'], 'file2'), - FileExists()) - self.assertThat( - os.path.join(self.parts['part2']['sourcedir'], 'file2'), - FileExists()) - - # Now try to prime again - self.run_snapcraft('prime') - self.assert_files_exist() diff -Nru snapcraft-2.40/tests/integration/general/test_clean.py snapcraft-2.41/tests/integration/general/test_clean.py --- snapcraft-2.40/tests/integration/general/test_clean.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_clean.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,77 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2018 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 ( - Contains, - DirExists, - Equals, - Not -) - -from tests import integration - - -class CleanTestCase(integration.TestCase): - - def test_clean(self): - self.copy_project_to_cwd('make-hello') - self.run_snapcraft('snap') - - snap_dirs = (self.parts_dir, self.stage_dir, self.prime_dir) - for dir_ in snap_dirs: - self.assertThat(dir_, DirExists()) - - self.run_snapcraft('clean') - for dir_ in snap_dirs: - self.assertThat(dir_, Not(DirExists())) - - def _clean_invalid_part(self, debug): - self.copy_project_to_cwd('make-hello') - self.run_snapcraft('snap') - - raised = self.assertRaises( - subprocess.CalledProcessError, self.run_snapcraft, - ['clean', 'invalid-part'], debug=debug) - self.assertThat(raised.returncode, Equals(2)) - self.assertThat( - raised.output, - Contains("The part named 'invalid-part' is not defined")) - return raised.output - - def test_clean_invalid_part_no_traceback_without_debug(self): - self.assertThat( - self._clean_invalid_part(False), Not(Contains("Traceback"))) - - def test_clean_invalid_part_traceback_with_debug(self): - self.assertThat( - self._clean_invalid_part(True), Contains("Traceback")) - - def test_clean_again(self): - # Clean a second time doesn't fail. - # Regression test for https://bugs.launchpad.net/snapcraft/+bug/1497371 - self.copy_project_to_cwd('make-hello') - self.run_snapcraft('snap') - self.run_snapcraft('clean') - self.run_snapcraft('clean') - - # Regression test for LP: #1596596 - def test_clean_invalid_yaml(self): - self.run_snapcraft('clean', 'invalid-snap') - self.assertThat(self.parts_dir, Not(DirExists())) - self.assertThat(self.stage_dir, Not(DirExists())) - self.assertThat(self.prime_dir, Not(DirExists())) diff -Nru snapcraft-2.40/tests/integration/general/test_clean_stage_step.py snapcraft-2.41/tests/integration/general/test_clean_stage_step.py --- snapcraft-2.40/tests/integration/general/test_clean_stage_step.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_clean_stage_step.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,117 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2018 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, - DirExists, - FileExists, - Not -) - -from tests import integration - - -class CleanStageStepStagedTestCase(integration.TestCase): - - def setUp(self): - super().setUp() - - self.copy_project_to_cwd('independent-parts') - self.run_snapcraft('stage') - self.bindir = os.path.join(self.stage_dir, 'bin') - - def assert_files_exist(self): - self.assertThat(os.path.join(self.bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(self.bindir, 'file2'), FileExists()) - - def test_clean_stage_step(self): - self.assert_files_exist() - - output = self.run_snapcraft( - ['clean', '--step=stage'], debug=False) - self.assertThat(self.stage_dir, Not(DirExists())) - self.assertThat(self.parts_dir, DirExists()) - - # Assert that the priming and staging areas were removed wholesale, not - # a part at a time (since we didn't specify any parts). - self.assertThat(output, Contains("Cleaning up priming area")) - self.assertThat(output, Contains("Cleaning up staging area")) - self.expectThat(output, Not(Contains('part1'))) - self.expectThat(output, Not(Contains('part2'))) - - # Now try to stage again - self.run_snapcraft('stage') - self.assert_files_exist() - - def test_clean_stage_step_single_part(self): - self.assert_files_exist() - - self.run_snapcraft(['clean', 'part1', '--step=stage']) - self.assertThat(os.path.join(self.bindir, 'file1'), Not(FileExists())) - self.assertThat(os.path.join(self.bindir, 'file2'), FileExists()) - self.assertThat(self.parts_dir, DirExists()) - - # Now try to stage again - self.run_snapcraft('stage') - self.assert_files_exist() - - -class CleanStageStepPrimedTestCase(integration.TestCase): - - def setUp(self): - super().setUp() - - self.copy_project_to_cwd('independent-parts') - self.run_snapcraft('prime') - - self.snap_bindir = os.path.join(self.prime_dir, 'bin') - self.stage_bindir = os.path.join(self.stage_dir, 'bin') - - def assert_files_exist(self): - self.assertThat(os.path.join(self.snap_bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) - self.assertThat(os.path.join(self.stage_bindir, 'file1'), FileExists()) - self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) - - def test_clean_stage_step(self): - self.assert_files_exist() - - self.run_snapcraft(['clean', '--step=stage']) - self.assertThat(self.stage_dir, Not(DirExists())) - self.assertThat(self.prime_dir, Not(DirExists())) - self.assertThat(self.parts_dir, DirExists()) - - # Now try to prime again - self.run_snapcraft('prime') - self.assert_files_exist() - - def test_clean_stage_step_single_part(self): - self.assert_files_exist() - - self.run_snapcraft(['clean', 'part1', '--step=stage']) - self.assertThat(os.path.join(self.stage_bindir, 'file1'), - Not(FileExists())) - self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) - self.assertThat(os.path.join(self.snap_bindir, 'file1'), - Not(FileExists())) - self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) - self.assertThat(self.parts_dir, DirExists()) - - # Now try to prime again - self.run_snapcraft('prime') - self.assert_files_exist() diff -Nru snapcraft-2.40/tests/integration/general/test_deb_source.py snapcraft-2.41/tests/integration/general/test_deb_source.py --- snapcraft-2.40/tests/integration/general/test_deb_source.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_deb_source.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,50 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2018 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 ( - Equals, - FileExists -) - -from tests import integration - - -class DebSourceTestCase(integration.TestCase): - - def test_stage_deb(self): - self.copy_project_to_cwd('deb-hello') - self.run_snapcraft(['stage', 'deb']) - - self.assertThat( - os.path.join(self.stage_dir, 'bin', 'hello'), - FileExists()) - self.assertThat( - os.path.join(self.stage_dir, 'usr', 'bin', 'world'), - FileExists()) - - # Regression test for LP: #1634813 - def test_stage_deb_with_symlink(self): - self.copy_project_to_cwd('deb-with-symlink') - self.run_snapcraft(['stage', 'deb-with-symlink']) - - target = os.path.join(self.stage_dir, 'target') - symlink = os.path.join(self.stage_dir, 'symlink') - self.assertThat(target, FileExists()) - self.assertThat(symlink, FileExists()) - self.assertTrue(os.path.islink(symlink)) - self.assertThat(os.readlink(symlink), Equals('target')) diff -Nru snapcraft-2.40/tests/integration/general/test_elf.py snapcraft-2.41/tests/integration/general/test_elf.py --- snapcraft-2.40/tests/integration/general/test_elf.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_elf.py 2018-04-14 12:13:35.000000000 +0000 @@ -106,8 +106,9 @@ snapcraft_yaml.update_part('test-part', { 'plugin': 'nil', 'build-attributes': attributes, - 'build': ('/usr/sbin/execstack --set-execstack ' - '$SNAPCRAFT_PART_INSTALL/usr/bin/hello'), + 'build': ( + 'execstack --set-execstack ' + '$SNAPCRAFT_PART_INSTALL/usr/bin/hello'), 'prime': ['usr/bin/hello'], 'build-packages': ['execstack'], 'stage-packages': ['hello'], diff -Nru snapcraft-2.40/tests/integration/general/test_git_source.py snapcraft-2.41/tests/integration/general/test_git_source.py --- snapcraft-2.40/tests/integration/general/test_git_source.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_git_source.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,167 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2018 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 shutil -from textwrap import dedent - -from testtools.matchers import Contains, Equals, FileExists - -from tests import integration - - -class GitSourceTestCase(integration.GitSourceBaseTestCase): - - def _get_git_revno(self, path, revrange='-1'): - return subprocess.check_output( - 'git -C {} log {} --oneline | cut -d\' \' -f2'.format( - path, revrange), - shell=True, universal_newlines=True).strip() - - def test_pull_git_head(self): - self.copy_project_to_cwd('git-head') - - self.init_source_control() - self.commit('"1"', allow_empty=True) - self.commit('"2"', allow_empty=True) - - self.run_snapcraft('pull') - revno = self._get_git_revno('parts/git/src') - self.assertThat(revno, Equals('"2"')) - - self.run_snapcraft('pull') - revno = self._get_git_revno('parts/git/src') - self.assertThat(revno, Equals('"2"')) - - def test_pull_git_tag(self): - self.copy_project_to_cwd('git-tag') - - self.init_source_control() - self.commit('"1"', allow_empty=True) - self.commit('"2"', allow_empty=True) - subprocess.check_call( - ['git', 'tag', 'initial', 'HEAD@{1}'], - stdout=subprocess.DEVNULL) - - self.run_snapcraft('pull') - revno = self._get_git_revno('parts/git/src') - self.assertThat(revno, Equals('"1"')) - - self.run_snapcraft('pull') - revno = self._get_git_revno('parts/git/src') - self.assertThat(revno, Equals('"1"')) - - def test_pull_git_commit(self): - self.copy_project_to_cwd('git-commit') - - self.init_source_control() - self.commit('"1"', allow_empty=True) - self.commit('"2"', allow_empty=True) - - # The test uses "HEAD^" so we can only test it once - self.run_snapcraft('pull') - revno = self._get_git_revno('parts/git/src') - self.assertThat(revno, Equals('"1"')) - - def test_pull_git_branch(self): - self.copy_project_to_cwd('git-branch') - - self.init_source_control() - self.commit('"1"', allow_empty=True) - self.commit('"2"', allow_empty=True) - subprocess.check_call( - ['git', 'branch', 'second', 'HEAD@{1}'], - stdout=subprocess.DEVNULL) - subprocess.check_call( - ['git', 'checkout', 'second'], - stderr=subprocess.DEVNULL) - self.commit('"3"', allow_empty=True) - subprocess.check_call( - ['git', 'checkout', 'master'], - stderr=subprocess.DEVNULL) - - self.run_snapcraft('pull') - revno = self._get_git_revno('parts/git/src', revrange='-2') - self.assertThat(revno, Equals('"3"\n"1"')) - - self.run_snapcraft('pull') - revno = self._get_git_revno('parts/git/src', revrange='-2') - self.assertThat(revno, Equals('"3"\n"1"')) - - def test_pull_git_with_depth(self): - """Regression test for LP: #1627772.""" - self.copy_project_to_cwd('git-depth') - - self.init_source_control() - self.commit('"1"', allow_empty=True) - self.commit('"2"', allow_empty=True) - - self.run_snapcraft('pull') - - -class GitGenerateVersionTestCase(integration.GitSourceBaseTestCase): - - def setUp(self): - super().setUp() - self.init_source_control() - os.mkdir('snap') - - with open(os.path.join('snap', 'snapcraft.yaml'), 'w') as f: - print(dedent("""\ - name: git-test - version: git - summary: test git generated version - description: test git generated version with git hint - architectures: [amd64] - parts: - nil: - plugin: nil - """), file=f) - - self.add_file(os.path.join('snap', 'snapcraft.yaml')) - self.commit('snapcraft.yaml added') - - def test_tag(self): - self.tag('2.0') - self.run_snapcraft('snap') - self.assertThat('git-test_2.0_amd64.snap', FileExists()) - - def test_tag_with_commits_ahead(self): - self.tag('2.0') - open('stub_file', 'w').close() - self.add_file('stub_file') - self.commit('new stub file') - self.run_snapcraft('snap') - revno = self.get_revno()[:7] - expected_file = 'git-test_2.0+git1.{}_amd64.snap'.format(revno) - self.assertThat(expected_file, FileExists()) - - def test_no_tag(self): - self.run_snapcraft('snap') - revno = self.get_revno()[:7] - expected_file = 'git-test_0+git.{}_amd64.snap'.format(revno) - self.assertThat(expected_file, FileExists()) - - def test_no_git(self): - shutil.rmtree('.git') - - exception = self.assertRaises( - subprocess.CalledProcessError, self.run_snapcraft, ['snap']) - self.assertThat( - exception.output, - Contains('fatal: Not a git repository (or any of the parent ' - 'directories): .git')) diff -Nru snapcraft-2.40/tests/integration/general/test_hg_source.py snapcraft-2.41/tests/integration/general/test_hg_source.py --- snapcraft-2.40/tests/integration/general/test_hg_source.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_hg_source.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,115 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2018 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 testtools.matchers import Equals, FileExists - -from tests import integration - - -class HgSourceTestCase(integration.HgSourceBaseTestCase): - - def test_pull_hg_head(self): - self.copy_project_to_cwd('hg-head') - - self.init_source_control() - open('1', 'w').close() - self.commit('1', '1') - open('2', 'w').close() - self.commit('2', '2') - - self.run_snapcraft('pull') - revno = self.get_revno( - os.path.join(self.parts_dir, 'mercurial', 'src')) - self.assertThat(revno, Equals('"2"')) - - self.run_snapcraft('pull') - revno = self.get_revno( - os.path.join(self.parts_dir, 'mercurial', 'src')) - self.assertThat(revno, Equals('"2"')) - - def test_pull_hg_tag(self): - self.copy_project_to_cwd('hg-tag') - - self.init_source_control() - open('1', 'w').close() - self.commit('1', '1') - subprocess.check_call( - ['hg', 'tag', 'initial', '--user', '"Example Dev"']) - open('2', 'w').close() - self.commit('2', '2') - - self.run_snapcraft('pull') - revno = subprocess.check_output( - 'ls -1 {} | wc -l '.format( - os.path.join(self.parts_dir, 'mercurial', 'src')), - shell=True, universal_newlines=True).strip() - self.assertThat(revno, Equals('1')) - - self.run_snapcraft('pull') - revno = subprocess.check_output( - 'ls -1 {} | wc -l '.format( - os.path.join(self.parts_dir, 'mercurial', 'src')), - shell=True, universal_newlines=True).strip() - self.assertThat(revno, Equals('1')) - - def test_pull_hg_commit(self): - self.copy_project_to_cwd('hg-commit') - - self.init_source_control() - open('1', 'w').close() - self.commit('1', '1') - open('2', 'w').close() - self.commit('2', '2') - - self.run_snapcraft('pull') - revno = subprocess.check_output( - 'ls -1 {} | wc -l '.format( - os.path.join(self.parts_dir, 'mercurial', 'src')), - shell=True, universal_newlines=True).strip() - self.assertThat(revno, Equals('1')) - - self.run_snapcraft('pull') - revno = subprocess.check_output( - 'ls -1 {} | wc -l '.format( - os.path.join(self.parts_dir, 'mercurial', 'src')), - shell=True, universal_newlines=True).strip() - self.assertThat(revno, Equals('1')) - - def test_pull_hg_branch(self): - self.copy_project_to_cwd('hg-branch') - - self.init_source_control() - subprocess.check_call( - ['hg', 'branch', 'second'], stdout=subprocess.DEVNULL) - open('second', 'w').close() - self.commit('second', 'second') - subprocess.check_call( - ['hg', 'branch', 'default'], stdout=subprocess.DEVNULL) - open('default', 'w').close() - self.commit('default', 'default') - - self.run_snapcraft('pull') - self.assertThat( - os.path.join(self.parts_dir, 'mercurial', 'src', 'second'), - FileExists()) - - self.run_snapcraft('pull') - self.assertThat( - os.path.join(self.parts_dir, 'mercurial', 'src', 'second'), - FileExists()) diff -Nru snapcraft-2.40/tests/integration/general/test_local_source.py snapcraft-2.41/tests/integration/general/test_local_source.py --- snapcraft-2.40/tests/integration/general/test_local_source.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_local_source.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,83 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2018 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 -from testtools.matchers import FileExists - -from tests import integration - - -class LocalSourceTestCase(integration.TestCase): - - def test_build_local_source(self): - self.run_snapcraft('build', 'local-source') - - self.assertThat( - os.path.join( - self.parts_dir, 'make-project', 'build', 'stamp-all'), - FileExists()) - - def test_stage_local_source(self): - self.run_snapcraft('stage', 'local-source') - - self.assertThat( - os.path.join( - self.parts_dir, 'make-project', 'build', - 'stamp-install'), - FileExists()) - - -class LocalSourceTypeTestCase(integration.TestCase): - - def test_build_local_source(self): - self.run_snapcraft('build', 'local-source-type') - - self.assertThat( - os.path.join( - self.parts_dir, 'make-project', 'build', 'stamp-all'), - FileExists()) - - def test_stage_local_source(self): - self.run_snapcraft('stage', 'local-source') - - self.assertThat( - os.path.join( - self.parts_dir, 'make-project', 'build', - 'stamp-install'), - FileExists()) - - -class LocalSourceSubfoldersTestCase( - testscenarios.WithScenarios, integration.TestCase): - - scenarios = [ - ('Top folder', - {'subfolder': '.'}), - ('Sub folder Level 1', - {'subfolder': 'packaging'}), - ('Sub folder Level 2', - {'subfolder': os.path.join('packaging', 'snap-package')}), - ('Sub folder Level 3', - {'subfolder': os.path.join( - 'packaging', 'snap-package', 'yes-really-deep')}), - ] - - def test_pull_local_source(self): - self.copy_project_to_cwd('local-source-subfolders') - os.chdir(self.subfolder) - self.run_snapcraft('pull') diff -Nru snapcraft-2.40/tests/integration/general/test_metadata_setuppy.py snapcraft-2.41/tests/integration/general/test_metadata_setuppy.py --- snapcraft-2.40/tests/integration/general/test_metadata_setuppy.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_metadata_setuppy.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,105 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 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 yaml +from textwrap import dedent + +from testtools.matchers import Equals + +from tests import integration, fixture_setup + + +class SetupPyMetadataTestCase(integration.TestCase): + + def setUp(self): + super().setUp() + + with open('setup.py', 'w') as setup_file: + print(dedent("""\ + from setuptools import setup + + setup( + name='hello-world', + version='test-setuppy-version', + description='test-setuppy-description', + author='Canonical LTD', + author_email='snapcraft@lists.snapcraft.io', + scripts=['hello'] + ) + """), file=setup_file) + + def test_metadata_extracted_from_setuppy(self): + snapcraft_yaml = fixture_setup.SnapcraftYaml( + self.path, version=None, description=None) + snapcraft_yaml.data['adopt-info'] = 'test-part' + snapcraft_yaml.update_part( + 'test-part', { + 'plugin': 'nil', + 'parse-info': ['setup.py']}) + self.useFixture(snapcraft_yaml) + + self.run_snapcraft('prime') + snap_yaml_path = os.path.join('prime', 'meta', 'snap.yaml') + with open(snap_yaml_path) as snap_yaml_file: + snap_yaml = yaml.load(snap_yaml_file) + + self.assertThat(snap_yaml['version'], + Equals('test-setuppy-version')) + self.assertThat(snap_yaml['description'], + Equals('test-setuppy-description')) + + def test_all_metadata_from_yaml(self): + snapcraft_yaml = fixture_setup.SnapcraftYaml( + self.path, version='test-yaml-version', + description='test-yaml-description') + snapcraft_yaml.data['adopt-info'] = 'test-part' + snapcraft_yaml.update_part( + 'test-part', { + 'plugin': 'dump', + 'parse-info': ['setup.py']}) + self.useFixture(snapcraft_yaml) + + self.run_snapcraft('prime') + snap_yaml_path = os.path.join('prime', 'meta', 'snap.yaml') + with open(snap_yaml_path) as snap_yaml_file: + snap_yaml = yaml.load(snap_yaml_file) + + self.assertThat(snap_yaml['version'], + Equals('test-yaml-version')) + self.assertThat(snap_yaml['description'], + Equals('test-yaml-description')) + + def test_version_metadata_from_yaml_description_collected(self): + snapcraft_yaml = fixture_setup.SnapcraftYaml( + self.path, version='test-yaml-version', + description=None) + snapcraft_yaml.data['adopt-info'] = 'test-part' + snapcraft_yaml.update_part( + 'test-part', { + 'plugin': 'dump', + 'parse-info': ['setup.py']}) + self.useFixture(snapcraft_yaml) + + self.run_snapcraft('prime') + snap_yaml_path = os.path.join('prime', 'meta', 'snap.yaml') + with open(snap_yaml_path) as snap_yaml_file: + snap_yaml = yaml.load(snap_yaml_file) + + self.assertThat(snap_yaml['version'], + Equals('test-yaml-version')) + self.assertThat(snap_yaml['description'], + Equals('test-setuppy-description')) diff -Nru snapcraft-2.40/tests/integration/general/test_prime.py snapcraft-2.41/tests/integration/general/test_prime.py --- snapcraft-2.40/tests/integration/general/test_prime.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_prime.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,217 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2018 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 textwrap import dedent - -import fixtures -import testscenarios -from testtools.matchers import ( - Contains, - Equals, - FileContains, - FileExists, - MatchesRegex, - Not, - StartsWith, -) - -from tests import integration, fixture_setup - - -class PrimeTestCase(integration.TestCase): - - def test_classic_confinement(self): - if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': - self.skipTest("The autopkgtest armhf runners can't install snaps") - project_dir = 'classic-build' - - # The first run should fail as the environment variable is not - # set but we can only test this on clean systems. - if not os.path.exists(os.path.join( - os.path.sep, 'snap', 'core', 'current')): - try: - self.run_snapcraft(['prime'], project_dir) - except subprocess.CalledProcessError: - pass - else: - self.fail( - 'This should fail as SNAPCRAFT_SETUP_CORE is not set') - - # Now we set the required environment variable - self.useFixture(fixtures.EnvironmentVariable( - 'SNAPCRAFT_SETUP_CORE', '1')) - - self.run_snapcraft(['prime'], project_dir) - - bin_path = os.path.join(self.prime_dir, 'bin', 'hello-classic') - self.assertThat(bin_path, FileExists()) - - interpreter = subprocess.check_output([ - self.patchelf_command, '--print-interpreter', bin_path]).decode() - expected_interpreter = r'^/snap/core/current/.*' - self.assertThat(interpreter, MatchesRegex(expected_interpreter)) - - # We check stage to make sure the hard link is broken. - staged_bin_path = os.path.join(self.stage_dir, 'bin', 'hello-classic') - self.assertThat(staged_bin_path, FileExists()) - - staged_interpreter = subprocess.check_output([ - self.patchelf_command, '--print-interpreter', - staged_bin_path]).decode() - self.assertThat(staged_interpreter, MatchesRegex(r'^/lib.*')) - - def test_classic_confinement_patchelf_disabled(self): - if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': - self.skipTest("The autopkgtest armhf runners can't install snaps") - project_dir = 'classic-build' - - # Now we set the required environment variable - self.useFixture(fixtures.EnvironmentVariable( - 'SNAPCRAFT_SETUP_CORE', '1')) - - self.copy_project_to_cwd(project_dir) - - # Create a new snapcraft.yaml - snapcraft_yaml = fixture_setup.SnapcraftYaml( - self.path, confinement='classic') - snapcraft_yaml.update_part('hello', { - 'source': '.', - 'plugin': 'make', - 'build-attributes': ['no-patchelf'] - }) - self.useFixture(snapcraft_yaml) - - self.run_snapcraft('prime') - - bin_path = os.path.join(self.prime_dir, 'bin', 'hello-classic') - self.assertThat(bin_path, FileExists()) - - interpreter = subprocess.check_output([ - self.patchelf_command, '--print-interpreter', bin_path]).decode() - self.assertThat(interpreter, StartsWith('/lib')) - - def test_classic_confinement_with_existing_rpath(self): - if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': - self.skipTest("The autopkgtest armhf runners can't install snaps") - project_dir = 'classic-build-existing-rpath' - - # The first run should fail as the environment variable is not - # set but we can only test this on clean systems. - if not os.path.exists(os.path.join( - os.path.sep, 'snap', 'core', 'current')): - try: - self.run_snapcraft(['prime'], project_dir) - except subprocess.CalledProcessError: - pass - else: - self.fail( - 'This should fail as SNAPCRAFT_SETUP_CORE is not set') - - # Now we set the required environment variable - self.useFixture(fixtures.EnvironmentVariable( - 'SNAPCRAFT_SETUP_CORE', '1')) - - self.run_snapcraft(['prime'], project_dir) - - bin_path = os.path.join(self.prime_dir, 'bin', 'hello-classic') - self.assertThat(bin_path, FileExists()) - - rpath = subprocess.check_output([ - self.patchelf_command, '--print-rpath', bin_path]).decode().strip() - expected_rpath = '$ORIGIN/../fake-lib:/snap/core/current/' - self.assertThat(rpath, StartsWith(expected_rpath)) - - def test_prime_includes_stage_fileset(self): - self.run_snapcraft('prime', 'prime-from-stage') - self.assertThat( - os.path.join(self.prime_dir, 'without-a'), - FileExists()) - self.assertThat( - os.path.join(self.prime_dir, 'without-b'), - Not(FileExists())) - self.assertThat( - os.path.join(self.prime_dir, 'without-c'), - FileExists()) - - def test_prime_includes_stage_excludes_fileset(self): - self.run_snapcraft('prime', 'prime-from-stage') - self.assertThat( - os.path.join(self.prime_dir, 'with-a'), - Not(FileExists())) - self.assertThat( - os.path.join(self.prime_dir, 'with-b'), - FileExists()) - self.assertThat( - os.path.join(self.prime_dir, 'with-c'), - FileExists()) - - def test_prime_with_non_ascii_desktop_file(self): - # Originally, in this test we forced LC_ALL=C. However, now that we - # are using the click python library we can't do it because it fails - # to run any command when the system language is ascii. - # --20170518 - elopio - self.run_snapcraft('prime', 'desktop-with-non-ascii') - - desktop_path = os.path.join( - self.prime_dir, 'meta', 'gui', 'test-app.desktop') - - self.expectThat( - desktop_path, FileContains(matcher=Contains('non ascíí'))) - - def _prime_invalid_part(self, debug): - exception = self.assertRaises( - subprocess.CalledProcessError, - self.run_snapcraft, ['prime', 'invalid-part-name'], - 'prime-from-stage', debug=debug) - - self.assertThat(exception.returncode, Equals(2)) - self.assertThat(exception.output, Contains( - "part named 'invalid-part-name' is not defined")) - - return exception.output - - def test_prime_invalid_part_no_traceback_without_debug(self): - self.assertThat( - self._prime_invalid_part(False), Not(Contains("Traceback"))) - - def test_prime_invalid_part_does_traceback_with_debug(self): - self.assertThat( - self._prime_invalid_part(True), Contains("Traceback")) - - -class PrimedAssetsTestCase(testscenarios.WithScenarios, - integration.TestCase): - - scenarios = [ - ('setup', dict(project_dir='assets-with-gui-in-setup')), - ('snap', dict(project_dir='assets-with-gui-in-snap')), - ] - - def test_assets_in_meta(self): - self.run_snapcraft('prime', self.project_dir) - - gui_dir = os.path.join(self.prime_dir, 'meta', 'gui') - expected_desktop = dedent("""\ - [Desktop Entry] - Name=My App - Exec=my-app - Type=Application - """) - self.expectThat(os.path.join(gui_dir, 'icon.png'), FileExists()) - self.expectThat(os.path.join(gui_dir, 'my-app.desktop'), - FileContains(expected_desktop)) diff -Nru snapcraft-2.40/tests/integration/general/test_pull_properties.py snapcraft-2.41/tests/integration/general/test_pull_properties.py --- snapcraft-2.40/tests/integration/general/test_pull_properties.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_pull_properties.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,306 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2018 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 collections import namedtuple -import os -import subprocess -import yaml - -import testscenarios -from testtools.matchers import ( - Equals, - FileExists -) - -from tests import ( - fixture_setup, - integration -) - - -class PullPropertiesTestCase(integration.TestCase): - - def test_pull(self): - self.assert_expected_pull_state('local-plugin-pull-properties') - - def test_pull_legacy_pull_properties(self): - self.assert_expected_pull_state('local-plugin-legacy-pull-properties') - - def assert_expected_pull_state(self, project_dir): - self.run_snapcraft('pull', project_dir) - - state_file = os.path.join( - self.parts_dir, 'x-local-plugin', 'state', 'pull') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - - # Verify that the correct schema dependencies made it into the state. - self.assertTrue('foo' in state.schema_properties) - self.assertTrue('stage-packages' in state.schema_properties) - - # Verify that the contents of the dependencies made it in as well. - self.assertTrue('foo' in state.properties) - self.assertTrue(len(state.assets['stage-packages']) > 0) - self.assertIn('build-packages', state.assets) - self.assertTrue('stage-packages' in state.properties) - self.assertThat(state.properties['foo'], Equals('bar')) - self.assertThat(state.properties['stage-packages'], Equals(['curl'])) - - def test_pull_with_arch(self): - if self.deb_arch == 'armhf': - self.skipTest('For now, we just support crosscompile from amd64') - self.run_snapcraft(['pull', '--target-arch=i386', 'go-hello'], - 'go-hello') - state_file = os.path.join(self.parts_dir, - 'go-hello', 'state', 'pull') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - self.assertThat(state.project_options['deb_arch'], Equals('i386')) - - def test_arch_with_pull(self): - if self.deb_arch == 'armhf': - self.skipTest('For now, we just support crosscompile from amd64') - self.run_snapcraft(['--target-arch=i386', 'pull', 'go-hello'], - 'go-hello') - state_file = os.path.join(self.parts_dir, - 'go-hello', 'state', 'pull') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - self.assertThat(state.project_options['deb_arch'], Equals('i386')) - - -class AssetTrackingTestCase(integration.TestCase): - - def test_pull(self): - self.copy_project_to_cwd('asset-tracking') - stage_version = self.set_stage_package_version( - 'snapcraft.yaml', part='asset-tracking', package='hello') - build_version = self.set_build_package_version( - 'snapcraft.yaml', part='asset-tracking', package='hello') - - self.run_snapcraft('pull') - - state_file = os.path.join( - self.parts_dir, 'asset-tracking', 'state', 'pull') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - - # Verify that the correct version of 'hello' is installed - self.assertTrue(len(state.assets['stage-packages']) > 0) - self.assertTrue(len(state.assets['build-packages']) > 0) - self.assertIn( - 'hello={}'.format(stage_version), state.assets['stage-packages']) - self.assertIn( - 'hello={}'.format(build_version), state.assets['build-packages']) - self.assertIn('source-details', state.assets) - - def test_pull_global_build_packages_are_excluded(self): - """ - Ensure global build-packages are not included in each part's - build-packages data. - """ - self.copy_project_to_cwd('build-package-global') - self.set_build_package_version( - os.path.join('snap', 'snapcraft.yaml'), - part=None, package='haskell-doc') - self.run_snapcraft('pull') - - state_file = os.path.join( - self.parts_dir, 'empty-part', 'state', 'pull') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - - self.assertTrue(len(state.assets['build-packages']) == 0) - - def test_pull_build_package_with_any_architecture(self): - self.copy_project_to_cwd('build-package') - self.set_build_package_architecture( - os.path.join('snap', 'snapcraft.yaml'), - part='hello', package='hello', architecture='any') - self.run_snapcraft('pull') - - state_file = os.path.join( - self.parts_dir, 'hello', 'state', 'pull') - with open(state_file) as f: - state = yaml.load(f) - self.assertIn('hello', state.assets['build-packages'][0]) - - def test_pull_with_virtual_build_package(self): - virtual_package = 'fortunes-off' - self.addCleanup( - subprocess.call, ['sudo', 'apt-get', 'remove', virtual_package]) - self.run_snapcraft('pull', 'build-virtual-package') - - state_file = os.path.join( - 'snap', '.snapcraft', 'state') - with open(state_file) as f: - state = yaml.load(f) - self.assertIn( - '{}={}'.format( - virtual_package, integration.get_package_version( - virtual_package, self.distro_series, self.deb_arch)), - state.assets['build-packages']) - - -TestDetail = namedtuple('TestDetail', ['field', 'value']) - - -class GitAssetTrackingTestCase(testscenarios.WithScenarios, - integration.TestCase): - - scenarios = [ - ('plain', { - 'part_name': 'git-part', - 'expected_details': None, - }), - ('branch', { - 'part_name': 'git-part-branch', - 'expected_details': TestDetail('source-branch', 'test-branch'), - }), - ('tag', { - 'part_name': 'git-part-tag', - 'expected_details': TestDetail('source-tag', 'feature-tag'), - }), - ] - - def test_pull_git(self): - repo_fixture = fixture_setup.GitRepo() - self.useFixture(repo_fixture) - project_dir = 'asset-tracking-git' - - self.run_snapcraft(['pull', self.part_name], project_dir) - - state_file = os.path.join( - self.parts_dir, self.part_name, 'state', 'pull') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - - self.assertIn('source-details', state.assets) - - # fall back to the commit if no other source option is provided - # snapcraft.source.Git doesn't allow both a tag and a commit - if self.expected_details: - self.assertThat( - state.assets['source-details'][self.expected_details.field], - Equals(self.expected_details.value)) - else: - self.assertThat( - state.assets['source-details']['source-commit'], - Equals(repo_fixture.commit)) - - -class BazaarAssetTrackingTestCase(testscenarios.WithScenarios, - integration.TestCase): - scenarios = [ - ('plain', { - 'part_name': 'bzr-part', - 'expected_details': None, - }), - ('tag', { - 'part_name': 'bzr-part-tag', - 'expected_details': TestDetail('source-tag', 'feature-tag'), - }), - ] - - def test_pull_bzr(self): - repo_fixture = fixture_setup.BzrRepo('bzr-source') - self.useFixture(repo_fixture) - project_dir = 'asset-tracking-bzr' - part = self.part_name - self.run_snapcraft(['pull', part], project_dir) - - state_file = os.path.join( - self.parts_dir, part, 'state', 'pull') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - - self.assertIn('source-details', state.assets) - - if self.expected_details: - self.assertThat( - state.assets['source-details'][self.expected_details.field], - Equals(self.expected_details.value)) - else: - self.assertThat( - state.assets['source-details']['source-commit'], - Equals(repo_fixture.commit)) - - -class MercurialAssetTrackingTestCase(testscenarios.WithScenarios, - integration.TestCase): - scenarios = [ - ('plain', { - 'part_name': 'hg-part', - 'expected_details': None, - }), - ('tag', { - 'part_name': 'hg-part-tag', - 'expected_details': TestDetail('source-tag', 'feature-tag'), - }), - ] - - def test_pull_hg(self): - repo_fixture = fixture_setup.HgRepo('hg-source') - self.useFixture(repo_fixture) - project_dir = 'asset-tracking-hg' - part = self.part_name - self.run_snapcraft(['pull', part], project_dir) - - state_file = os.path.join( - self.parts_dir, part, 'state', 'pull') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - - self.assertIn('source-details', state.assets) - - if self.expected_details: - self.assertThat( - state.assets['source-details'][self.expected_details.field], - Equals(self.expected_details.value)) - else: - self.assertThat( - state.assets['source-details']['source-commit'], - Equals(repo_fixture.commit)) - - -class SubversionAssetTrackingTestCase(integration.TestCase): - - def test_pull_svn(self): - repo_fixture = fixture_setup.SvnRepo('svn-source') - self.useFixture(repo_fixture) - project_dir = 'asset-tracking-svn' - part = 'svn-part' - expected_commit = repo_fixture.commit - self.run_snapcraft(['pull', part], project_dir) - - state_file = os.path.join( - self.parts_dir, part, 'state', 'pull') - self.assertThat(state_file, FileExists()) - with open(state_file) as f: - state = yaml.load(f) - - self.assertIn('source-details', state.assets) - self.assertThat( - state.assets['source-details']['source-commit'], - Equals(expected_commit)) diff -Nru snapcraft-2.40/tests/integration/general/test_pull.py snapcraft-2.41/tests/integration/general/test_pull.py --- snapcraft-2.40/tests/integration/general/test_pull.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_pull.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,48 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017-2018 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 ( - Contains, - Equals, - Not, -) - -from tests import integration - - -class PullTestCase(integration.TestCase): - - def _pull_invalid_part(self, debug): - exception = self.assertRaises( - subprocess.CalledProcessError, - self.run_snapcraft, ['pull', 'invalid-part-name'], - 'go-hello', debug=debug) - - self.assertThat(exception.returncode, Equals(2)) - self.assertThat(exception.output, Contains( - "part named 'invalid-part-name' is not defined")) - - return exception.output - - def test_pull_invalid_part_no_traceback_without_debug(self): - self.assertThat( - self._pull_invalid_part(False), Not(Contains("Traceback"))) - - def test_pull_invalid_part_does_traceback_with_debug(self): - self.assertThat( - self._pull_invalid_part(True), Contains("Traceback")) diff -Nru snapcraft-2.40/tests/integration/general/test_rpm_source.py snapcraft-2.41/tests/integration/general/test_rpm_source.py --- snapcraft-2.40/tests/integration/general/test_rpm_source.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_rpm_source.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2017 Neal Gompa -# Copyright (C) 2017-2018 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 FileExists - -from tests import integration - - -class RpmSourceTestCase(integration.TestCase): - - def test_stage_rpm(self): - self.run_snapcraft('stage', 'rpm-hello') - - self.assertThat( - os.path.join(self.stage_dir, 'bin', 'hello'), - FileExists()) - self.assertThat( - os.path.join(self.stage_dir, 'usr', 'bin', 'world'), - FileExists()) diff -Nru snapcraft-2.40/tests/integration/general/test_scriptlets.py snapcraft-2.41/tests/integration/general/test_scriptlets.py --- snapcraft-2.40/tests/integration/general/test_scriptlets.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_scriptlets.py 2018-04-14 12:13:35.000000000 +0000 @@ -17,7 +17,7 @@ import multiprocessing import os -from testtools.matchers import FileContains, FileExists +from testtools.matchers import FileContains, FileExists, Not from tests import integration @@ -61,3 +61,98 @@ arch_triplet_file = os.path.join(installdir, 'lib', self.arch_triplet, 'lib.so') self.assertThat(arch_triplet_file, FileExists()) + + def test_override_pull(self): + self.run_snapcraft('pull', 'scriptlet-override-pull') + + sourcedir = os.path.join( + self.parts_dir, 'override-pull-scriptlet-test', 'src') + + self.assertThat(os.path.join(sourcedir, 'file'), FileExists()) + self.assertThat(os.path.join(sourcedir, 'before-pull'), FileExists()) + self.assertThat(os.path.join(sourcedir, 'after-pull'), FileExists()) + + def test_override_build(self): + self.run_snapcraft('build', 'scriptlet-override-build') + + builddir = os.path.join( + self.parts_dir, 'override-build-scriptlet-test', 'build') + installdir = os.path.join( + self.parts_dir, 'override-build-scriptlet-test', 'install') + + self.assertThat(os.path.join(builddir, 'file'), FileExists()) + self.assertThat(os.path.join(builddir, 'before-build'), FileExists()) + self.assertThat(os.path.join(builddir, 'after-build'), FileExists()) + self.assertThat(os.path.join(installdir, 'file'), FileExists()) + self.assertThat( + os.path.join(installdir, 'before-install'), FileExists()) + self.assertThat( + os.path.join(installdir, 'after-install'), FileExists()) + + def test_override_stage(self): + self.run_snapcraft('stage', 'scriptlet-override-stage') + + installdir = os.path.join( + self.parts_dir, 'override-stage-scriptlet-test', 'install') + + # Assert that the before/after stage files aren't placed in the + # installdir, although the file is. + self.assertThat(os.path.join(installdir, 'file'), FileExists()) + self.assertThat( + os.path.join(installdir, 'before-stage'), Not(FileExists())) + self.assertThat( + os.path.join(installdir, 'after-stage'), Not(FileExists())) + + self.assertThat(os.path.join(self.stage_dir, 'file'), FileExists()) + self.assertThat( + os.path.join(self.stage_dir, 'before-stage'), FileExists()) + self.assertThat( + os.path.join(self.stage_dir, 'after-stage'), FileExists()) + + # Also assert that, while file2 was installed, it wasn't staged + self.assertThat( + os.path.join( + self.parts_dir, 'override-stage-do-nothing', 'install', + 'file2'), + FileExists()) + self.assertThat( + os.path.join(self.stage_dir, 'file2'), Not(FileExists())) + + def test_override_prime(self): + self.run_snapcraft('prime', 'scriptlet-override-prime') + + installdir = os.path.join( + self.parts_dir, 'override-prime-scriptlet-test', 'install') + + # Assert that the before/after prime files aren't placed in the + # installdir, although the file is. + self.assertThat(os.path.join(installdir, 'file'), FileExists()) + self.assertThat( + os.path.join(installdir, 'before-prime'), Not(FileExists())) + self.assertThat( + os.path.join(installdir, 'after-prime'), Not(FileExists())) + + # Assert that the before/after prime files aren't placed in the + # stagedir, although the file is. + self.assertThat(os.path.join(self.stage_dir, 'file'), FileExists()) + self.assertThat( + os.path.join(self.stage_dir, 'before-prime'), Not(FileExists())) + self.assertThat( + os.path.join(self.stage_dir, 'after-prime'), Not(FileExists())) + + self.assertThat(os.path.join(self.prime_dir, 'file'), FileExists()) + self.assertThat( + os.path.join(self.prime_dir, 'before-prime'), FileExists()) + self.assertThat( + os.path.join(self.prime_dir, 'after-prime'), FileExists()) + + # Also assert that, while file2 was installed and staged, it wasn't + # primed + self.assertThat( + os.path.join( + self.parts_dir, 'override-prime-do-nothing', 'install', + 'file2'), + FileExists()) + self.assertThat(os.path.join(self.stage_dir, 'file2'), FileExists()) + self.assertThat( + os.path.join(self.prime_dir, 'file2'), Not(FileExists())) diff -Nru snapcraft-2.40/tests/integration/general/test_snapcraftctl_set_grade.py snapcraft-2.41/tests/integration/general/test_snapcraftctl_set_grade.py --- snapcraft-2.40/tests/integration/general/test_snapcraftctl_set_grade.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_snapcraftctl_set_grade.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,76 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 Canonical Ltd +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License grade 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 +import yaml + +from testtools.matchers import Equals, Contains + +from tests import integration + + +class SnapcraftctlSetGradeTestCase(integration.TestCase): + + def test_set_grade(self): + self.construct_yaml( + grade=None, adopt_info='my-part', parts=textwrap.dedent("""\ + my-part: + plugin: nil + override-pull: snapcraftctl set-grade devel + """)) + + self.run_snapcraft('prime') + + with open(os.path.join('prime', 'meta', 'snap.yaml')) as f: + y = yaml.load(f) + + self.assertThat(y['grade'], Equals('devel')) + + def test_set_grade_no_overwrite(self): + self.construct_yaml( + grade='devel', adopt_info='my-part', + parts=textwrap.dedent("""\ + my-part: + plugin: nil + override-pull: snapcraftctl set-grade stable + """)) + + self.run_snapcraft('prime') + + with open(os.path.join('prime', 'meta', 'snap.yaml')) as f: + y = yaml.load(f) + + self.assertThat(y['grade'], Equals('devel')) + + def test_set_grade_twice_errors(self): + self.construct_yaml( + grade=None, adopt_info='my-part', parts=textwrap.dedent("""\ + my-part: + plugin: nil + override-pull: snapcraftctl set-grade stable + override-prime: snapcraftctl set-grade devel + """)) + + raised = self.assertRaises( + subprocess.CalledProcessError, self.run_snapcraft, 'prime') + self.assertThat( + raised.output, Contains( + "Unable to set grade: it was already set in the 'pull' " + "step")) + self.assertThat( + raised.output, Contains("Failed to run 'override-prime'")) diff -Nru snapcraft-2.40/tests/integration/general/test_snapcraftctl_set_version.py snapcraft-2.41/tests/integration/general/test_snapcraftctl_set_version.py --- snapcraft-2.40/tests/integration/general/test_snapcraftctl_set_version.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_snapcraftctl_set_version.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,76 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 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 +import yaml + +from testtools.matchers import Equals, Contains + +from tests import integration + + +class SnapcraftctlSetVersionTestCase(integration.TestCase): + + def test_set_version(self): + self.construct_yaml( + version=None, adopt_info='my-part', parts=textwrap.dedent("""\ + my-part: + plugin: nil + override-pull: snapcraftctl set-version override-version + """)) + + self.run_snapcraft('prime') + + with open(os.path.join('prime', 'meta', 'snap.yaml')) as f: + y = yaml.load(f) + + self.assertThat(y['version'], Equals('override-version')) + + def test_set_version_no_overwrite(self): + self.construct_yaml( + version='test-version', adopt_info='my-part', + parts=textwrap.dedent("""\ + my-part: + plugin: nil + override-pull: snapcraftctl set-version override-version + """)) + + self.run_snapcraft('prime') + + with open(os.path.join('prime', 'meta', 'snap.yaml')) as f: + y = yaml.load(f) + + self.assertThat(y['version'], Equals('test-version')) + + def test_set_version_twice_errors(self): + self.construct_yaml( + version=None, adopt_info='my-part', parts=textwrap.dedent("""\ + my-part: + plugin: nil + override-pull: snapcraftctl set-version override-version + override-prime: snapcraftctl set-version no-this-version + """)) + + raised = self.assertRaises( + subprocess.CalledProcessError, self.run_snapcraft, 'prime') + self.assertThat( + raised.output, Contains( + "Unable to set version: it was already set in the 'pull' " + "step")) + self.assertThat( + raised.output, Contains("Failed to run 'override-prime'")) diff -Nru snapcraft-2.40/tests/integration/general/test_snap.py snapcraft-2.41/tests/integration/general/test_snap.py --- snapcraft-2.40/tests/integration/general/test_snap.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_snap.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,264 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2018 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 textwrap import dedent - -import fixtures -import testtools -from testtools.matchers import ( - Equals, - EndsWith, - FileContains, - FileExists, - Not, -) - -from tests import integration - - -class SnapTestCase(integration.TestCase): - - def test_snap(self): - self.copy_project_to_cwd('assemble') - self.run_snapcraft('snap') - - snap_file_path = 'assemble_1.0_{}.snap'.format(self.deb_arch) - self.assertThat(snap_file_path, FileExists()) - - binary1_wrapper_path = os.path.join( - self.prime_dir, 'command-assemble-bin.wrapper') - with open('binary1.after', 'r') as file_: - expected_binary1_wrapper = file_.read() - self.assertThat( - binary1_wrapper_path, FileContains(expected_binary1_wrapper)) - - self.useFixture( - fixtures.EnvironmentVariable( - 'SNAP', os.path.join(os.getcwd(), self.prime_dir))) - binary_scenarios = ( - ('command-assemble-service.wrapper', 'service-start\n'), - ('stop-command-assemble-service.wrapper', 'service-stop\n'), - ('command-assemble-bin.wrapper', 'binary1\n'), - ('command-binary2.wrapper', 'binary2\n'), - ) - for binary, expected_output in binary_scenarios: - output = subprocess.check_output( - os.path.join(self.prime_dir, binary), universal_newlines=True) - self.assertThat(output, Equals(expected_output)) - - with testtools.ExpectedException(subprocess.CalledProcessError): - subprocess.check_output( - os.path.join(self.prime_dir, 'bin', 'not-wrapped'), - stderr=subprocess.STDOUT) - - self.assertThat( - 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())) - - # LP: #1750658 - self.assertThat( - os.path.join(self.prime_dir, 'meta', 'snap.yaml'), - FileContains(dedent("""\ - name: assemble - version: 1.0 - summary: one line summary - description: a longer description - architectures: - - {} - confinement: strict - grade: stable - apps: - assemble-bin: - command: command-assemble-bin.wrapper - assemble-service: - command: command-assemble-service.wrapper - daemon: simple - stop-command: stop-command-assemble-service.wrapper - binary-wrapper-none: - command: subdir/binary3 - binary2: - command: command-binary2.wrapper - """).format(self.deb_arch))) - - def test_snap_default(self): - self.copy_project_to_cwd('assemble') - self.run_snapcraft([]) - 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') - - 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 snap ` are always in - # sync). - 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()) - - def test_snap_short_output_option(self): - self.run_snapcraft(['snap', '-o', 'mysnap.snap'], 'assemble') - self.assertThat('mysnap.snap', FileExists()) - - def test_error_with_unexistent_build_package(self): - self.copy_project_to_cwd('assemble') - with open('snapcraft.yaml', 'a') as yaml_file: - yaml_file.write('build-packages:\n' - ' - inexistent-package\n') - - # We update here to get a clean log/stdout later - self.run_snapcraft('update') - - exception = self.assertRaises( - subprocess.CalledProcessError, self.run_snapcraft, 'snap') - expected = ( - "Could not find a required package in 'build-packages': " - "inexistent-package\n") - self.assertThat(exception.output, EndsWith(expected)) - - def test_snap_with_exposed_files(self): - self.copy_project_to_cwd('nil-plugin-pkgfilter') - self.run_snapcraft('stage') - self.assertThat( - os.path.join(self.stage_dir, 'usr', 'bin', 'nmcli'), - FileExists()) - - self.run_snapcraft('snap') - self.assertThat( - os.path.join(self.prime_dir, 'usr', 'bin', 'nmcli'), - FileExists()) - self.assertThat( - os.path.join(self.prime_dir, 'usr', 'bin', 'nmtui'), - Not(FileExists())) - - def test_snap_from_snapcraft_init(self): - self.assertThat('snapcraft.yaml', Not(FileExists())) - self.run_snapcraft('init') - self.assertThat(os.path.join('snap', 'snapcraft.yaml'), FileExists()) - - self.run_snapcraft('snap') - - def test_snap_with_arch(self): - if self.deb_arch == 'armhf': - self.skipTest('For now, we just support crosscompile from amd64') - self.run_snapcraft('init') - - self.run_snapcraft(['snap', '--target-arch=i386']) - self.assertThat('my-snap-name_0.1_i386.snap', FileExists()) - - def test_arch_with_snap(self): - if self.deb_arch == 'armhf': - self.skipTest('For now, we just support crosscompile from amd64') - self.run_snapcraft('init') - - self.run_snapcraft(['--target-arch=i386', 'snap']) - self.assertThat('my-snap-name_0.1_i386.snap', FileExists()) - - def test_implicit_command_with_arch(self): - if self.deb_arch == 'armhf': - self.skipTest('For now, we just support crosscompile from amd64') - self.run_snapcraft('init') - - self.run_snapcraft('--target-arch=i386') - self.assertThat('my-snap-name_0.1_i386.snap', FileExists()) - - def test_error_on_bad_yaml(self): - error = self.assertRaises( - subprocess.CalledProcessError, - self.run_snapcraft, 'stage', 'bad-yaml') - self.assertIn( - "Issues while validating snapcraft.yaml: found character '\\t' " - "that cannot start any token on line 13 of snapcraft.yaml", - str(error.output)) - - def test_yaml_merge_tag(self): - self.copy_project_to_cwd('yaml-merge-tag') - self.run_snapcraft('stage') - self.assertThat( - 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.40/tests/integration/general/test_source_grammar.py snapcraft-2.41/tests/integration/general/test_source_grammar.py --- snapcraft-2.40/tests/integration/general/test_source_grammar.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_source_grammar.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,88 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2017-2018 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 Contains - -from tests import integration, fixture_setup - - -class PartsGrammarTestCase(integration.TestCase): - - def setUp(self): - super().setUp() - self.test_source = ( - 'https://github.com/snapcrafters/fork-and-rename-me.git') - - def test_plain_source_string(self): - snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) - snapcraft_yaml.update_part('my-part', { - 'plugin': 'nil', - 'source': self.test_source, - }) - self.useFixture(snapcraft_yaml) - self.run_snapcraft(['pull']) - - def test_source_on_current_arch(self): - snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) - snapcraft_yaml.update_part('my-part', { - 'plugin': 'nil', - 'source': [ - {'on {}'.format(self.deb_arch): self.test_source}, - {'else': 'invalid'}, - ] - }) - self.useFixture(snapcraft_yaml) - self.run_snapcraft(['pull']) - - def test_source_try(self): - snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) - snapcraft_yaml.update_part('my-part', { - 'plugin': 'nil', - 'source': [ - {'try': self.test_source}, - {'else': 'invalid'}, - ] - }) - self.useFixture(snapcraft_yaml) - self.run_snapcraft(['pull']) - - def test_source_on_other_arch(self): - snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) - snapcraft_yaml.update_part('my-part', { - 'plugin': 'nil', - 'source': [ - {'on other-arch': 'invalid'}, - ] - }) - self.useFixture(snapcraft_yaml) - self.run_snapcraft(['pull']) - - def test_source_on_other_arch_else_fail(self): - snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) - snapcraft_yaml.update_part('my-part', { - 'plugin': 'nil', - 'source': [ - {'on other-arch': 'invalid'}, - 'else fail', - ] - }) - self.useFixture(snapcraft_yaml) - self.assertThat(self.assertRaises( - subprocess.CalledProcessError, self.run_snapcraft, - ['pull']).output, Contains( - "Unable to satisfy 'on other-arch', failure forced")) diff -Nru snapcraft-2.40/tests/integration/general/test_stage.py snapcraft-2.41/tests/integration/general/test_stage.py --- snapcraft-2.40/tests/integration/general/test_stage.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_stage.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,149 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2018 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 testtools.matchers import ( - Contains, - Equals, - FileExists, - Not -) - -from tests import integration - - -class StageTestCase(integration.TestCase): - - def _stage_conflicts(self, debug): - exception = self.assertRaises( - subprocess.CalledProcessError, - self.run_snapcraft, 'stage', 'conflicts', debug=debug) - - self.assertThat(exception.returncode, Equals(2)) - expected_conflicts = ( - "Failed to stage: " - "Parts 'p1' and 'p2' have the following files, but with different " - "contents:\n bin/test\n") - self.assertThat(exception.output, Contains(expected_conflicts)) - - expected_help = ( - 'Snapcraft offers some capabilities to solve this by use ' - 'of the following keywords:\n' - ' - `filesets`\n' - ' - `stage`\n' - ' - `snap`\n' - ' - `organize`\n\n' - 'To learn more about these part keywords, run ' - '`snapcraft help plugins`.' - ) - self.assertThat(exception.output, Contains(expected_help)) - return exception.output - - def test_conflicts_no_traceback_without_debug(self): - self.assertThat( - self._stage_conflicts(False), Not(Contains("Traceback"))) - - def test_conflicts_traceback_with_debug(self): - self.assertThat( - self._stage_conflicts(True), Contains("Traceback")) - - def test_conflicts(self): - exception = self.assertRaises( - subprocess.CalledProcessError, - self.run_snapcraft, 'stage', 'conflicts') - - self.assertThat(exception.returncode, Equals(2)) - expected_conflicts = ( - "Failed to stage: " - "Parts 'p1' and 'p2' have the following files, but with different " - "contents:\n bin/test\n") - self.assertThat(exception.output, Contains(expected_conflicts)) - - expected_help = ( - 'Snapcraft offers some capabilities to solve this by use ' - 'of the following keywords:\n' - ' - `filesets`\n' - ' - `stage`\n' - ' - `snap`\n' - ' - `organize`\n\n' - 'To learn more about these part keywords, run ' - '`snapcraft help plugins`.' - ) - self.assertThat(exception.output, Contains(expected_help)) - - def test_classic_confinement(self): - if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': - self.skipTest("The autopkgtest armhf runners can't install snaps") - project_dir = 'classic-build' - - # The first run should fail as the environment variable is not - # set but we can only test this on clean systems. - if not os.path.exists(os.path.join( - os.path.sep, 'snap', 'core', 'current')): - try: - self.run_snapcraft(['stage'], project_dir) - except subprocess.CalledProcessError: - pass - else: - self.fail( - 'This should fail as SNAPCRAFT_SETUP_CORE is not set') - - # Now we set the required environment variable - self.useFixture(fixtures.EnvironmentVariable( - 'SNAPCRAFT_SETUP_CORE', '1')) - - self.run_snapcraft(['stage'], project_dir) - bin_path = os.path.join(self.stage_dir, 'bin', 'hello-classic') - self.assertThat(bin_path, FileExists()) - - # ld-linux will not be set until everything is primed. - interpreter = subprocess.check_output([ - self.patchelf_command, '--print-interpreter', bin_path]).decode() - self.assertThat(interpreter, Not(Contains('/snap/core/current'))) - - def test_staging_libc_links(self): - project_dir = 'staging_links_to_libc' - - # First, stage libc6-dev via stage-packages - self.run_snapcraft(['stage', 'from-package'], project_dir) - - # Now tar up the staging area - subprocess.check_call(['tar', 'cf', 'stage.tar', 'stage/']) - - # Now attempt to stage the tarred staging area once again. This should - # not conflict. - try: - self.run_snapcraft(['stage', 'from-tar'], project_dir) - except subprocess.CalledProcessError as e: - if 'have the following file paths in common' in e.output: - self.fail('Parts unexpectedly conflicted') - else: - raise - - def symlinks_to_libc_should_build(self): - """Regression test for LP: #1665089""" - - # This will fail to build if the libc symlinks are missing - self.run_snapcraft('stage', 'use_libc_dl') - - def test_stage_with_file_to_check_for_collisions_not_build(self): - """Regression test for LP: #1660696""" - # This will fail if we try to check for collisions even in parts that - # haven't been build. - self.run_snapcraft('stage', 'stage-with-two-equal-files') diff -Nru snapcraft-2.40/tests/integration/general/test_svn_pull.py snapcraft-2.41/tests/integration/general/test_svn_pull.py --- snapcraft-2.40/tests/integration/general/test_svn_pull.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_svn_pull.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,80 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2016-2018 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 testtools.matchers import Equals, FileExists - -from tests import integration - - -class SubversionSourceTestCase(integration.SubversionSourceBaseTestCase): - - def test_pull_svn_checkout(self): - self.copy_project_to_cwd('svn-pull') - - self.init_source_control() - self.checkout( - 'file:///{}'.format(os.path.join(self.path, 'repo')), - 'local') - open(os.path.join('local', 'file'), 'w').close() - self.add('file', cwd='local/') - self.commit('test', cwd='local/') - self.update(cwd='local/') - subprocess.check_call( - ['rm', '-rf', 'local/'], stdout=subprocess.DEVNULL) - - self.run_snapcraft('pull') - part_src_path = os.path.join(self.parts_dir, 'svn', 'src') - revno = subprocess.check_output(['svnversion', part_src_path]).strip() - self.assertThat(revno, Equals(b'1')) - self.assertThat(os.path.join(part_src_path, 'file'), FileExists()) - - def test_pull_svn_update(self): - self.copy_project_to_cwd('svn-pull-update') - - self.init_source_control() - - self.checkout( - 'file:///{}'.format(os.path.join(self.path, 'repo')), - 'local') - open(os.path.join('local', 'file'), 'w').close() - self.add('file', cwd='local/') - self.commit('test', cwd='local/') - self.update(cwd='local/') - subprocess.check_call( - ['rm', '-rf', 'local/'], stdout=subprocess.DEVNULL) - - part_src_path = os.path.join(self.parts_dir, 'svn', 'src') - self.checkout( - 'file:///{}'.format(os.path.join(self.path, 'repo')), - part_src_path) - self.checkout( - 'file:///{}'.format(os.path.join(self.path, 'repo')), - 'local') - open(os.path.join('local', 'filetwo'), 'w').close() - self.add('filetwo', cwd='local/') - self.commit('testtwo', cwd='local/') - self.update(cwd='local/') - subprocess.check_call( - ['rm', '-rf', 'local/'], stdout=subprocess.DEVNULL) - - self.run_snapcraft('pull') - revno = subprocess.check_output(['svnversion', part_src_path]).strip() - self.assertThat(revno, Equals(b'2')) - self.assertThat(os.path.join(part_src_path, 'file'), FileExists()) - self.assertThat(os.path.join(part_src_path, 'filetwo'), FileExists()) diff -Nru snapcraft-2.40/tests/integration/general/test_zip_source.py snapcraft-2.41/tests/integration/general/test_zip_source.py --- snapcraft-2.40/tests/integration/general/test_zip_source.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/general/test_zip_source.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,60 +0,0 @@ -# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- -# -# Copyright (C) 2015-2018 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 ( - Equals, - DirExists, - FileExists -) - -from tests import integration - - -class TarPluginTestCase(integration.TestCase): - - def test_stage_zip_source(self): - self.copy_project_to_cwd('zip') - self.run_snapcraft('stage') - - expected_files = [ - 'exec', - 'top-simple', - os.path.join('dir-simple', 'sub') - ] - for expected_file in expected_files: - 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)) - self.assertThat(os.access( - os.path.join(self.stage_dir, 'non-unix'), os.X_OK), - Equals(True)) - expected_dirs = [ - 'dir-simple', - 'non-unix', - ] - for expected_dir in expected_dirs: - self.assertThat( - os.path.join(self.stage_dir, expected_dir), - DirExists()) - - # Regression test for - # https://bugs.launchpad.net/snapcraft/+bug/1500728 - self.run_snapcraft('pull') diff -Nru snapcraft-2.40/tests/integration/__init__.py snapcraft-2.41/tests/integration/__init__.py --- snapcraft-2.40/tests/integration/__init__.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/__init__.py 2018-04-14 12:13:35.000000000 +0000 @@ -72,7 +72,7 @@ 'as described in HACKING.md.') if os.getenv('SNAPCRAFT_FROM_SNAP', False): - self.patchelf_command = '/snap/snapcraft/current/bin/patchelf' + self.patchelf_command = '/snap/snapcraft/current/usr/bin/patchelf' self.execstack_command = ( '/snap/snapcraft/current/usr/sbin/execstack') else: @@ -92,6 +92,10 @@ 'XDG_DATA_HOME', os.path.join(self.path, 'data'))) self.useFixture(fixtures.EnvironmentVariable('TERM', 'dumb')) + # Do not send crash reports + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_SEND_ERROR_DATA', 'n')) + patcher = mock.patch( 'xdg.BaseDirectory.xdg_config_home', new=os.path.join(self.path, '.config')) @@ -209,19 +213,28 @@ def construct_yaml(self, name='test', version='0.1', summary='Simple test snap', description='Something something', + grade=None, parts=dedent('''\ my-part: plugin: nil '''), - build_packages='[]'): + build_packages='[]', + adopt_info=None): snapcraft_yaml = { 'name': name, - 'version': version, 'summary': summary, 'description': description, 'parts': yaml.load(parts), 'build-packages': yaml.load(build_packages), } + + if version: + snapcraft_yaml['version'] = version + if adopt_info: + snapcraft_yaml['adopt-info'] = adopt_info + if grade: + snapcraft_yaml['grade'] = grade + with open('snapcraft.yaml', 'w') as f: yaml.dump(snapcraft_yaml, f, default_flow_style=False) diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_build_properties.py snapcraft-2.41/tests/integration/lifecycle/test_build_properties.py --- snapcraft-2.40/tests/integration/lifecycle/test_build_properties.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_build_properties.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,75 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2018 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 yaml + +from testtools.matchers import Equals, FileExists + +from tests import integration + + +class BuildPropertiesTestCase(integration.TestCase): + + def test_build(self): + self.assert_expected_build_state('local-plugin-build-properties') + + def test_build_legacy_build_properties(self): + self.assert_expected_build_state( + 'local-plugin-legacy-build-properties') + + def assert_expected_build_state(self, project_dir): + self.run_snapcraft('build', project_dir) + + state_file = os.path.join( + self.parts_dir, 'x-local-plugin', 'state', 'build') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + + # Verify that the correct schema dependencies made it into the state. + self.assertTrue('foo' in state.schema_properties) + self.assertTrue('stage-packages' in state.schema_properties) + + # Verify that the contents of the dependencies made it in as well. + self.assertTrue('foo' in state.properties) + self.assertTrue('stage-packages' in state.properties) + self.assertThat(state.properties['foo'], Equals('bar')) + self.assertThat(state.properties['stage-packages'], Equals(['curl'])) + + def test_build_with_arch(self): + if self.deb_arch == 'armhf': + self.skipTest('For now, we just support crosscompile from amd64') + self.run_snapcraft(['build', '--target-arch=i386', 'go-hello'], + 'go-hello') + state_file = os.path.join(self.parts_dir, + 'go-hello', 'state', 'build') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + self.assertThat(state.project_options['deb_arch'], Equals('i386')) + + def test_arch_with_build(self): + if self.deb_arch == 'armhf': + self.skipTest('For now, we just support crosscompile from amd64') + self.run_snapcraft(['--target-arch=i386', 'build', 'go-hello'], + 'go-hello') + state_file = os.path.join(self.parts_dir, + 'go-hello', 'state', 'build') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + self.assertThat(state.project_options['deb_arch'], Equals('i386')) diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_build.py snapcraft-2.41/tests/integration/lifecycle/test_build.py --- snapcraft-2.40/tests/integration/lifecycle/test_build.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_build.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,48 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017-2018 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 ( + Contains, + Equals, + Not, +) + +from tests import integration + + +class BuildTestCase(integration.TestCase): + + def _build_invalid_part(self, debug): + exception = self.assertRaises( + subprocess.CalledProcessError, + self.run_snapcraft, ['build', 'invalid-part-name'], + 'go-hello', debug=debug) + + self.assertThat(exception.returncode, Equals(2)) + self.assertThat(exception.output, Contains( + "part named 'invalid-part-name' is not defined")) + + return exception.output + + def test_build_invalid_part_no_traceback_without_debug(self): + self.assertThat( + self._build_invalid_part(False), Not(Contains("Traceback"))) + + def test_build_invalid_part_does_traceback_with_debug(self): + self.assertThat( + self._build_invalid_part(True), Contains("Traceback")) diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_clean_build_step.py snapcraft-2.41/tests/integration/lifecycle/test_clean_build_step.py --- snapcraft-2.40/tests/integration/lifecycle/test_clean_build_step.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_clean_build_step.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,182 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2018 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, + DirExists, + Equals, + FileExists, + Not +) + +from tests import integration + + +class CleanBuildStepBuiltTestCase(integration.TestCase): + + def setUp(self): + super().setUp() + + self.copy_project_to_cwd('independent-parts') + self.run_snapcraft('build') + self.parts = {} + for part in ['part1', 'part2']: + partdir = os.path.join(self.parts_dir, part) + self.parts[part] = { + 'partdir': partdir, + 'sourcedir': os.path.join(partdir, 'src'), + 'builddir': os.path.join(partdir, 'build'), + 'installdir': os.path.join(partdir, 'install'), + 'bindir': os.path.join(partdir, 'install', 'bin'), + } + + def assert_files_exist(self): + for d in ['builddir', 'bindir']: + self.assertThat(os.path.join(self.parts['part1'][d], 'file1'), + FileExists()) + self.assertThat(os.path.join(self.parts['part2'][d], 'file2'), + FileExists()) + + def test_clean_build_step(self): + self.assert_files_exist() + + output = self.run_snapcraft( + ['clean', '--step=build'], debug=False) + + for part_name, part in self.parts.items(): + self.assertThat(part['builddir'], Not(DirExists())) + self.assertThat(part['installdir'], Not(DirExists())) + self.assertThat(part['sourcedir'], DirExists()) + + # Assert that the priming and staging areas were removed wholesale, not + # a part at a time (since we didn't specify any parts). + self.assertThat(output, Contains("Cleaning up priming area")) + self.assertThat(output, Contains("Cleaning up staging area")) + + output = output.split('\n') + part1_output = [line.strip() for line in output if 'part1' in line] + part2_output = [line.strip() for line in output if 'part2' in line] + self.expectThat(part1_output, Equals([ + 'Skipping cleaning priming area for part1 (already clean)', + 'Skipping cleaning staging area for part1 (already clean)', + 'Cleaning build for part1' + ])) + self.expectThat(part2_output, Equals([ + 'Skipping cleaning priming area for part2 (already clean)', + 'Skipping cleaning staging area for part2 (already clean)', + 'Cleaning build for part2' + ])) + + # Now try to build again + self.run_snapcraft('build') + self.assert_files_exist() + + def test_clean_build_step_single_part(self): + self.assert_files_exist() + + self.run_snapcraft(['clean', 'part1', '--step=build']) + self.assertThat(self.parts['part1']['builddir'], Not(DirExists())) + self.assertThat(self.parts['part1']['installdir'], Not(DirExists())) + self.assertThat(self.parts['part1']['sourcedir'], DirExists()) + + self.assertThat( + os.path.join(self.parts['part2']['builddir'], 'file2'), + FileExists()) + self.assertThat( + os.path.join(self.parts['part2']['bindir'], 'file2'), + FileExists()) + + # Now try to build again + self.run_snapcraft('build') + self.assert_files_exist() + + +class CleanBuildStepPrimedTestCase(integration.TestCase): + + def setUp(self): + super().setUp() + + self.copy_project_to_cwd('independent-parts') + self.run_snapcraft('prime') + + self.snap_bindir = os.path.join(self.prime_dir, 'bin') + self.stage_bindir = os.path.join(self.stage_dir, 'bin') + self.parts = {} + for part in ['part1', 'part2']: + partdir = os.path.join(self.parts_dir, part) + self.parts[part] = { + 'partdir': partdir, + 'sourcedir': os.path.join(partdir, 'src'), + 'builddir': os.path.join(partdir, 'build'), + 'installdir': os.path.join(partdir, 'install'), + 'bindir': os.path.join(partdir, 'install', 'bin'), + } + + def assert_files_exist(self): + for d in ['builddir', 'bindir']: + self.assertThat(os.path.join(self.parts['part1'][d], 'file1'), + FileExists()) + self.assertThat(os.path.join(self.parts['part2'][d], 'file2'), + FileExists()) + + self.assertThat(os.path.join(self.snap_bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) + self.assertThat(os.path.join(self.stage_bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) + + def test_clean_build_step(self): + self.assert_files_exist() + + self.run_snapcraft(['clean', '--step=build']) + self.assertThat(self.stage_dir, Not(DirExists())) + self.assertThat(self.prime_dir, Not(DirExists())) + + for part_name, part in self.parts.items(): + self.assertThat(part['builddir'], Not(DirExists())) + self.assertThat(part['installdir'], Not(DirExists())) + self.assertThat(part['sourcedir'], DirExists()) + + # Now try to prime again + self.run_snapcraft('prime') + self.assert_files_exist() + + def test_clean_build_step_single_part(self): + self.assert_files_exist() + + self.run_snapcraft(['clean', 'part1', '--step=build']) + self.assertThat(os.path.join(self.stage_bindir, 'file1'), + Not(FileExists())) + self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) + self.assertThat(os.path.join(self.snap_bindir, 'file1'), + Not(FileExists())) + self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) + + self.assertThat(self.parts['part1']['builddir'], Not(DirExists())) + self.assertThat(self.parts['part1']['installdir'], Not(DirExists())) + self.assertThat(self.parts['part1']['sourcedir'], DirExists()) + + self.assertThat( + os.path.join(self.parts['part2']['builddir'], 'file2'), + FileExists()) + self.assertThat( + os.path.join(self.parts['part2']['bindir'], 'file2'), + FileExists()) + + # Now try to prime again + self.run_snapcraft('prime') + self.assert_files_exist() diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_clean_dependents.py snapcraft-2.41/tests/integration/lifecycle/test_clean_dependents.py --- snapcraft-2.40/tests/integration/lifecycle/test_clean_dependents.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_clean_dependents.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,136 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2018 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 ( + DirExists, + Not +) + +from tests import integration + + +class CleanDependentsTestCase(integration.TestCase): + + def setUp(self): + super().setUp() + + self.copy_project_to_cwd('dependencies') + self.run_snapcraft('prime') + + # Need to use the state directory here instead of partdir due to + # bug #1567054. + self.part_dirs = { + 'p1': os.path.join(self.parts_dir, 'p1', 'state'), + 'p2': os.path.join(self.parts_dir, 'p2', 'state'), + 'p3': os.path.join(self.parts_dir, 'p3', 'state'), + 'p4': os.path.join(self.parts_dir, 'p4', 'state'), + } + + def assert_clean(self, parts, common=False): + for part in parts: + self.expectThat( + self.part_dirs[part], Not(DirExists()), + 'Expected part directory for {!r} to be cleaned'.format(part)) + + if common: + self.expectThat(self.parts_dir, Not(DirExists()), + 'Expected parts/ directory to be cleaned') + self.expectThat(self.stage_dir, Not(DirExists()), + 'Expected stage/ directory to be cleaned') + self.expectThat(self.prime_dir, Not(DirExists()), + 'Expected snap/ directory to be cleaned') + + def assert_not_clean(self, parts, common=False): + for part in parts: + self.expectThat( + self.part_dirs[part], DirExists(), + 'Expected part directory for {!r} to be uncleaned'.format( + part)) + + if common: + self.expectThat(self.parts_dir, DirExists(), + 'Expected parts/ directory to be uncleaned') + self.expectThat(self.stage_dir, DirExists(), + 'Expected stage/ directory to be uncleaned') + self.expectThat(self.prime_dir, DirExists(), + 'Expected snap/ directory to be uncleaned') + + def test_clean_nested_dependent(self): + # Test that p3 (which has dependencies but no dependents) cleans with + # no extra parameters. + self.run_snapcraft(['clean', 'p3']) + self.assert_clean(['p3']) + self.assert_not_clean(['p1', 'p2', 'p4'], True) + + # Now run prime again + self.run_snapcraft('prime') + self.assert_not_clean(['p1', 'p2', 'p3', 'p4'], True) + + def test_clean_dependent(self): + # Test that p2 (which has both dependencies and dependents) cleans with + # its dependents (p3 and p4) also specified. + self.run_snapcraft(['clean', 'p2', 'p3', 'p4']) + self.assert_clean(['p2', 'p3', 'p4']) + self.assert_not_clean(['p1'], True) + + # Now run prime again + self.run_snapcraft('prime') + self.assert_not_clean(['p1', 'p2', 'p3', 'p4'], True) + + def test_clean_main(self): + # Test that p1 (which has no dependencies but dependents) cleans with + # its dependents (p2 and, as an extension, p3 and p4) also specified. + self.run_snapcraft(['clean', 'p1', 'p2', 'p3', 'p4']) + self.assert_clean(['p1', 'p2', 'p3', 'p4'], True) + + # Now run prime again + self.run_snapcraft('prime') + self.assert_not_clean(['p1', 'p2', 'p3', 'p4'], True) + + def test_clean_dependent_without_nested_dependents(self): + # Test that p2 (which has both dependencies and dependents) + # cleans its dependents (p3 and p4) are not specified + self.run_snapcraft(['clean', 'p2']) + self.assert_not_clean(['p1'], False) + self.assert_clean(['p2', 'p3', 'p4'], False) + + def test_clean_dependent_without_nested_dependent(self): + # Test that p2 (which has both dependencies and dependents) + # cleans its dependents (p4) is not specified + self.run_snapcraft(['clean', 'p2', 'p3']) + self.assert_not_clean(['p1'], False) + self.assert_clean(['p2', 'p3', 'p4'], False) + + def test_clean_main_without_any_dependent(self): + # Test that p1 (which has no dependencies but dependents) + # cleans if none of its dependents are also specified. + self.run_snapcraft(['clean', 'p1']) + self.assert_clean(['p1', 'p2', 'p3', 'p4'], True) + + def test_clean_main_without_dependent(self): + # Test that p1 (which has no dependencies but dependents) + # cleans if its dependent (p2) is not specified + self.run_snapcraft(['clean', 'p1', 'p3', 'p4']) + self.assert_clean(['p1', 'p2', 'p3', 'p4'], True) + + def test_clean_main_without_nested_dependent(self): + # Test that p1 (which has no dependencies but dependents) + # cleans if its nested dependent (p3, by way of p2) is not + # specified + self.run_snapcraft(['clean', 'p1', 'p2']) + self.assert_clean(['p1', 'p2', 'p3', 'p4'], True) diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_clean_prime_step.py snapcraft-2.41/tests/integration/lifecycle/test_clean_prime_step.py --- snapcraft-2.40/tests/integration/lifecycle/test_clean_prime_step.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_clean_prime_step.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,88 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2018 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, + DirExists, + FileExists, + Not +) + +from tests import integration + + +class CleanPrimeStepTestCase(integration.TestCase): + + def setUp(self): + super().setUp() + + self.copy_project_to_cwd('independent-parts') + self.run_snapcraft('prime') + + def test_clean_prime_step(self): + bindir = os.path.join(self.prime_dir, 'bin') + self.assertThat(os.path.join(bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(bindir, 'file2'), FileExists()) + + output = self.run_snapcraft( + ['clean', '--step=prime'], debug=False) + self.assertThat(self.prime_dir, Not(DirExists())) + self.assertThat(self.stage_dir, DirExists()) + self.assertThat(self.parts_dir, DirExists()) + + # Assert that the priming area was removed wholesale, not a part at a + # time (since we didn't specify any parts). + self.assertThat(output, Contains("Cleaning up priming area")) + self.expectThat(output, Not(Contains('part1'))) + self.expectThat(output, Not(Contains('part2'))) + + # Now try to prime again + self.run_snapcraft('prime') + self.assertThat(os.path.join(bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(bindir, 'file2'), FileExists()) + + def test_clean_prime_step_single_part(self): + bindir = os.path.join(self.prime_dir, 'bin') + self.assertThat(os.path.join(bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(bindir, 'file2'), FileExists()) + + self.run_snapcraft(['clean', 'part1', '--step=prime']) + self.assertThat(os.path.join(bindir, 'file1'), Not(FileExists())) + self.assertThat(os.path.join(bindir, 'file2'), FileExists()) + self.assertThat(self.stage_dir, DirExists()) + self.assertThat(self.parts_dir, DirExists()) + + # Now try to prime again + self.run_snapcraft('prime') + self.assertThat(os.path.join(bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(bindir, 'file2'), FileExists()) + + def test_clean_with_deprecated_strip_step(self): + bindir = os.path.join(self.prime_dir, 'bin') + self.assertThat(os.path.join(bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(bindir, 'file2'), FileExists()) + + self.run_snapcraft(['clean', '--step=strip']) + self.assertThat(self.prime_dir, Not(DirExists())) + self.assertThat(self.stage_dir, DirExists()) + self.assertThat(self.parts_dir, DirExists()) + + # Now try to prime again + self.run_snapcraft('prime') + self.assertThat(os.path.join(bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(bindir, 'file2'), FileExists()) diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_clean_pull_step.py snapcraft-2.41/tests/integration/lifecycle/test_clean_pull_step.py --- snapcraft-2.40/tests/integration/lifecycle/test_clean_pull_step.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_clean_pull_step.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,154 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2018 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 ( + DirExists, + Equals, + FileExists, + Not +) + +from tests import integration + + +class CleanPullStepPulledTestCase(integration.TestCase): + + def setUp(self): + super().setUp() + + self.copy_project_to_cwd('independent-parts') + self.run_snapcraft('pull') + self.part1_sourcedir = os.path.join(self.parts_dir, 'part1', 'src') + self.part2_sourcedir = os.path.join(self.parts_dir, 'part2', 'src') + + def assert_files_exist(self): + self.assertThat(os.path.join(self.part1_sourcedir, 'file1'), + FileExists()) + self.assertThat(os.path.join(self.part2_sourcedir, 'file2'), + FileExists()) + + def test_clean_pull_step(self): + self.assert_files_exist() + + self.run_snapcraft(['clean', '--step=pull']) + self.assertThat(self.part1_sourcedir, Not(DirExists())) + self.assertThat(self.part2_sourcedir, Not(DirExists())) + + # Now try to pull again + self.run_snapcraft('pull') + self.assert_files_exist() + + def test_clean_pull_step_single_part(self): + self.assert_files_exist() + + self.run_snapcraft(['clean', 'part1', '--step=pull']) + self.assertThat(self.part1_sourcedir, Not(DirExists())) + self.assertThat(os.path.join(self.part2_sourcedir, 'file2'), + FileExists()) + + # Now try to pull again + self.run_snapcraft('pull') + self.assert_files_exist() + + +class CleanPullStepPrimedTestCase(integration.TestCase): + + def setUp(self): + super().setUp() + + self.copy_project_to_cwd('independent-parts') + self.run_snapcraft('prime') + + self.snap_bindir = os.path.join(self.prime_dir, 'bin') + self.stage_bindir = os.path.join(self.stage_dir, 'bin') + self.parts = {} + for part in ['part1', 'part2']: + partdir = os.path.join(self.parts_dir, part) + self.parts[part] = { + 'partdir': partdir, + 'sourcedir': os.path.join(partdir, 'src'), + 'builddir': os.path.join(partdir, 'build'), + 'installdir': os.path.join(partdir, 'install'), + 'bindir': os.path.join(partdir, 'install', 'bin'), + } + + def assert_files_exist(self): + for d in ['builddir', 'bindir', 'sourcedir']: + self.assertThat(os.path.join(self.parts['part1'][d], 'file1'), + FileExists()) + self.assertThat(os.path.join(self.parts['part2'][d], 'file2'), + FileExists()) + + self.assertThat(os.path.join(self.snap_bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) + self.assertThat(os.path.join(self.stage_bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) + + def test_clean_pull_step(self): + self.assert_files_exist() + + output = self.run_snapcraft(['clean', '--step=pull'], debug=False) + self.assertThat(self.stage_dir, Not(DirExists())) + self.assertThat(self.prime_dir, Not(DirExists())) + + for part_name, part in self.parts.items(): + self.assertThat(part['builddir'], Not(DirExists())) + self.assertThat(part['installdir'], Not(DirExists())) + self.assertThat(part['sourcedir'], Not(DirExists())) + + # Assert that the priming and staging areas were removed wholesale, not + # a part at a time (since we didn't specify any parts). + output = output.strip().split('\n') + self.expectThat(output, Equals([ + 'Cleaning up priming area', + 'Cleaning up staging area', + 'Cleaning up parts directory' + ])) + + # Now try to prime again + self.run_snapcraft('prime') + self.assert_files_exist() + + def test_clean_pull_step_single_part(self): + self.assert_files_exist() + + self.run_snapcraft(['clean', 'part1', '--step=pull']) + self.assertThat(os.path.join(self.stage_bindir, 'file1'), + Not(FileExists())) + self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) + self.assertThat(os.path.join(self.snap_bindir, 'file1'), + Not(FileExists())) + self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) + + self.assertThat(self.parts['part1']['builddir'], Not(DirExists())) + self.assertThat(self.parts['part1']['installdir'], Not(DirExists())) + self.assertThat(self.parts['part1']['sourcedir'], Not(DirExists())) + + self.assertThat( + os.path.join(self.parts['part2']['builddir'], 'file2'), + FileExists()) + self.assertThat( + os.path.join(self.parts['part2']['bindir'], 'file2'), + FileExists()) + self.assertThat( + os.path.join(self.parts['part2']['sourcedir'], 'file2'), + FileExists()) + + # Now try to prime again + self.run_snapcraft('prime') + self.assert_files_exist() diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_clean.py snapcraft-2.41/tests/integration/lifecycle/test_clean.py --- snapcraft-2.40/tests/integration/lifecycle/test_clean.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_clean.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,77 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2018 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 ( + Contains, + DirExists, + Equals, + Not +) + +from tests import integration + + +class CleanTestCase(integration.TestCase): + + def test_clean(self): + self.copy_project_to_cwd('make-hello') + self.run_snapcraft('snap') + + snap_dirs = (self.parts_dir, self.stage_dir, self.prime_dir) + for dir_ in snap_dirs: + self.assertThat(dir_, DirExists()) + + self.run_snapcraft('clean') + for dir_ in snap_dirs: + self.assertThat(dir_, Not(DirExists())) + + def _clean_invalid_part(self, debug): + self.copy_project_to_cwd('make-hello') + self.run_snapcraft('snap') + + raised = self.assertRaises( + subprocess.CalledProcessError, self.run_snapcraft, + ['clean', 'invalid-part'], debug=debug) + self.assertThat(raised.returncode, Equals(2)) + self.assertThat( + raised.output, + Contains("The part named 'invalid-part' is not defined")) + return raised.output + + def test_clean_invalid_part_no_traceback_without_debug(self): + self.assertThat( + self._clean_invalid_part(False), Not(Contains("Traceback"))) + + def test_clean_invalid_part_traceback_with_debug(self): + self.assertThat( + self._clean_invalid_part(True), Contains("Traceback")) + + def test_clean_again(self): + # Clean a second time doesn't fail. + # Regression test for https://bugs.launchpad.net/snapcraft/+bug/1497371 + self.copy_project_to_cwd('make-hello') + self.run_snapcraft('snap') + self.run_snapcraft('clean') + self.run_snapcraft('clean') + + # Regression test for LP: #1596596 + def test_clean_invalid_yaml(self): + self.run_snapcraft('clean', 'invalid-snap') + self.assertThat(self.parts_dir, Not(DirExists())) + self.assertThat(self.stage_dir, Not(DirExists())) + self.assertThat(self.prime_dir, Not(DirExists())) diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_clean_stage_step.py snapcraft-2.41/tests/integration/lifecycle/test_clean_stage_step.py --- snapcraft-2.40/tests/integration/lifecycle/test_clean_stage_step.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_clean_stage_step.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,117 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2018 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, + DirExists, + FileExists, + Not +) + +from tests import integration + + +class CleanStageStepStagedTestCase(integration.TestCase): + + def setUp(self): + super().setUp() + + self.copy_project_to_cwd('independent-parts') + self.run_snapcraft('stage') + self.bindir = os.path.join(self.stage_dir, 'bin') + + def assert_files_exist(self): + self.assertThat(os.path.join(self.bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(self.bindir, 'file2'), FileExists()) + + def test_clean_stage_step(self): + self.assert_files_exist() + + output = self.run_snapcraft( + ['clean', '--step=stage'], debug=False) + self.assertThat(self.stage_dir, Not(DirExists())) + self.assertThat(self.parts_dir, DirExists()) + + # Assert that the priming and staging areas were removed wholesale, not + # a part at a time (since we didn't specify any parts). + self.assertThat(output, Contains("Cleaning up priming area")) + self.assertThat(output, Contains("Cleaning up staging area")) + self.expectThat(output, Not(Contains('part1'))) + self.expectThat(output, Not(Contains('part2'))) + + # Now try to stage again + self.run_snapcraft('stage') + self.assert_files_exist() + + def test_clean_stage_step_single_part(self): + self.assert_files_exist() + + self.run_snapcraft(['clean', 'part1', '--step=stage']) + self.assertThat(os.path.join(self.bindir, 'file1'), Not(FileExists())) + self.assertThat(os.path.join(self.bindir, 'file2'), FileExists()) + self.assertThat(self.parts_dir, DirExists()) + + # Now try to stage again + self.run_snapcraft('stage') + self.assert_files_exist() + + +class CleanStageStepPrimedTestCase(integration.TestCase): + + def setUp(self): + super().setUp() + + self.copy_project_to_cwd('independent-parts') + self.run_snapcraft('prime') + + self.snap_bindir = os.path.join(self.prime_dir, 'bin') + self.stage_bindir = os.path.join(self.stage_dir, 'bin') + + def assert_files_exist(self): + self.assertThat(os.path.join(self.snap_bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) + self.assertThat(os.path.join(self.stage_bindir, 'file1'), FileExists()) + self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) + + def test_clean_stage_step(self): + self.assert_files_exist() + + self.run_snapcraft(['clean', '--step=stage']) + self.assertThat(self.stage_dir, Not(DirExists())) + self.assertThat(self.prime_dir, Not(DirExists())) + self.assertThat(self.parts_dir, DirExists()) + + # Now try to prime again + self.run_snapcraft('prime') + self.assert_files_exist() + + def test_clean_stage_step_single_part(self): + self.assert_files_exist() + + self.run_snapcraft(['clean', 'part1', '--step=stage']) + self.assertThat(os.path.join(self.stage_bindir, 'file1'), + Not(FileExists())) + self.assertThat(os.path.join(self.stage_bindir, 'file2'), FileExists()) + self.assertThat(os.path.join(self.snap_bindir, 'file1'), + Not(FileExists())) + self.assertThat(os.path.join(self.snap_bindir, 'file2'), FileExists()) + self.assertThat(self.parts_dir, DirExists()) + + # Now try to prime again + self.run_snapcraft('prime') + self.assert_files_exist() diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_prime.py snapcraft-2.41/tests/integration/lifecycle/test_prime.py --- snapcraft-2.40/tests/integration/lifecycle/test_prime.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_prime.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,217 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2018 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 textwrap import dedent + +import fixtures +import testscenarios +from testtools.matchers import ( + Contains, + Equals, + FileContains, + FileExists, + MatchesRegex, + Not, + StartsWith, +) + +from tests import integration, fixture_setup + + +class PrimeTestCase(integration.TestCase): + + def test_classic_confinement(self): + if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': + self.skipTest("The autopkgtest armhf runners can't install snaps") + project_dir = 'classic-build' + + # The first run should fail as the environment variable is not + # set but we can only test this on clean systems. + if not os.path.exists(os.path.join( + os.path.sep, 'snap', 'core', 'current')): + try: + self.run_snapcraft(['prime'], project_dir) + except subprocess.CalledProcessError: + pass + else: + self.fail( + 'This should fail as SNAPCRAFT_SETUP_CORE is not set') + + # Now we set the required environment variable + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_SETUP_CORE', '1')) + + self.run_snapcraft(['prime'], project_dir) + + bin_path = os.path.join(self.prime_dir, 'bin', 'hello-classic') + self.assertThat(bin_path, FileExists()) + + interpreter = subprocess.check_output([ + self.patchelf_command, '--print-interpreter', bin_path]).decode() + expected_interpreter = r'^/snap/core/current/.*' + self.assertThat(interpreter, MatchesRegex(expected_interpreter)) + + # We check stage to make sure the hard link is broken. + staged_bin_path = os.path.join(self.stage_dir, 'bin', 'hello-classic') + self.assertThat(staged_bin_path, FileExists()) + + staged_interpreter = subprocess.check_output([ + self.patchelf_command, '--print-interpreter', + staged_bin_path]).decode() + self.assertThat(staged_interpreter, MatchesRegex(r'^/lib.*')) + + def test_classic_confinement_patchelf_disabled(self): + if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': + self.skipTest("The autopkgtest armhf runners can't install snaps") + project_dir = 'classic-build' + + # Now we set the required environment variable + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_SETUP_CORE', '1')) + + self.copy_project_to_cwd(project_dir) + + # Create a new snapcraft.yaml + snapcraft_yaml = fixture_setup.SnapcraftYaml( + self.path, confinement='classic') + snapcraft_yaml.update_part('hello', { + 'source': '.', + 'plugin': 'make', + 'build-attributes': ['no-patchelf'] + }) + self.useFixture(snapcraft_yaml) + + self.run_snapcraft('prime') + + bin_path = os.path.join(self.prime_dir, 'bin', 'hello-classic') + self.assertThat(bin_path, FileExists()) + + interpreter = subprocess.check_output([ + self.patchelf_command, '--print-interpreter', bin_path]).decode() + self.assertThat(interpreter, StartsWith('/lib')) + + def test_classic_confinement_with_existing_rpath(self): + if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': + self.skipTest("The autopkgtest armhf runners can't install snaps") + project_dir = 'classic-build-existing-rpath' + + # The first run should fail as the environment variable is not + # set but we can only test this on clean systems. + if not os.path.exists(os.path.join( + os.path.sep, 'snap', 'core', 'current')): + try: + self.run_snapcraft(['prime'], project_dir) + except subprocess.CalledProcessError: + pass + else: + self.fail( + 'This should fail as SNAPCRAFT_SETUP_CORE is not set') + + # Now we set the required environment variable + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_SETUP_CORE', '1')) + + self.run_snapcraft(['prime'], project_dir) + + bin_path = os.path.join(self.prime_dir, 'bin', 'hello-classic') + self.assertThat(bin_path, FileExists()) + + rpath = subprocess.check_output([ + self.patchelf_command, '--print-rpath', bin_path]).decode().strip() + expected_rpath = '$ORIGIN/../fake-lib:/snap/core/current/' + self.assertThat(rpath, StartsWith(expected_rpath)) + + def test_prime_includes_stage_fileset(self): + self.run_snapcraft('prime', 'prime-from-stage') + self.assertThat( + os.path.join(self.prime_dir, 'without-a'), + FileExists()) + self.assertThat( + os.path.join(self.prime_dir, 'without-b'), + Not(FileExists())) + self.assertThat( + os.path.join(self.prime_dir, 'without-c'), + FileExists()) + + def test_prime_includes_stage_excludes_fileset(self): + self.run_snapcraft('prime', 'prime-from-stage') + self.assertThat( + os.path.join(self.prime_dir, 'with-a'), + Not(FileExists())) + self.assertThat( + os.path.join(self.prime_dir, 'with-b'), + FileExists()) + self.assertThat( + os.path.join(self.prime_dir, 'with-c'), + FileExists()) + + def test_prime_with_non_ascii_desktop_file(self): + # Originally, in this test we forced LC_ALL=C. However, now that we + # are using the click python library we can't do it because it fails + # to run any command when the system language is ascii. + # --20170518 - elopio + self.run_snapcraft('prime', 'desktop-with-non-ascii') + + desktop_path = os.path.join( + self.prime_dir, 'meta', 'gui', 'test-app.desktop') + + self.expectThat( + desktop_path, FileContains(matcher=Contains('non ascíí'))) + + def _prime_invalid_part(self, debug): + exception = self.assertRaises( + subprocess.CalledProcessError, + self.run_snapcraft, ['prime', 'invalid-part-name'], + 'prime-from-stage', debug=debug) + + self.assertThat(exception.returncode, Equals(2)) + self.assertThat(exception.output, Contains( + "part named 'invalid-part-name' is not defined")) + + return exception.output + + def test_prime_invalid_part_no_traceback_without_debug(self): + self.assertThat( + self._prime_invalid_part(False), Not(Contains("Traceback"))) + + def test_prime_invalid_part_does_traceback_with_debug(self): + self.assertThat( + self._prime_invalid_part(True), Contains("Traceback")) + + +class PrimedAssetsTestCase(testscenarios.WithScenarios, + integration.TestCase): + + scenarios = [ + ('setup', dict(project_dir='assets-with-gui-in-setup')), + ('snap', dict(project_dir='assets-with-gui-in-snap')), + ] + + def test_assets_in_meta(self): + self.run_snapcraft('prime', self.project_dir) + + gui_dir = os.path.join(self.prime_dir, 'meta', 'gui') + expected_desktop = dedent("""\ + [Desktop Entry] + Name=My App + Exec=my-app + Type=Application + """) + self.expectThat(os.path.join(gui_dir, 'icon.png'), FileExists()) + self.expectThat(os.path.join(gui_dir, 'my-app.desktop'), + FileContains(expected_desktop)) diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_pull_properties.py snapcraft-2.41/tests/integration/lifecycle/test_pull_properties.py --- snapcraft-2.40/tests/integration/lifecycle/test_pull_properties.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_pull_properties.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,306 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2018 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 collections import namedtuple +import os +import subprocess +import yaml + +import testscenarios +from testtools.matchers import ( + Equals, + FileExists +) + +from tests import ( + fixture_setup, + integration +) + + +class PullPropertiesTestCase(integration.TestCase): + + def test_pull(self): + self.assert_expected_pull_state('local-plugin-pull-properties') + + def test_pull_legacy_pull_properties(self): + self.assert_expected_pull_state('local-plugin-legacy-pull-properties') + + def assert_expected_pull_state(self, project_dir): + self.run_snapcraft('pull', project_dir) + + state_file = os.path.join( + self.parts_dir, 'x-local-plugin', 'state', 'pull') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + + # Verify that the correct schema dependencies made it into the state. + self.assertTrue('foo' in state.schema_properties) + self.assertTrue('stage-packages' in state.schema_properties) + + # Verify that the contents of the dependencies made it in as well. + self.assertTrue('foo' in state.properties) + self.assertTrue(len(state.assets['stage-packages']) > 0) + self.assertIn('build-packages', state.assets) + self.assertTrue('stage-packages' in state.properties) + self.assertThat(state.properties['foo'], Equals('bar')) + self.assertThat(state.properties['stage-packages'], Equals(['curl'])) + + def test_pull_with_arch(self): + if self.deb_arch == 'armhf': + self.skipTest('For now, we just support crosscompile from amd64') + self.run_snapcraft(['pull', '--target-arch=i386', 'go-hello'], + 'go-hello') + state_file = os.path.join(self.parts_dir, + 'go-hello', 'state', 'pull') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + self.assertThat(state.project_options['deb_arch'], Equals('i386')) + + def test_arch_with_pull(self): + if self.deb_arch == 'armhf': + self.skipTest('For now, we just support crosscompile from amd64') + self.run_snapcraft(['--target-arch=i386', 'pull', 'go-hello'], + 'go-hello') + state_file = os.path.join(self.parts_dir, + 'go-hello', 'state', 'pull') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + self.assertThat(state.project_options['deb_arch'], Equals('i386')) + + +class AssetTrackingTestCase(integration.TestCase): + + def test_pull(self): + self.copy_project_to_cwd('asset-tracking') + stage_version = self.set_stage_package_version( + 'snapcraft.yaml', part='asset-tracking', package='hello') + build_version = self.set_build_package_version( + 'snapcraft.yaml', part='asset-tracking', package='hello') + + self.run_snapcraft('pull') + + state_file = os.path.join( + self.parts_dir, 'asset-tracking', 'state', 'pull') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + + # Verify that the correct version of 'hello' is installed + self.assertTrue(len(state.assets['stage-packages']) > 0) + self.assertTrue(len(state.assets['build-packages']) > 0) + self.assertIn( + 'hello={}'.format(stage_version), state.assets['stage-packages']) + self.assertIn( + 'hello={}'.format(build_version), state.assets['build-packages']) + self.assertIn('source-details', state.assets) + + def test_pull_global_build_packages_are_excluded(self): + """ + Ensure global build-packages are not included in each part's + build-packages data. + """ + self.copy_project_to_cwd('build-package-global') + self.set_build_package_version( + os.path.join('snap', 'snapcraft.yaml'), + part=None, package='haskell-doc') + self.run_snapcraft('pull') + + state_file = os.path.join( + self.parts_dir, 'empty-part', 'state', 'pull') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + + self.assertTrue(len(state.assets['build-packages']) == 0) + + def test_pull_build_package_with_any_architecture(self): + self.copy_project_to_cwd('build-package') + self.set_build_package_architecture( + os.path.join('snap', 'snapcraft.yaml'), + part='hello', package='hello', architecture='any') + self.run_snapcraft('pull') + + state_file = os.path.join( + self.parts_dir, 'hello', 'state', 'pull') + with open(state_file) as f: + state = yaml.load(f) + self.assertIn('hello', state.assets['build-packages'][0]) + + def test_pull_with_virtual_build_package(self): + virtual_package = 'fortunes-off' + self.addCleanup( + subprocess.call, ['sudo', 'apt-get', 'remove', virtual_package]) + self.run_snapcraft('pull', 'build-virtual-package') + + state_file = os.path.join( + 'snap', '.snapcraft', 'state') + with open(state_file) as f: + state = yaml.load(f) + self.assertIn( + '{}={}'.format( + virtual_package, integration.get_package_version( + virtual_package, self.distro_series, self.deb_arch)), + state.assets['build-packages']) + + +TestDetail = namedtuple('TestDetail', ['field', 'value']) + + +class GitAssetTrackingTestCase(testscenarios.WithScenarios, + integration.TestCase): + + scenarios = [ + ('plain', { + 'part_name': 'git-part', + 'expected_details': None, + }), + ('branch', { + 'part_name': 'git-part-branch', + 'expected_details': TestDetail('source-branch', 'test-branch'), + }), + ('tag', { + 'part_name': 'git-part-tag', + 'expected_details': TestDetail('source-tag', 'feature-tag'), + }), + ] + + def test_pull_git(self): + repo_fixture = fixture_setup.GitRepo() + self.useFixture(repo_fixture) + project_dir = 'asset-tracking-git' + + self.run_snapcraft(['pull', self.part_name], project_dir) + + state_file = os.path.join( + self.parts_dir, self.part_name, 'state', 'pull') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + + self.assertIn('source-details', state.assets) + + # fall back to the commit if no other source option is provided + # snapcraft.source.Git doesn't allow both a tag and a commit + if self.expected_details: + self.assertThat( + state.assets['source-details'][self.expected_details.field], + Equals(self.expected_details.value)) + else: + self.assertThat( + state.assets['source-details']['source-commit'], + Equals(repo_fixture.commit)) + + +class BazaarAssetTrackingTestCase(testscenarios.WithScenarios, + integration.TestCase): + scenarios = [ + ('plain', { + 'part_name': 'bzr-part', + 'expected_details': None, + }), + ('tag', { + 'part_name': 'bzr-part-tag', + 'expected_details': TestDetail('source-tag', 'feature-tag'), + }), + ] + + def test_pull_bzr(self): + repo_fixture = fixture_setup.BzrRepo('bzr-source') + self.useFixture(repo_fixture) + project_dir = 'asset-tracking-bzr' + part = self.part_name + self.run_snapcraft(['pull', part], project_dir) + + state_file = os.path.join( + self.parts_dir, part, 'state', 'pull') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + + self.assertIn('source-details', state.assets) + + if self.expected_details: + self.assertThat( + state.assets['source-details'][self.expected_details.field], + Equals(self.expected_details.value)) + else: + self.assertThat( + state.assets['source-details']['source-commit'], + Equals(repo_fixture.commit)) + + +class MercurialAssetTrackingTestCase(testscenarios.WithScenarios, + integration.TestCase): + scenarios = [ + ('plain', { + 'part_name': 'hg-part', + 'expected_details': None, + }), + ('tag', { + 'part_name': 'hg-part-tag', + 'expected_details': TestDetail('source-tag', 'feature-tag'), + }), + ] + + def test_pull_hg(self): + repo_fixture = fixture_setup.HgRepo('hg-source') + self.useFixture(repo_fixture) + project_dir = 'asset-tracking-hg' + part = self.part_name + self.run_snapcraft(['pull', part], project_dir) + + state_file = os.path.join( + self.parts_dir, part, 'state', 'pull') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + + self.assertIn('source-details', state.assets) + + if self.expected_details: + self.assertThat( + state.assets['source-details'][self.expected_details.field], + Equals(self.expected_details.value)) + else: + self.assertThat( + state.assets['source-details']['source-commit'], + Equals(repo_fixture.commit)) + + +class SubversionAssetTrackingTestCase(integration.TestCase): + + def test_pull_svn(self): + repo_fixture = fixture_setup.SvnRepo('svn-source') + self.useFixture(repo_fixture) + project_dir = 'asset-tracking-svn' + part = 'svn-part' + expected_commit = repo_fixture.commit + self.run_snapcraft(['pull', part], project_dir) + + state_file = os.path.join( + self.parts_dir, part, 'state', 'pull') + self.assertThat(state_file, FileExists()) + with open(state_file) as f: + state = yaml.load(f) + + self.assertIn('source-details', state.assets) + self.assertThat( + state.assets['source-details']['source-commit'], + Equals(expected_commit)) diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_pull.py snapcraft-2.41/tests/integration/lifecycle/test_pull.py --- snapcraft-2.40/tests/integration/lifecycle/test_pull.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_pull.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,48 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017-2018 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 ( + Contains, + Equals, + Not, +) + +from tests import integration + + +class PullTestCase(integration.TestCase): + + def _pull_invalid_part(self, debug): + exception = self.assertRaises( + subprocess.CalledProcessError, + self.run_snapcraft, ['pull', 'invalid-part-name'], + 'go-hello', debug=debug) + + self.assertThat(exception.returncode, Equals(2)) + self.assertThat(exception.output, Contains( + "part named 'invalid-part-name' is not defined")) + + return exception.output + + def test_pull_invalid_part_no_traceback_without_debug(self): + self.assertThat( + self._pull_invalid_part(False), Not(Contains("Traceback"))) + + def test_pull_invalid_part_does_traceback_with_debug(self): + self.assertThat( + self._pull_invalid_part(True), Contains("Traceback")) diff -Nru snapcraft-2.40/tests/integration/lifecycle/test_snap.py snapcraft-2.41/tests/integration/lifecycle/test_snap.py --- snapcraft-2.40/tests/integration/lifecycle/test_snap.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_snap.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,264 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2018 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 textwrap import dedent + +import fixtures +import testtools +from testtools.matchers import ( + Equals, + EndsWith, + FileContains, + FileExists, + Not, +) + +from tests import integration + + +class SnapTestCase(integration.TestCase): + + def test_snap(self): + self.copy_project_to_cwd('assemble') + self.run_snapcraft('snap') + + snap_file_path = 'assemble_1.0_{}.snap'.format(self.deb_arch) + self.assertThat(snap_file_path, FileExists()) + + binary1_wrapper_path = os.path.join( + self.prime_dir, 'command-assemble-bin.wrapper') + with open('binary1.after', 'r') as file_: + expected_binary1_wrapper = file_.read() + self.assertThat( + binary1_wrapper_path, FileContains(expected_binary1_wrapper)) + + self.useFixture( + fixtures.EnvironmentVariable( + 'SNAP', os.path.join(os.getcwd(), self.prime_dir))) + binary_scenarios = ( + ('command-assemble-service.wrapper', 'service-start\n'), + ('stop-command-assemble-service.wrapper', 'service-stop\n'), + ('command-assemble-bin.wrapper', 'binary1\n'), + ('command-binary2.wrapper', 'binary2\n'), + ) + for binary, expected_output in binary_scenarios: + output = subprocess.check_output( + os.path.join(self.prime_dir, binary), universal_newlines=True) + self.assertThat(output, Equals(expected_output)) + + with testtools.ExpectedException(subprocess.CalledProcessError): + subprocess.check_output( + os.path.join(self.prime_dir, 'bin', 'not-wrapped'), + stderr=subprocess.STDOUT) + + self.assertThat( + 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())) + + # LP: #1750658 + self.assertThat( + os.path.join(self.prime_dir, 'meta', 'snap.yaml'), + FileContains(dedent("""\ + name: assemble + version: 1.0 + summary: one line summary + description: a longer description + architectures: + - {} + confinement: strict + grade: stable + apps: + assemble-bin: + command: command-assemble-bin.wrapper + assemble-service: + command: command-assemble-service.wrapper + daemon: simple + stop-command: stop-command-assemble-service.wrapper + binary-wrapper-none: + command: subdir/binary3 + binary2: + command: command-binary2.wrapper + """).format(self.deb_arch))) + + def test_snap_default(self): + self.copy_project_to_cwd('assemble') + self.run_snapcraft([]) + 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') + + 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 snap ` are always in + # sync). + 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()) + + def test_snap_short_output_option(self): + self.run_snapcraft(['snap', '-o', 'mysnap.snap'], 'assemble') + self.assertThat('mysnap.snap', FileExists()) + + def test_error_with_unexistent_build_package(self): + self.copy_project_to_cwd('assemble') + with open('snapcraft.yaml', 'a') as yaml_file: + yaml_file.write('build-packages:\n' + ' - inexistent-package\n') + + # We update here to get a clean log/stdout later + self.run_snapcraft('update') + + exception = self.assertRaises( + subprocess.CalledProcessError, self.run_snapcraft, 'snap') + expected = ( + "Could not find a required package in 'build-packages': " + "inexistent-package\n") + self.assertThat(exception.output, EndsWith(expected)) + + def test_snap_with_exposed_files(self): + self.copy_project_to_cwd('nil-plugin-pkgfilter') + self.run_snapcraft('stage') + self.assertThat( + os.path.join(self.stage_dir, 'usr', 'bin', 'nmcli'), + FileExists()) + + self.run_snapcraft('snap') + self.assertThat( + os.path.join(self.prime_dir, 'usr', 'bin', 'nmcli'), + FileExists()) + self.assertThat( + os.path.join(self.prime_dir, 'usr', 'bin', 'nmtui'), + Not(FileExists())) + + def test_snap_from_snapcraft_init(self): + self.assertThat('snapcraft.yaml', Not(FileExists())) + self.run_snapcraft('init') + self.assertThat(os.path.join('snap', 'snapcraft.yaml'), FileExists()) + + self.run_snapcraft('snap') + + def test_snap_with_arch(self): + if self.deb_arch == 'armhf': + self.skipTest('For now, we just support crosscompile from amd64') + self.run_snapcraft('init') + + self.run_snapcraft(['snap', '--target-arch=i386']) + self.assertThat('my-snap-name_0.1_i386.snap', FileExists()) + + def test_arch_with_snap(self): + if self.deb_arch == 'armhf': + self.skipTest('For now, we just support crosscompile from amd64') + self.run_snapcraft('init') + + self.run_snapcraft(['--target-arch=i386', 'snap']) + self.assertThat('my-snap-name_0.1_i386.snap', FileExists()) + + def test_implicit_command_with_arch(self): + if self.deb_arch == 'armhf': + self.skipTest('For now, we just support crosscompile from amd64') + self.run_snapcraft('init') + + self.run_snapcraft('--target-arch=i386') + self.assertThat('my-snap-name_0.1_i386.snap', FileExists()) + + def test_error_on_bad_yaml(self): + error = self.assertRaises( + subprocess.CalledProcessError, + self.run_snapcraft, 'stage', 'bad-yaml') + self.assertIn( + "Issues while validating snapcraft.yaml: found character '\\t' " + "that cannot start any token on line 13 of snapcraft.yaml", + str(error.output)) + + def test_yaml_merge_tag(self): + self.copy_project_to_cwd('yaml-merge-tag') + self.run_snapcraft('stage') + self.assertThat( + 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.40/tests/integration/lifecycle/test_stage.py snapcraft-2.41/tests/integration/lifecycle/test_stage.py --- snapcraft-2.40/tests/integration/lifecycle/test_stage.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/lifecycle/test_stage.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,166 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2018 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 testtools.matchers import ( + Contains, + Equals, + FileExists, + Not +) + +from tests import integration + + +class StageTestCase(integration.TestCase): + + def _stage_conflicts(self, debug): + exception = self.assertRaises( + subprocess.CalledProcessError, + self.run_snapcraft, 'stage', 'conflicts', debug=debug) + + self.assertThat(exception.returncode, Equals(2)) + expected_conflicts = ( + "Failed to stage: " + "Parts 'p1' and 'p2' have the following files, but with different " + "contents:\n bin/test\n") + self.assertThat(exception.output, Contains(expected_conflicts)) + + expected_help = ( + 'Snapcraft offers some capabilities to solve this by use ' + 'of the following keywords:\n' + ' - `filesets`\n' + ' - `stage`\n' + ' - `snap`\n' + ' - `organize`\n\n' + 'To learn more about these part keywords, run ' + '`snapcraft help plugins`.' + ) + self.assertThat(exception.output, Contains(expected_help)) + return exception.output + + def test_conflicts_no_traceback_without_debug(self): + self.assertThat( + self._stage_conflicts(False), Not(Contains("Traceback"))) + + def test_conflicts_traceback_with_debug(self): + self.assertThat( + self._stage_conflicts(True), Contains("Traceback")) + + def test_conflicts(self): + exception = self.assertRaises( + subprocess.CalledProcessError, + self.run_snapcraft, 'stage', 'conflicts') + + self.assertThat(exception.returncode, Equals(2)) + expected_conflicts = ( + "Failed to stage: " + "Parts 'p1' and 'p2' have the following files, but with different " + "contents:\n bin/test\n") + self.assertThat(exception.output, Contains(expected_conflicts)) + + expected_help = ( + 'Snapcraft offers some capabilities to solve this by use ' + 'of the following keywords:\n' + ' - `filesets`\n' + ' - `stage`\n' + ' - `snap`\n' + ' - `organize`\n\n' + 'To learn more about these part keywords, run ' + '`snapcraft help plugins`.' + ) + self.assertThat(exception.output, Contains(expected_help)) + + def test_no_conflicts(self): + self.run_snapcraft(['stage'], 'organize-no-conflicts') + self.assertThat(os.path.join(self.stage_dir, 'file'), FileExists()) + self.assertThat(os.path.join(self.stage_dir, 'file2'), FileExists()) + + def test_stage_twice(self): + """Test that snap that uses organize can be staged twice""" + self.run_snapcraft(['stage'], 'stage-twice') + self.assertThat( + os.path.join(self.stage_dir, 'dir', 'dir', 'file'), FileExists()) + + # Now clean, and stage again + self.run_snapcraft(['clean', '--step=stage'], 'stage-twice') + self.run_snapcraft(['stage'], 'stage-twice') + self.assertThat( + os.path.join(self.stage_dir, 'dir', 'dir', 'file'), FileExists()) + + def test_classic_confinement(self): + if os.environ.get('ADT_TEST') and self.deb_arch == 'armhf': + self.skipTest("The autopkgtest armhf runners can't install snaps") + project_dir = 'classic-build' + + # The first run should fail as the environment variable is not + # set but we can only test this on clean systems. + if not os.path.exists(os.path.join( + os.path.sep, 'snap', 'core', 'current')): + try: + self.run_snapcraft(['stage'], project_dir) + except subprocess.CalledProcessError: + pass + else: + self.fail( + 'This should fail as SNAPCRAFT_SETUP_CORE is not set') + + # Now we set the required environment variable + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_SETUP_CORE', '1')) + + self.run_snapcraft(['stage'], project_dir) + bin_path = os.path.join(self.stage_dir, 'bin', 'hello-classic') + self.assertThat(bin_path, FileExists()) + + # ld-linux will not be set until everything is primed. + interpreter = subprocess.check_output([ + self.patchelf_command, '--print-interpreter', bin_path]).decode() + self.assertThat(interpreter, Not(Contains('/snap/core/current'))) + + def test_staging_libc_links(self): + project_dir = 'staging_links_to_libc' + + # First, stage libc6-dev via stage-packages + self.run_snapcraft(['stage', 'from-package'], project_dir) + + # Now tar up the staging area + subprocess.check_call(['tar', 'cf', 'stage.tar', 'stage/']) + + # Now attempt to stage the tarred staging area once again. This should + # not conflict. + try: + self.run_snapcraft(['stage', 'from-tar'], project_dir) + except subprocess.CalledProcessError as e: + if 'have the following file paths in common' in e.output: + self.fail('Parts unexpectedly conflicted') + else: + raise + + def symlinks_to_libc_should_build(self): + """Regression test for LP: #1665089""" + + # This will fail to build if the libc symlinks are missing + self.run_snapcraft('stage', 'use_libc_dl') + + def test_stage_with_file_to_check_for_collisions_not_build(self): + """Regression test for LP: #1660696""" + # This will fail if we try to check for collisions even in parts that + # haven't been build. + self.run_snapcraft('stage', 'stage-with-two-equal-files') diff -Nru snapcraft-2.40/tests/integration/snaps/old-part-src/parts/part-name/state/pull snapcraft-2.41/tests/integration/snaps/old-part-src/parts/part-name/state/pull --- snapcraft-2.40/tests/integration/snaps/old-part-src/parts/part-name/state/pull 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/old-part-src/parts/part-name/state/pull 2018-04-14 12:13:35.000000000 +0000 @@ -11,4 +11,5 @@ source-tag: '' source-type: '' stage-packages: [] + override-pull: 'snapcraftctl pull' schema_properties: [] diff -Nru snapcraft-2.40/tests/integration/snaps/organize-no-conflicts/snap/snapcraft.yaml snapcraft-2.41/tests/integration/snaps/organize-no-conflicts/snap/snapcraft.yaml --- snapcraft-2.40/tests/integration/snaps/organize-no-conflicts/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/organize-no-conflicts/snap/snapcraft.yaml 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,19 @@ +name: organize-no-conflicts +version: '0.1' # just for humans, typically '1.2+git' or '1.3.2' +summary: Test snapcraft's organize keyword +description: | + No parts should conflict with each other unless `organize` is broken. + +grade: devel # must be 'stable' to release into candidate/stable channels +confinement: devmode # use 'strict' once you have the right plugs and slots + +parts: + part1: + plugin: dump + source: src1/ + organize: + file: file2 + + part2: + plugin: dump + source: src2/ diff -Nru snapcraft-2.40/tests/integration/snaps/organize-no-conflicts/src1/file snapcraft-2.41/tests/integration/snaps/organize-no-conflicts/src1/file --- snapcraft-2.40/tests/integration/snaps/organize-no-conflicts/src1/file 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/organize-no-conflicts/src1/file 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1 @@ +1 diff -Nru snapcraft-2.40/tests/integration/snaps/organize-no-conflicts/src2/file snapcraft-2.41/tests/integration/snaps/organize-no-conflicts/src2/file --- snapcraft-2.40/tests/integration/snaps/organize-no-conflicts/src2/file 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/organize-no-conflicts/src2/file 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1 @@ +2 diff -Nru snapcraft-2.40/tests/integration/snaps/scriptlet-override-build/file snapcraft-2.41/tests/integration/snaps/scriptlet-override-build/file --- snapcraft-2.40/tests/integration/snaps/scriptlet-override-build/file 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/scriptlet-override-build/file 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1 @@ +I'm a nice file diff -Nru snapcraft-2.40/tests/integration/snaps/scriptlet-override-build/snap/snapcraft.yaml snapcraft-2.41/tests/integration/snaps/scriptlet-override-build/snap/snapcraft.yaml --- snapcraft-2.40/tests/integration/snaps/scriptlet-override-build/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/scriptlet-override-build/snap/snapcraft.yaml 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,17 @@ +name: override-build-scriptlet-test +version: '0.1' +summary: Runs the override-build scriptlet for a part +description: | + Runs the shell script defined in `override-build` instead of plugin build. +grade: devel +confinement: devmode + +parts: + override-build-scriptlet-test: + plugin: dump + override-build: | + touch before-build + touch $SNAPCRAFT_PART_INSTALL/before-install + snapcraftctl build + touch after-build + touch $SNAPCRAFT_PART_INSTALL/after-install diff -Nru snapcraft-2.40/tests/integration/snaps/scriptlet-override-prime/part1/file snapcraft-2.41/tests/integration/snaps/scriptlet-override-prime/part1/file --- snapcraft-2.40/tests/integration/snaps/scriptlet-override-prime/part1/file 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/scriptlet-override-prime/part1/file 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1 @@ +I'm a nice file diff -Nru snapcraft-2.40/tests/integration/snaps/scriptlet-override-prime/part2/file2 snapcraft-2.41/tests/integration/snaps/scriptlet-override-prime/part2/file2 --- snapcraft-2.40/tests/integration/snaps/scriptlet-override-prime/part2/file2 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/scriptlet-override-prime/part2/file2 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1 @@ +I'm a nicer file diff -Nru snapcraft-2.40/tests/integration/snaps/scriptlet-override-prime/snap/snapcraft.yaml snapcraft-2.41/tests/integration/snaps/scriptlet-override-prime/snap/snapcraft.yaml --- snapcraft-2.40/tests/integration/snaps/scriptlet-override-prime/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/scriptlet-override-prime/snap/snapcraft.yaml 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,23 @@ +name: override-prime-scriptlet-test +version: '0.1' +summary: Runs the override-prime scriptlet for a part +description: | + Runs the shell script defined in `override-prime` instead of normal prime. +grade: devel +confinement: devmode + +parts: + override-prime-scriptlet-test: + plugin: dump + source: part1/ + override-prime: | + touch before-prime + snapcraftctl prime + touch after-prime + + override-prime-do-nothing: + plugin: dump + source: part2/ + override-prime: | + # Completely skip prime + exit 0 diff -Nru snapcraft-2.40/tests/integration/snaps/scriptlet-override-pull/file snapcraft-2.41/tests/integration/snaps/scriptlet-override-pull/file --- snapcraft-2.40/tests/integration/snaps/scriptlet-override-pull/file 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/scriptlet-override-pull/file 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1 @@ +I'm a nice file diff -Nru snapcraft-2.40/tests/integration/snaps/scriptlet-override-pull/snap/snapcraft.yaml snapcraft-2.41/tests/integration/snaps/scriptlet-override-pull/snap/snapcraft.yaml --- snapcraft-2.40/tests/integration/snaps/scriptlet-override-pull/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/scriptlet-override-pull/snap/snapcraft.yaml 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,15 @@ +name: override-pull-scriptlet-test +version: '0.1' +summary: Runs the override-build scriptlet for a part +description: | + Runs the shell script defined in `override-build` instead of plugin build. +grade: devel +confinement: devmode + +parts: + override-pull-scriptlet-test: + plugin: dump + override-pull: | + touch before-pull + snapcraftctl pull + touch after-pull diff -Nru snapcraft-2.40/tests/integration/snaps/scriptlet-override-stage/part1/file snapcraft-2.41/tests/integration/snaps/scriptlet-override-stage/part1/file --- snapcraft-2.40/tests/integration/snaps/scriptlet-override-stage/part1/file 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/scriptlet-override-stage/part1/file 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1 @@ +I'm a nice file diff -Nru snapcraft-2.40/tests/integration/snaps/scriptlet-override-stage/part2/file2 snapcraft-2.41/tests/integration/snaps/scriptlet-override-stage/part2/file2 --- snapcraft-2.40/tests/integration/snaps/scriptlet-override-stage/part2/file2 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/scriptlet-override-stage/part2/file2 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1 @@ +I'm a nicer file diff -Nru snapcraft-2.40/tests/integration/snaps/scriptlet-override-stage/snap/snapcraft.yaml snapcraft-2.41/tests/integration/snaps/scriptlet-override-stage/snap/snapcraft.yaml --- snapcraft-2.40/tests/integration/snaps/scriptlet-override-stage/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/scriptlet-override-stage/snap/snapcraft.yaml 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,23 @@ +name: override-stage-scriptlet-test +version: '0.1' +summary: Runs the override-stage scriptlet for a part +description: | + Runs the shell script defined in `override-stage` instead of plugin stage. +grade: devel +confinement: devmode + +parts: + override-stage-scriptlet-test: + plugin: dump + source: part1/ + override-stage: | + touch before-stage + snapcraftctl stage + touch after-stage + + override-stage-do-nothing: + plugin: dump + source: part2/ + override-stage: | + # Completely skip stage + exit 0 diff -Nru snapcraft-2.40/tests/integration/snaps/stage-twice/dir/file snapcraft-2.41/tests/integration/snaps/stage-twice/dir/file --- snapcraft-2.40/tests/integration/snaps/stage-twice/dir/file 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/stage-twice/dir/file 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1 @@ +The best little file you ever did see diff -Nru snapcraft-2.40/tests/integration/snaps/stage-twice/snap/snapcraft.yaml snapcraft-2.41/tests/integration/snaps/stage-twice/snap/snapcraft.yaml --- snapcraft-2.40/tests/integration/snaps/stage-twice/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/snaps/stage-twice/snap/snapcraft.yaml 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,16 @@ +name: stage-twice +version: '0.1' +summary: A snap which should be able to be staged twice +description: | + Snaps that organize should be able to be staged, clean the stage, and staged + again. + +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: + plugin: dump + source-subdir: dir/ + organize: + '*': dir/dir/ diff -Nru snapcraft-2.40/tests/integration/sources/test_7z_source.py snapcraft-2.41/tests/integration/sources/test_7z_source.py --- snapcraft-2.40/tests/integration/sources/test_7z_source.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/sources/test_7z_source.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,36 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017 Tim Süberkrüb +# Copyright (C) 2017-2018 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 FileExists + +from tests import integration + + +class SevenZipTestCase(integration.TestCase): + + _7z_test_files = {'test1.txt', 'test2.txt', 'test3.txt'} + + def test_stage_7z(self): + self.run_snapcraft('stage', '7z-hello') + + for filename in self._7z_test_files: + self.assertThat( + os.path.join(self.stage_dir, filename), + FileExists() + ) diff -Nru snapcraft-2.40/tests/integration/sources/test_bzr_source.py snapcraft-2.41/tests/integration/sources/test_bzr_source.py --- snapcraft-2.40/tests/integration/sources/test_bzr_source.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/sources/test_bzr_source.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,76 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2018 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 + +from tests import integration + + +class BzrSourceTestCase(integration.BzrSourceBaseTestCase): + + def test_pull_bzr_head(self): + self.copy_project_to_cwd('bzr-head') + + self.init_source_control() + self.commit('"1"', unchanged=True) + self.commit('"2"', unchanged=True) + # test initial branch + self.run_snapcraft('pull') + revno = subprocess.check_output( + ['bzr', 'revno', '-r', '-1', 'parts/bzr/src'], + universal_newlines=True).strip() + self.assertThat(revno, Equals('2')) + # test pull doesn't fail + self.run_snapcraft('pull') + revno = subprocess.check_output( + ['bzr', 'revno', '-r', '-1', 'parts/bzr/src'], + universal_newlines=True).strip() + self.assertThat(revno, Equals('2')) + + def test_pull_bzr_tag(self): + self.copy_project_to_cwd('bzr-tag') + + self.init_source_control() + self.commit('"1"', unchanged=True) + self.commit('"2"', unchanged=True) + subprocess.check_call( + ['bzr', 'tag', '-r', '1', 'initial'], + stderr=subprocess.DEVNULL) + # test initial branch + self.run_snapcraft('pull') + revno = self.get_revno('parts/bzr/src') + self.assertThat(revno, Equals('1')) + # test pull doesn't fail + self.run_snapcraft('pull') + revno = self.get_revno('parts/bzr/src') + self.assertThat(revno, Equals('1')) + + def test_pull_bzr_commit(self): + self.copy_project_to_cwd('bzr-commit') + + self.init_source_control() + self.commit('"1"', unchanged=True) + self.commit('"2"', unchanged=True) + # test initial branch + self.run_snapcraft('pull') + revno = self.get_revno('parts/bzr/src') + self.assertThat(revno, Equals('1')) + # test pull doesn't fail + self.run_snapcraft('pull') + revno = self.get_revno('parts/bzr/src') + self.assertThat(revno, Equals('1')) diff -Nru snapcraft-2.40/tests/integration/sources/test_deb_source.py snapcraft-2.41/tests/integration/sources/test_deb_source.py --- snapcraft-2.40/tests/integration/sources/test_deb_source.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/sources/test_deb_source.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,50 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2018 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 ( + Equals, + FileExists +) + +from tests import integration + + +class DebSourceTestCase(integration.TestCase): + + def test_stage_deb(self): + self.copy_project_to_cwd('deb-hello') + self.run_snapcraft(['stage', 'deb']) + + self.assertThat( + os.path.join(self.stage_dir, 'bin', 'hello'), + FileExists()) + self.assertThat( + os.path.join(self.stage_dir, 'usr', 'bin', 'world'), + FileExists()) + + # Regression test for LP: #1634813 + def test_stage_deb_with_symlink(self): + self.copy_project_to_cwd('deb-with-symlink') + self.run_snapcraft(['stage', 'deb-with-symlink']) + + target = os.path.join(self.stage_dir, 'target') + symlink = os.path.join(self.stage_dir, 'symlink') + self.assertThat(target, FileExists()) + self.assertThat(symlink, FileExists()) + self.assertTrue(os.path.islink(symlink)) + self.assertThat(os.readlink(symlink), Equals('target')) diff -Nru snapcraft-2.40/tests/integration/sources/test_git_source.py snapcraft-2.41/tests/integration/sources/test_git_source.py --- snapcraft-2.40/tests/integration/sources/test_git_source.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/sources/test_git_source.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,167 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2018 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 shutil +from textwrap import dedent + +from testtools.matchers import Contains, Equals, FileExists + +from tests import integration + + +class GitSourceTestCase(integration.GitSourceBaseTestCase): + + def _get_git_revno(self, path, revrange='-1'): + return subprocess.check_output( + 'git -C {} log {} --oneline | cut -d\' \' -f2'.format( + path, revrange), + shell=True, universal_newlines=True).strip() + + def test_pull_git_head(self): + self.copy_project_to_cwd('git-head') + + self.init_source_control() + self.commit('"1"', allow_empty=True) + self.commit('"2"', allow_empty=True) + + self.run_snapcraft('pull') + revno = self._get_git_revno('parts/git/src') + self.assertThat(revno, Equals('"2"')) + + self.run_snapcraft('pull') + revno = self._get_git_revno('parts/git/src') + self.assertThat(revno, Equals('"2"')) + + def test_pull_git_tag(self): + self.copy_project_to_cwd('git-tag') + + self.init_source_control() + self.commit('"1"', allow_empty=True) + self.commit('"2"', allow_empty=True) + subprocess.check_call( + ['git', 'tag', 'initial', 'HEAD@{1}'], + stdout=subprocess.DEVNULL) + + self.run_snapcraft('pull') + revno = self._get_git_revno('parts/git/src') + self.assertThat(revno, Equals('"1"')) + + self.run_snapcraft('pull') + revno = self._get_git_revno('parts/git/src') + self.assertThat(revno, Equals('"1"')) + + def test_pull_git_commit(self): + self.copy_project_to_cwd('git-commit') + + self.init_source_control() + self.commit('"1"', allow_empty=True) + self.commit('"2"', allow_empty=True) + + # The test uses "HEAD^" so we can only test it once + self.run_snapcraft('pull') + revno = self._get_git_revno('parts/git/src') + self.assertThat(revno, Equals('"1"')) + + def test_pull_git_branch(self): + self.copy_project_to_cwd('git-branch') + + self.init_source_control() + self.commit('"1"', allow_empty=True) + self.commit('"2"', allow_empty=True) + subprocess.check_call( + ['git', 'branch', 'second', 'HEAD@{1}'], + stdout=subprocess.DEVNULL) + subprocess.check_call( + ['git', 'checkout', 'second'], + stderr=subprocess.DEVNULL) + self.commit('"3"', allow_empty=True) + subprocess.check_call( + ['git', 'checkout', 'master'], + stderr=subprocess.DEVNULL) + + self.run_snapcraft('pull') + revno = self._get_git_revno('parts/git/src', revrange='-2') + self.assertThat(revno, Equals('"3"\n"1"')) + + self.run_snapcraft('pull') + revno = self._get_git_revno('parts/git/src', revrange='-2') + self.assertThat(revno, Equals('"3"\n"1"')) + + def test_pull_git_with_depth(self): + """Regression test for LP: #1627772.""" + self.copy_project_to_cwd('git-depth') + + self.init_source_control() + self.commit('"1"', allow_empty=True) + self.commit('"2"', allow_empty=True) + + self.run_snapcraft('pull') + + +class GitGenerateVersionTestCase(integration.GitSourceBaseTestCase): + + def setUp(self): + super().setUp() + self.init_source_control() + os.mkdir('snap') + + with open(os.path.join('snap', 'snapcraft.yaml'), 'w') as f: + print(dedent("""\ + name: git-test + version: git + summary: test git generated version + description: test git generated version with git hint + architectures: [amd64] + parts: + nil: + plugin: nil + """), file=f) + + self.add_file(os.path.join('snap', 'snapcraft.yaml')) + self.commit('snapcraft.yaml added') + + def test_tag(self): + self.tag('2.0') + self.run_snapcraft('snap') + self.assertThat('git-test_2.0_amd64.snap', FileExists()) + + def test_tag_with_commits_ahead(self): + self.tag('2.0') + open('stub_file', 'w').close() + self.add_file('stub_file') + self.commit('new stub file') + self.run_snapcraft('snap') + revno = self.get_revno()[:7] + expected_file = 'git-test_2.0+git1.{}_amd64.snap'.format(revno) + self.assertThat(expected_file, FileExists()) + + def test_no_tag(self): + self.run_snapcraft('snap') + revno = self.get_revno()[:7] + expected_file = 'git-test_0+git.{}_amd64.snap'.format(revno) + self.assertThat(expected_file, FileExists()) + + def test_no_git(self): + shutil.rmtree('.git') + + exception = self.assertRaises( + subprocess.CalledProcessError, self.run_snapcraft, ['snap']) + self.assertThat( + exception.output, + Contains('fatal: Not a git repository (or any of the parent ' + 'directories): .git')) diff -Nru snapcraft-2.40/tests/integration/sources/test_hg_source.py snapcraft-2.41/tests/integration/sources/test_hg_source.py --- snapcraft-2.40/tests/integration/sources/test_hg_source.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/sources/test_hg_source.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,115 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2018 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 testtools.matchers import Equals, FileExists + +from tests import integration + + +class HgSourceTestCase(integration.HgSourceBaseTestCase): + + def test_pull_hg_head(self): + self.copy_project_to_cwd('hg-head') + + self.init_source_control() + open('1', 'w').close() + self.commit('1', '1') + open('2', 'w').close() + self.commit('2', '2') + + self.run_snapcraft('pull') + revno = self.get_revno( + os.path.join(self.parts_dir, 'mercurial', 'src')) + self.assertThat(revno, Equals('"2"')) + + self.run_snapcraft('pull') + revno = self.get_revno( + os.path.join(self.parts_dir, 'mercurial', 'src')) + self.assertThat(revno, Equals('"2"')) + + def test_pull_hg_tag(self): + self.copy_project_to_cwd('hg-tag') + + self.init_source_control() + open('1', 'w').close() + self.commit('1', '1') + subprocess.check_call( + ['hg', 'tag', 'initial', '--user', '"Example Dev"']) + open('2', 'w').close() + self.commit('2', '2') + + self.run_snapcraft('pull') + revno = subprocess.check_output( + 'ls -1 {} | wc -l '.format( + os.path.join(self.parts_dir, 'mercurial', 'src')), + shell=True, universal_newlines=True).strip() + self.assertThat(revno, Equals('1')) + + self.run_snapcraft('pull') + revno = subprocess.check_output( + 'ls -1 {} | wc -l '.format( + os.path.join(self.parts_dir, 'mercurial', 'src')), + shell=True, universal_newlines=True).strip() + self.assertThat(revno, Equals('1')) + + def test_pull_hg_commit(self): + self.copy_project_to_cwd('hg-commit') + + self.init_source_control() + open('1', 'w').close() + self.commit('1', '1') + open('2', 'w').close() + self.commit('2', '2') + + self.run_snapcraft('pull') + revno = subprocess.check_output( + 'ls -1 {} | wc -l '.format( + os.path.join(self.parts_dir, 'mercurial', 'src')), + shell=True, universal_newlines=True).strip() + self.assertThat(revno, Equals('1')) + + self.run_snapcraft('pull') + revno = subprocess.check_output( + 'ls -1 {} | wc -l '.format( + os.path.join(self.parts_dir, 'mercurial', 'src')), + shell=True, universal_newlines=True).strip() + self.assertThat(revno, Equals('1')) + + def test_pull_hg_branch(self): + self.copy_project_to_cwd('hg-branch') + + self.init_source_control() + subprocess.check_call( + ['hg', 'branch', 'second'], stdout=subprocess.DEVNULL) + open('second', 'w').close() + self.commit('second', 'second') + subprocess.check_call( + ['hg', 'branch', 'default'], stdout=subprocess.DEVNULL) + open('default', 'w').close() + self.commit('default', 'default') + + self.run_snapcraft('pull') + self.assertThat( + os.path.join(self.parts_dir, 'mercurial', 'src', 'second'), + FileExists()) + + self.run_snapcraft('pull') + self.assertThat( + os.path.join(self.parts_dir, 'mercurial', 'src', 'second'), + FileExists()) diff -Nru snapcraft-2.40/tests/integration/sources/test_local_source.py snapcraft-2.41/tests/integration/sources/test_local_source.py --- snapcraft-2.40/tests/integration/sources/test_local_source.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/sources/test_local_source.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,83 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2018 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 +from testtools.matchers import FileExists + +from tests import integration + + +class LocalSourceTestCase(integration.TestCase): + + def test_build_local_source(self): + self.run_snapcraft('build', 'local-source') + + self.assertThat( + os.path.join( + self.parts_dir, 'make-project', 'build', 'stamp-all'), + FileExists()) + + def test_stage_local_source(self): + self.run_snapcraft('stage', 'local-source') + + self.assertThat( + os.path.join( + self.parts_dir, 'make-project', 'build', + 'stamp-install'), + FileExists()) + + +class LocalSourceTypeTestCase(integration.TestCase): + + def test_build_local_source(self): + self.run_snapcraft('build', 'local-source-type') + + self.assertThat( + os.path.join( + self.parts_dir, 'make-project', 'build', 'stamp-all'), + FileExists()) + + def test_stage_local_source(self): + self.run_snapcraft('stage', 'local-source') + + self.assertThat( + os.path.join( + self.parts_dir, 'make-project', 'build', + 'stamp-install'), + FileExists()) + + +class LocalSourceSubfoldersTestCase( + testscenarios.WithScenarios, integration.TestCase): + + scenarios = [ + ('Top folder', + {'subfolder': '.'}), + ('Sub folder Level 1', + {'subfolder': 'packaging'}), + ('Sub folder Level 2', + {'subfolder': os.path.join('packaging', 'snap-package')}), + ('Sub folder Level 3', + {'subfolder': os.path.join( + 'packaging', 'snap-package', 'yes-really-deep')}), + ] + + def test_pull_local_source(self): + self.copy_project_to_cwd('local-source-subfolders') + os.chdir(self.subfolder) + self.run_snapcraft('pull') diff -Nru snapcraft-2.40/tests/integration/sources/test_rpm_source.py snapcraft-2.41/tests/integration/sources/test_rpm_source.py --- snapcraft-2.40/tests/integration/sources/test_rpm_source.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/sources/test_rpm_source.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,35 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2017 Neal Gompa +# Copyright (C) 2017-2018 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 FileExists + +from tests import integration + + +class RpmSourceTestCase(integration.TestCase): + + def test_stage_rpm(self): + self.run_snapcraft('stage', 'rpm-hello') + + self.assertThat( + os.path.join(self.stage_dir, 'bin', 'hello'), + FileExists()) + self.assertThat( + os.path.join(self.stage_dir, 'usr', 'bin', 'world'), + FileExists()) diff -Nru snapcraft-2.40/tests/integration/sources/test_source_grammar.py snapcraft-2.41/tests/integration/sources/test_source_grammar.py --- snapcraft-2.40/tests/integration/sources/test_source_grammar.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/sources/test_source_grammar.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,88 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2017-2018 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 Contains + +from tests import integration, fixture_setup + + +class PartsGrammarTestCase(integration.TestCase): + + def setUp(self): + super().setUp() + self.test_source = ( + 'https://github.com/snapcrafters/fork-and-rename-me.git') + + def test_plain_source_string(self): + snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) + snapcraft_yaml.update_part('my-part', { + 'plugin': 'nil', + 'source': self.test_source, + }) + self.useFixture(snapcraft_yaml) + self.run_snapcraft(['pull']) + + def test_source_on_current_arch(self): + snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) + snapcraft_yaml.update_part('my-part', { + 'plugin': 'nil', + 'source': [ + {'on {}'.format(self.deb_arch): self.test_source}, + {'else': 'invalid'}, + ] + }) + self.useFixture(snapcraft_yaml) + self.run_snapcraft(['pull']) + + def test_source_try(self): + snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) + snapcraft_yaml.update_part('my-part', { + 'plugin': 'nil', + 'source': [ + {'try': self.test_source}, + {'else': 'invalid'}, + ] + }) + self.useFixture(snapcraft_yaml) + self.run_snapcraft(['pull']) + + def test_source_on_other_arch(self): + snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) + snapcraft_yaml.update_part('my-part', { + 'plugin': 'nil', + 'source': [ + {'on other-arch': 'invalid'}, + ] + }) + self.useFixture(snapcraft_yaml) + self.run_snapcraft(['pull']) + + def test_source_on_other_arch_else_fail(self): + snapcraft_yaml = fixture_setup.SnapcraftYaml(self.path) + snapcraft_yaml.update_part('my-part', { + 'plugin': 'nil', + 'source': [ + {'on other-arch': 'invalid'}, + 'else fail', + ] + }) + self.useFixture(snapcraft_yaml) + self.assertThat(self.assertRaises( + subprocess.CalledProcessError, self.run_snapcraft, + ['pull']).output, Contains( + "Unable to satisfy 'on other-arch', failure forced")) diff -Nru snapcraft-2.40/tests/integration/sources/test_svn_pull.py snapcraft-2.41/tests/integration/sources/test_svn_pull.py --- snapcraft-2.40/tests/integration/sources/test_svn_pull.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/sources/test_svn_pull.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,80 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2018 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 testtools.matchers import Equals, FileExists + +from tests import integration + + +class SubversionSourceTestCase(integration.SubversionSourceBaseTestCase): + + def test_pull_svn_checkout(self): + self.copy_project_to_cwd('svn-pull') + + self.init_source_control() + self.checkout( + 'file:///{}'.format(os.path.join(self.path, 'repo')), + 'local') + open(os.path.join('local', 'file'), 'w').close() + self.add('file', cwd='local/') + self.commit('test', cwd='local/') + self.update(cwd='local/') + subprocess.check_call( + ['rm', '-rf', 'local/'], stdout=subprocess.DEVNULL) + + self.run_snapcraft('pull') + part_src_path = os.path.join(self.parts_dir, 'svn', 'src') + revno = subprocess.check_output(['svnversion', part_src_path]).strip() + self.assertThat(revno, Equals(b'1')) + self.assertThat(os.path.join(part_src_path, 'file'), FileExists()) + + def test_pull_svn_update(self): + self.copy_project_to_cwd('svn-pull-update') + + self.init_source_control() + + self.checkout( + 'file:///{}'.format(os.path.join(self.path, 'repo')), + 'local') + open(os.path.join('local', 'file'), 'w').close() + self.add('file', cwd='local/') + self.commit('test', cwd='local/') + self.update(cwd='local/') + subprocess.check_call( + ['rm', '-rf', 'local/'], stdout=subprocess.DEVNULL) + + part_src_path = os.path.join(self.parts_dir, 'svn', 'src') + self.checkout( + 'file:///{}'.format(os.path.join(self.path, 'repo')), + part_src_path) + self.checkout( + 'file:///{}'.format(os.path.join(self.path, 'repo')), + 'local') + open(os.path.join('local', 'filetwo'), 'w').close() + self.add('filetwo', cwd='local/') + self.commit('testtwo', cwd='local/') + self.update(cwd='local/') + subprocess.check_call( + ['rm', '-rf', 'local/'], stdout=subprocess.DEVNULL) + + self.run_snapcraft('pull') + revno = subprocess.check_output(['svnversion', part_src_path]).strip() + self.assertThat(revno, Equals(b'2')) + self.assertThat(os.path.join(part_src_path, 'file'), FileExists()) + self.assertThat(os.path.join(part_src_path, 'filetwo'), FileExists()) diff -Nru snapcraft-2.40/tests/integration/sources/test_zip_source.py snapcraft-2.41/tests/integration/sources/test_zip_source.py --- snapcraft-2.40/tests/integration/sources/test_zip_source.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/integration/sources/test_zip_source.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,60 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2015-2018 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 ( + Equals, + DirExists, + FileExists +) + +from tests import integration + + +class TarPluginTestCase(integration.TestCase): + + def test_stage_zip_source(self): + self.copy_project_to_cwd('zip') + self.run_snapcraft('stage') + + expected_files = [ + 'exec', + 'top-simple', + os.path.join('dir-simple', 'sub') + ] + for expected_file in expected_files: + 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)) + self.assertThat(os.access( + os.path.join(self.stage_dir, 'non-unix'), os.X_OK), + Equals(True)) + expected_dirs = [ + 'dir-simple', + 'non-unix', + ] + for expected_dir in expected_dirs: + self.assertThat( + os.path.join(self.stage_dir, expected_dir), + DirExists()) + + # Regression test for + # https://bugs.launchpad.net/snapcraft/+bug/1500728 + self.run_snapcraft('pull') diff -Nru snapcraft-2.40/tests/integration/store/test_store_release.py snapcraft-2.41/tests/integration/store/test_store_release.py --- snapcraft-2.40/tests/integration/store/test_store_release.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/integration/store/test_store_release.py 2018-04-14 12:13:35.000000000 +0000 @@ -19,6 +19,7 @@ import subprocess from testtools.matchers import ( + Contains, FileExists, MatchesRegex, ) @@ -63,3 +64,72 @@ output = self.run_snapcraft(['release', name, '1', 'edge']) expected = r'.*The \'edge\' channel is now open.*' self.assertThat(output, MatchesRegex(expected, flags=re.DOTALL)) + + def test_release_to_channel_without_permission(self): + if not self.is_store_fake(): + self.skipTest("The real store won't return the proper response") + + self.addCleanup(self.logout) + self.login() + + # Change to a random name and version. + name = self.get_unique_name() + version = self.get_unique_version() + self.copy_project_to_cwd('basic') + self.update_name_and_version(name, version) + + self.run_snapcraft('snap') + + # Register the snap + self.register(name) + # Upload the snap + snap_file_path = '{}_{}_{}.snap'.format(name, version, 'all') + self.assertThat( + os.path.join(snap_file_path), FileExists()) + + output = self.run_snapcraft(['push', snap_file_path]) + expected = r'.*Ready to release!.*'.format(name) + self.assertThat(output, MatchesRegex(expected, flags=re.DOTALL)) + + # Attempt to release it + error = self.assertRaises( + subprocess.CalledProcessError, + self.run_snapcraft, ['release', name, '1', 'no-permission']) + self.assertThat(error.output, Contains( + "Received 403: Lacking permission to release to channel(s) " + "'no-permission'")) + + def test_release_internal_error(self): + if not self.is_store_fake(): + self.skipTest("The real store won't return the proper response") + + self.addCleanup(self.logout) + self.login() + + # Change to a random name and version. + name = self.get_unique_name() + version = self.get_unique_version() + self.copy_project_to_cwd('basic') + self.update_name_and_version(name, version) + + self.run_snapcraft('snap') + + # Register the snap + self.register(name) + # Upload the snap + snap_file_path = '{}_{}_{}.snap'.format(name, version, 'all') + self.assertThat( + os.path.join(snap_file_path), FileExists()) + + output = self.run_snapcraft(['push', snap_file_path]) + expected = r'.*Ready to release!.*'.format(name) + self.assertThat(output, MatchesRegex(expected, flags=re.DOTALL)) + + # Attempt to release it + error = self.assertRaises( + subprocess.CalledProcessError, + self.run_snapcraft, ['release', name, '1', 'bad-channel']) + self.assertThat(error.output, Contains( + 'store encountered an internal error. The status of store and ' + 'associated services can be checked at:\n' + 'https://status.snapcraft.io/')) diff -Nru snapcraft-2.40/tests/unit/cli/test_errors.py snapcraft-2.41/tests/unit/cli/test_errors.py --- snapcraft-2.40/tests/unit/cli/test_errors.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/cli/test_errors.py 2018-04-14 12:13:35.000000000 +0000 @@ -17,6 +17,8 @@ import sys from unittest import mock +import fixtures + import snapcraft.internal.errors from snapcraft.cli._errors import exception_handler from tests import unit @@ -56,27 +58,86 @@ except Exception: exception_handler(*sys.exc_info(), debug=debug) - def assert_exception_traceback_exit_1(self): + def assert_exception_traceback_exit_1_with_debug(self): self.error_mock.assert_not_called self.exit_mock.assert_called_once_with(1) self.print_exception_mock.assert_called_once_with( RuntimeError, mock.ANY, mock.ANY) - def test_handler_traceback_non_snapcraft_exceptions_no_debug(self): + def assert_no_exception_traceback_exit_1_without_debug(self): + self.error_mock.assert_not_called + self.exit_mock.assert_called_once_with(1) + self.print_exception_mock.assert_not_called() + + @mock.patch('click.confirm', return_value=False) + def test_handler_traceback_non_snapcraft_exceptions_no_debug( + self, click_confirm_mock): + """ + Verify that the traceback is printed given that raven is available. + """ try: self.call_handler(RuntimeError('not a SnapcraftError'), False) except Exception: self.fail('Exception unexpectedly raised') - self.assert_exception_traceback_exit_1() + self.assert_exception_traceback_exit_1_with_debug() + + @mock.patch('click.confirm', return_value=False) + def test_handler_traceback_non_snapcraft_exceptions_debug( + self, click_confirm_mock): + try: + self.call_handler(RuntimeError('not a SnapcraftError'), True) + except Exception: + self.fail('Exception unexpectedly raised') + + self.assert_exception_traceback_exit_1_with_debug() + + @mock.patch.object(snapcraft.cli._errors, 'RavenClient') + def test_handler_no_raven_traceback_non_snapcraft_exceptions_debug( + self, raven_client_mock): + snapcraft.cli._errors.RavenClient = None + try: + self.call_handler(RuntimeError('not a SnapcraftError'), True) + except Exception: + self.fail('Exception unexpectedly raised') + + self.assert_exception_traceback_exit_1_with_debug() + + def test_handler_raven_but_no_sentry_feature_flag(self): + try: + self.call_handler(RuntimeError('not a SnapcraftError'), True) + except Exception: + self.fail('Exception unexpectedly raised') + + self.assert_exception_traceback_exit_1_with_debug() + + @mock.patch('click.confirm', return_value=True) + def test_handler_traceback_send_traceback_to_sentry( + self, click_confirm_mock): + try: + import raven # noqa: F401 + except ImportError: + self.skipTest('raven needs to be installed for this test.') + + # Only patch after checking for the availability of raven + patcher = mock.patch('snapcraft.cli._errors.RequestsHTTPTransport') + raven_request_mock = patcher.start() + self.addCleanup(patcher.stop) + patcher = mock.patch('snapcraft.cli._errors.RavenClient') + raven_client_mock = patcher.start() + self.addCleanup(patcher.stop) - def test_handler_traceback_non_snapcraft_exceptions_debug(self): + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFT_ENABLE_SENTRY', 'yes')) try: self.call_handler(RuntimeError('not a SnapcraftError'), True) except Exception: self.fail('Exception unexpectedly raised') - self.assert_exception_traceback_exit_1() + self.assert_exception_traceback_exit_1_with_debug() + raven_client_mock.assert_called_once_with( + mock.ANY, transport=raven_request_mock, processors=mock.ANY, + auto_log_stacks=False) def test_handler_catches_snapcraft_exceptions_no_debug(self): try: diff -Nru snapcraft-2.40/tests/unit/commands/snapcraftctl/__init__.py snapcraft-2.41/tests/unit/commands/snapcraftctl/__init__.py --- snapcraft-2.40/tests/unit/commands/snapcraftctl/__init__.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/unit/commands/snapcraftctl/__init__.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,52 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 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 fixtures +import os + +from click.testing import CliRunner + +from snapcraft.cli.snapcraftctl._runner import run +from tests import unit + + +class CommandBaseNoFifoTestCase(unit.TestCase): + + def setUp(self): + super().setUp() + self.runner = CliRunner() + + def run_command(self, args, **kwargs): + return self.runner.invoke(run, args, catch_exceptions=False, **kwargs) + + +class CommandBaseTestCase(CommandBaseNoFifoTestCase): + + def setUp(self): + super().setUp() + + tempdir = self.useFixture(fixtures.TempDir()).path + self.call_fifo = os.path.join(tempdir, 'call_fifo') + self.feedback_fifo = os.path.join(tempdir, 'feedback_fifo') + + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFTCTL_CALL_FIFO', self.call_fifo)) + self.useFixture(fixtures.EnvironmentVariable( + 'SNAPCRAFTCTL_FEEDBACK_FIFO', self.feedback_fifo)) + + # Create the FIFOs + open(self.call_fifo, 'w').close() + open(self.feedback_fifo, 'w').close() diff -Nru snapcraft-2.40/tests/unit/commands/snapcraftctl/test_build.py snapcraft-2.41/tests/unit/commands/snapcraftctl/test_build.py --- snapcraft-2.40/tests/unit/commands/snapcraftctl/test_build.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/unit/commands/snapcraftctl/test_build.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,65 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 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 + +from testtools.matchers import ( + Contains, + Equals, + FileExists, +) + +from snapcraft.internal import errors + +from . import CommandBaseTestCase, CommandBaseNoFifoTestCase + + +class BuildCommandTestCase(CommandBaseTestCase): + + def test_build(self): + self.run_command(['build']) + self.assertThat(self.call_fifo, FileExists()) + + with open(self.call_fifo, 'r') as f: + data = json.loads(f.read()) + + self.assertThat(data, Contains('function')) + self.assertThat(data, Contains('args')) + + self.assertThat(data['function'], Equals('build')) + self.assertThat(data['args'], Equals({})) + + def test_build_error(self): + # If there is a string in the feedback, it should be considered an + # error + with open(self.feedback_fifo, 'w') as f: + f.write('this is an error\n') + + raised = self.assertRaises( + errors.SnapcraftctlError, self.run_command, ['build']) + + self.assertThat(str(raised), Equals('this is an error')) + + +class BuildCommandWithoutFifoTestCase(CommandBaseNoFifoTestCase): + + def test_build_without_fifo(self): + raised = self.assertRaises( + errors.SnapcraftEnvironmentError, self.run_command, ['build']) + + self.assertThat(str(raised), Contains( + 'environment variable must be defined. Note that this utility is ' + 'only designed for use within a snapcraft.yaml')) diff -Nru snapcraft-2.40/tests/unit/commands/snapcraftctl/test_set_grade.py snapcraft-2.41/tests/unit/commands/snapcraftctl/test_set_grade.py --- snapcraft-2.40/tests/unit/commands/snapcraftctl/test_set_grade.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/unit/commands/snapcraftctl/test_set_grade.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,67 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 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 + +from testtools.matchers import ( + Contains, + Equals, + FileExists, +) + +from snapcraft.internal import errors + +from . import CommandBaseTestCase, CommandBaseNoFifoTestCase + + +class SetGradeCommandTestCase(CommandBaseTestCase): + + def test_set_grade(self): + self.run_command(['set-grade', 'test-grade']) + self.assertThat(self.call_fifo, FileExists()) + + with open(self.call_fifo, 'r') as f: + data = json.loads(f.read()) + + self.assertThat(data, Contains('function')) + self.assertThat(data, Contains('args')) + + self.assertThat(data['function'], Equals('set-grade')) + self.assertThat(data['args'], Equals({'grade': 'test-grade'})) + + def test_set_grade_error(self): + # If there is a string in the feedback, it should be considered an + # error + with open(self.feedback_fifo, 'w') as f: + f.write('this is an error\n') + + raised = self.assertRaises( + errors.SnapcraftctlError, self.run_command, + ['set-grade', 'test-grade']) + + self.assertThat(str(raised), Equals('this is an error')) + + +class SetGradeCommandWithoutFifoTestCase(CommandBaseNoFifoTestCase): + + def test_set_grade_without_fifo(self): + raised = self.assertRaises( + errors.SnapcraftEnvironmentError, self.run_command, + ['set-grade', 'test-grade']) + + self.assertThat(str(raised), Contains( + 'environment variable must be defined. Note that this utility is ' + 'only designed for use within a snapcraft.yaml')) diff -Nru snapcraft-2.40/tests/unit/commands/snapcraftctl/test_set_version.py snapcraft-2.41/tests/unit/commands/snapcraftctl/test_set_version.py --- snapcraft-2.40/tests/unit/commands/snapcraftctl/test_set_version.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/unit/commands/snapcraftctl/test_set_version.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,67 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 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 + +from testtools.matchers import ( + Contains, + Equals, + FileExists, +) + +from snapcraft.internal import errors + +from . import CommandBaseTestCase, CommandBaseNoFifoTestCase + + +class SetVersionCommandTestCase(CommandBaseTestCase): + + def test_set_version(self): + self.run_command(['set-version', 'test-version']) + self.assertThat(self.call_fifo, FileExists()) + + with open(self.call_fifo, 'r') as f: + data = json.loads(f.read()) + + self.assertThat(data, Contains('function')) + self.assertThat(data, Contains('args')) + + self.assertThat(data['function'], Equals('set-version')) + self.assertThat(data['args'], Equals({'version': 'test-version'})) + + def test_set_version_error(self): + # If there is a string in the feedback, it should be considered an + # error + with open(self.feedback_fifo, 'w') as f: + f.write('this is an error\n') + + raised = self.assertRaises( + errors.SnapcraftctlError, self.run_command, + ['set-version', 'test-version']) + + self.assertThat(str(raised), Equals('this is an error')) + + +class SetVersionCommandWithoutFifoTestCase(CommandBaseNoFifoTestCase): + + def test_set_version_without_fifo(self): + raised = self.assertRaises( + errors.SnapcraftEnvironmentError, self.run_command, + ['set-version', 'test-version']) + + self.assertThat(str(raised), Contains( + 'environment variable must be defined. Note that this utility is ' + 'only designed for use within a snapcraft.yaml')) diff -Nru snapcraft-2.40/tests/unit/commands/test_export_login.py snapcraft-2.41/tests/unit/commands/test_export_login.py --- snapcraft-2.40/tests/unit/commands/test_export_login.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/commands/test_export_login.py 2018-04-14 12:13:35.000000000 +0000 @@ -83,6 +83,49 @@ 'get_account_information') @mock.patch.object(storeapi.StoreClient, 'login') @mock.patch.object(storeapi.StoreClient, 'acl') + def test_successful_export_stdout( + self, mock_acl, mock_login, mock_get_account_information): + self.mock_input.return_value = 'user@example.com' + mock_acl.return_value = { + 'snap_ids': None, + 'channels': None, + 'permissions': None, + 'expires': '2018-02-01T00:00:00', + } + + result = self.run_command(['export-login', '-']) + + self.assertThat(result.exit_code, Equals(0)) + self.assertThat(result.output, Contains( + storeapi.constants.TWO_FACTOR_WARNING)) + self.assertThat( + result.output, Contains('Exported login starts on next line')) + self.assertThat( + result.output, Contains( + 'Login successfully exported and printed above')) + self.assertThat( + result.output, MatchesRegex( + r'.*snaps:.*?No restriction', re.DOTALL)) + self.assertThat( + result.output, MatchesRegex( + r'.*channels:.*?No restriction', re.DOTALL)) + self.assertThat( + result.output, MatchesRegex( + r'.*permissions:.*?No restriction', re.DOTALL)) + self.assertThat( + result.output, MatchesRegex( + r'.*expires:.*?2018-02-01T00:00:00', re.DOTALL)) + + self.mock_input.assert_called_once_with('Email: ') + mock_login.assert_called_once_with( + 'user@example.com', mock.ANY, acls=None, packages=None, + channels=None, expires=None, save=False, config_fd=None) + mock_acl.assert_called_once_with() + + @mock.patch.object(storeapi._sca_client.SCAClient, + 'get_account_information') + @mock.patch.object(storeapi.StoreClient, 'login') + @mock.patch.object(storeapi.StoreClient, 'acl') def test_successful_export_expires( self, mock_acl, mock_login, mock_get_account_information): self.mock_input.return_value = 'user@example.com' diff -Nru snapcraft-2.40/tests/unit/commands/test_push.py snapcraft-2.41/tests/unit/commands/test_push.py --- snapcraft-2.40/tests/unit/commands/test_push.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/commands/test_push.py 2018-04-14 12:13:35.000000000 +0000 @@ -14,7 +14,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os -import fixtures import subprocess from unittest import mock @@ -31,6 +30,7 @@ StoreUploadError ) import tests +from tests import fixture_setup from . import CommandBaseTestCase @@ -84,10 +84,7 @@ mock_upload.assert_called_once_with('basic', self.snap_file) def test_push_a_snap_running_from_snap(self): - self.useFixture(fixtures.EnvironmentVariable( - 'SNAP', '/snap/snapcraft/current')) - self.useFixture(fixtures.EnvironmentVariable( - 'SNAP_NAME', 'snapcraft')) + self.useFixture(fixture_setup.FakeSnapcraftIsASnap()) mock_tracker = mock.Mock(storeapi._status_tracker.StatusTracker) mock_tracker.track.return_value = { diff -Nru snapcraft-2.40/tests/unit/commands/test_snap.py snapcraft-2.41/tests/unit/commands/test_snap.py --- snapcraft-2.40/tests/unit/commands/test_snap.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/commands/test_snap.py 2018-04-14 12:13:35.000000000 +0000 @@ -96,10 +96,7 @@ @mock.patch('snapcraft.internal.lifecycle._packer._run_mksquashfs') def test_mksquashfs_from_snap_used_if_using_snap(self, mock_run_mksquashfs, mock_check_command): - self.useFixture(fixtures.EnvironmentVariable( - 'SNAP', '/snap/snapcraft/current')) - self.useFixture(fixtures.EnvironmentVariable( - 'SNAP_NAME', 'snapcraft')) + self.useFixture(fixture_setup.FakeSnapcraftIsASnap()) self.make_snapcraft_yaml() @@ -546,10 +543,7 @@ 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(['python3', '-c', mock.ANY]), call(['apt-get', 'update']), call(['apt-get', 'install', 'squashfuse', '-y']), call(['snapcraft', 'snap', '--output', @@ -585,10 +579,7 @@ 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(['python3', '-c', mock.ANY]), call(['snapcraft', 'snap', '--output', 'snap-test_1.0_amd64.snap'], cwd=project_folder, user='root'), @@ -650,13 +641,10 @@ 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, user='root'), + call(['python3', '-c', mock.ANY]), + call(['snapcraft', 'snap', '--output', + 'snap-test_1.0_amd64.snap'], + cwd=project_folder, user='root'), ]) # Ensure there's no unexpected calls eg. two network checks self.assertThat(mock_container_run.call_count, Equals(2)) diff -Nru snapcraft-2.40/tests/unit/extractors/test_metadata.py snapcraft-2.41/tests/unit/extractors/test_metadata.py --- snapcraft-2.40/tests/unit/extractors/test_metadata.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/extractors/test_metadata.py 2018-04-14 12:13:35.000000000 +0000 @@ -45,6 +45,13 @@ self.assertThat(metadata.get_summary(), Equals('summary')) self.assertThat(metadata.get_description(), Equals('new description')) + def test_overlap(self): + metadata = ExtractedMetadata( + summary='summary', description='description') + metadata2 = ExtractedMetadata(description='new description') + + self.assertThat(metadata.overlap(metadata2), Equals({'description'})) + def test_eq(self): metadata1 = ExtractedMetadata(summary='summary') metadata2 = ExtractedMetadata(summary='summary') @@ -58,6 +65,13 @@ metadata2 = ExtractedMetadata(description='description') self.assertThat(metadata1, Not(Equals(metadata2))) + def test_len(self): + metadata = ExtractedMetadata(version='version') + self.assertThat(len(metadata), Equals(1)) + + metadata = ExtractedMetadata(summary='summary', version='version') + self.assertThat(len(metadata), Equals(2)) + def test_to_dict_partial(self): metadata = ExtractedMetadata(summary='summary') self.assertThat(metadata.to_dict(), Equals({'summary': 'summary'})) @@ -77,3 +91,34 @@ # Ensure the metadata cannot be edited with its dict self.assertThat(metadata.get_summary(), Equals('summary')) + + +class ExtractedMetadataGettersTestCase(unit.TestCase): + + scenarios = [ + ('common_id', {'property': 'common_id', 'value': 'test-value'}), + ('summary', {'property': 'summary', 'value': 'test-value'}), + ('description', {'property': 'description', 'value': 'test-value'}), + ('version', {'property': 'version', 'value': 'test-value'}), + ('grade', {'property': 'grade', 'value': 'test-value'}), + ('icon', {'property': 'icon', 'value': 'test-value'}), + ('desktop_file_paths', { + 'property': 'desktop_file_paths', 'value': ['test-value']}), + ] + + properties = ('common_id', 'summary', 'description', 'version', 'grade', + 'icon', 'desktop_file_paths') + + def test_getters(self): + metadata = ExtractedMetadata(**{self.property: self.value}) + for prop in self.properties: + gotten = getattr(metadata, 'get_{}'.format(prop))() + if prop == self.property: + self.assertThat( + gotten, Equals(self.value), + 'Expected {!r} getter to return {}'.format( + prop, self.value)) + else: + self.assertThat( + gotten, Equals(None), + 'Expected {!r} getter to return None'.format(prop)) diff -Nru snapcraft-2.40/tests/unit/extractors/test_setuppy.py snapcraft-2.41/tests/unit/extractors/test_setuppy.py --- snapcraft-2.40/tests/unit/extractors/test_setuppy.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/unit/extractors/test_setuppy.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,120 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2018 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 snapcraft.extractors import setuppy, ExtractedMetadata + +from testscenarios import multiply_scenarios +from testtools.matchers import Equals + +from snapcraft.extractors import _errors +from tests import unit + + +class SetupPyTestCase(unit.TestCase): + + metadata = [ + ('description', dict(params=dict( + version=None, + description='test-description'))), + ('version', dict(params=dict( + version='test-version', + description=None))), + ('key and version', dict(params=dict( + description='test-description', + version='test-version'))) + ] + + tools = [ + ('setuptools', dict( + import_statement='import setuptools', + method='setuptools.setup')), + ('from setuptools', dict( + import_statement='from setuptools import setup', + method='setup')), + ('distutils', dict( + import_statement='import distutils.core', + method='distutils.core.setup')), + ('from distutils.core', dict( + import_statement='from distutils.core import setup', + method='setup')), + ] + + scenarios = multiply_scenarios(metadata, tools) + + def setUp(self): + super().setUp() + + params = [' {}="{}",'.format(k, v) + for k, v in self.params.items() if v] + + fmt = dict( + params='\n'.join(params), + import_statement=self.import_statement, + method=self.method, + ) + + with open('setup.py', 'w') as setup_file: + print(dedent("""\ + {import_statement} + + {method}( + name='hello-world', + {params} + author='Canonical LTD', + author_email='snapcraft@lists.snapcraft.io', + ) + """).format(**fmt), file=setup_file) + + def test_info_extraction(self): + expected = ExtractedMetadata(**self.params) + actual = setuppy.extract('setup.py') + self.assertThat(str(actual), Equals(str(expected))) + self.assertThat(actual, Equals(expected)) + + +class SetupPyErrorsTestCase(unit.TestCase): + + def test_unhandled_file_test_case(self): + raised = self.assertRaises( + _errors.UnhandledFileError, setuppy.extract, + 'unhandled-file') + + self.assertThat(raised.path, Equals('unhandled-file')) + self.assertThat(raised.extractor_name, Equals('setup.py')) + + def test_bad_import(self): + with open('setup.py', 'w') as setup_file: + print('import bad_module', file=setup_file) + + self.assertRaises( + _errors.SetupPyImportError, setuppy.extract, + 'setup.py') + + def test_unsupported_setup(self): + with open('setup.py', 'w') as setup_file: + print(dedent("""\ + def setup(**kwargs): + # Raise what setuptools and distutils raise + raise SystemExit() + + setup(name='name', version='version') + """), file=setup_file) + + self.assertRaises( + _errors.SetupPyFileParseError, setuppy.extract, + 'setup.py') diff -Nru snapcraft-2.40/tests/unit/__init__.py snapcraft-2.41/tests/unit/__init__.py --- snapcraft-2.40/tests/unit/__init__.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/__init__.py 2018-04-14 12:13:35.000000000 +0000 @@ -149,6 +149,10 @@ machine=machine) self.useFixture(self.base_environment) + # Make sure SNAPCRAFT_DEBUG is reset between tests + self.useFixture(fixtures.EnvironmentVariable('SNAPCRAFT_DEBUG')) + self.useFixture(fixture_setup.FakeSnapcraftctl()) + def make_snapcraft_yaml(self, content, encoding='utf-8'): with contextlib.suppress(FileExistsError): os.mkdir('snap') diff -Nru snapcraft-2.40/tests/unit/pluginhandler/test_pluginhandler.py snapcraft-2.41/tests/unit/pluginhandler/test_pluginhandler.py --- snapcraft-2.40/tests/unit/pluginhandler/test_pluginhandler.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/pluginhandler/test_pluginhandler.py 2018-04-14 12:13:35.000000000 +0000 @@ -556,6 +556,12 @@ (['bar', 'basefoo', 'foo'], 'bin'), ] )), + ('leading_slash_in_value', dict( + setup_dirs=[], + setup_files=['foo'], + organize_set={'foo': '/bar'}, + expected=[(['bar'], '')], + )), ('overwrite_existing_file', dict( setup_dirs=[], setup_files=['foo', 'bar'], @@ -775,6 +781,49 @@ self.assertTrue(os.path.exists(d), '{} does not exist'.format(d)) +class NextLastStepTestCase(unit.TestCase): + def setUp(self): + super().setUp() + + self.handler = self.load_part('test_part') + + def test_pull(self): + self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) + + self.handler.pull() + + self.assertThat(self.handler.last_step(), Equals('pull')) + self.assertThat(self.handler.next_step(), Equals('build')) + + def test_build(self): + self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) + + self.handler.build() + + self.assertThat(self.handler.last_step(), Equals('build')) + self.assertThat(self.handler.next_step(), Equals('stage')) + + def test_stage(self): + self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) + + self.handler.stage() + + self.assertThat(self.handler.last_step(), Equals('stage')) + self.assertThat(self.handler.next_step(), Equals('prime')) + + def test_prime(self): + self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) + + self.handler.prime() + + self.assertThat(self.handler.last_step(), Equals('prime')) + self.assertThat(self.handler.next_step(), Equals(None)) + + class StateBaseTestCase(unit.TestCase): def setUp(self): @@ -824,21 +873,23 @@ @patch('snapcraft.internal.repo.Repo') def test_pull_state(self, repo_mock): self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) repo_mock.get_installed_build_packages.return_value = [] self.handler.pull() self.assertThat(self.handler.last_step(), Equals('pull')) + self.assertThat(self.handler.next_step(), Equals('build')) state = states.get_state(self.handler.plugin.statedir, 'pull') self.assertTrue(state, 'Expected pull to save state YAML') self.assertTrue(type(state) is states.PullState) self.assertTrue(type(state.properties) is OrderedDict) - self.assertThat(len(state.properties), Equals(10)) + self.assertThat(len(state.properties), Equals(11)) for expected in ['source', 'source-branch', 'source-commit', 'source-depth', 'source-subdir', 'source-tag', 'source-type', 'plugin', 'stage-packages', - 'parse-info']: + 'parse-info', 'override-pull']: self.assertTrue(expected in state.properties) self.assertTrue(type(state.project_options) is OrderedDict) self.assertTrue('deb_arch' in state.project_options) @@ -866,21 +917,23 @@ 'fake', _fake_extractor)) self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) repo_mock.get_installed_build_packages.return_value = [] self.handler.pull() self.assertThat(self.handler.last_step(), Equals('pull')) + self.assertThat(self.handler.next_step(), Equals('build')) state = self.handler.get_pull_state() self.assertTrue(state, 'Expected pull to save state YAML') self.assertTrue(type(state) is states.PullState) self.assertTrue(type(state.properties) is OrderedDict) - self.assertThat(len(state.properties), Equals(10)) + self.assertThat(len(state.properties), Equals(11)) for expected in ['source', 'source-branch', 'source-commit', 'source-depth', 'source-subdir', 'source-tag', 'source-type', 'plugin', 'stage-packages', - 'parse-info']: + 'parse-info', 'override-pull']: self.assertThat(state.properties, Contains(expected)) self.assertTrue(type(state.project_options) is OrderedDict) self.assertThat(state.project_options, Contains('deb_arch')) @@ -900,16 +953,50 @@ files = state.extracted_metadata['files'] self.assertThat(files, Equals(['metadata-file'])) + @patch('snapcraft.internal.repo.Repo') + def test_pull_state_with_scriptlet_metadata(self, repo_mock): + self.handler = self.load_part('test_part', part_properties={ + 'override-pull': 'snapcraftctl set-version override-version' + }) + + self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) + repo_mock.get_installed_build_packages.return_value = [] + + self.handler.pull() + + self.assertThat(self.handler.last_step(), Equals('pull')) + self.assertThat(self.handler.next_step(), Equals('build')) + state = self.handler.get_pull_state() + + self.assertTrue(state, 'Expected pull to save state YAML') + self.assertTrue(type(state) is states.PullState) + self.assertTrue(type(state.properties) is OrderedDict) + self.assertThat(len(state.properties), Equals(11)) + for expected in ['source', 'source-branch', 'source-commit', + 'source-depth', 'source-subdir', 'source-tag', + 'source-type', 'plugin', 'stage-packages', + 'parse-info', 'override-pull']: + self.assertThat(state.properties, Contains(expected)) + self.assertThat( + state.properties['override-pull'], + Equals('snapcraftctl set-version override-version')) + + metadata = state.scriptlet_metadata + self.assertThat(metadata.get_version(), Equals('override-version')) + def test_pull_state_with_properties(self): self.get_pull_properties_mock.return_value = ['foo'] self.handler.plugin.options.foo = 'bar' self.handler._part_properties = {'foo': 'bar'} self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) self.handler.pull() self.assertThat(self.handler.last_step(), Equals('pull')) + self.assertThat(self.handler.next_step(), Equals('build')) state = states.get_state(self.handler.plugin.statedir, 'pull') self.assertTrue(state, 'Expected pull to save state YAML') @@ -923,6 +1010,7 @@ @patch.object(nil.NilPlugin, 'clean_pull') def test_clean_pull_state(self, mock_clean_pull): self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) self.handler.pull() @@ -932,22 +1020,25 @@ mock_clean_pull.assert_called_once_with() self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) def test_build_state(self): self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) self.handler.build() self.assertThat(self.handler.last_step(), Equals('build')) + self.assertThat(self.handler.next_step(), Equals('stage')) state = states.get_state(self.handler.plugin.statedir, 'build') 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(8)) + self.assertThat(len(state.properties), Equals(9)) for expected in ['after', 'build-attributes', 'build-packages', 'disable-parallel', 'organize', 'prepare', 'build', - 'install']: + 'install', 'override-build']: self.assertTrue(expected in state.properties) self.assertTrue(type(state.project_options) is OrderedDict) self.assertTrue('deb_arch' in state.project_options) @@ -974,19 +1065,21 @@ 'fake', _fake_extractor)) self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) self.handler.build() self.assertThat(self.handler.last_step(), Equals('build')) + self.assertThat(self.handler.next_step(), Equals('stage')) state = self.handler.get_build_state() 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(8)) + self.assertThat(len(state.properties), Equals(9)) for expected in ['after', 'build-attributes', 'build-packages', 'disable-parallel', 'organize', 'prepare', 'build', - 'install']: + 'install', 'override-build']: self.assertThat(state.properties, Contains(expected)) self.assertTrue(type(state.project_options) is OrderedDict) self.assertThat(state.project_options, Contains('deb_arch')) @@ -1006,17 +1099,49 @@ files = state.extracted_metadata['files'] self.assertThat(files, Equals(['metadata-file'])) + def test_build_state_with_scriptlet_metadata(self): + self.handler = self.load_part('test_part', part_properties={ + 'override-build': 'snapcraftctl set-version override-version' + }) + + self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) + + self.handler.pull() + self.handler.build() + + self.assertThat(self.handler.last_step(), Equals('build')) + self.assertThat(self.handler.next_step(), Equals('stage')) + state = self.handler.get_build_state() + + 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(9)) + for expected in ['after', 'build-attributes', 'build-packages', + 'disable-parallel', 'organize', 'prepare', 'build', + 'install', 'override-build']: + self.assertThat(state.properties, Contains(expected)) + self.assertThat( + state.properties['override-build'], + Equals('snapcraftctl set-version override-version')) + + metadata = state.scriptlet_metadata + self.assertThat(metadata.get_version(), Equals('override-version')) + def test_build_state_with_properties(self): self.get_build_properties_mock.return_value = ['foo'] self.handler.plugin.options.foo = 'bar' self.handler._part_properties = {'foo': 'bar'} self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) self.handler.build() self.assertThat(self.handler.last_step(), Equals('build')) - state = states.get_state(self.handler.plugin.statedir, 'build') + self.assertThat(self.handler.next_step(), Equals('stage')) + state = self.handler.get_build_state() self.assertTrue(state, 'Expected build to save state YAML') self.assertTrue(type(state) is states.BuildState) @@ -1039,9 +1164,11 @@ mock_clean_build.assert_called_once_with() self.assertThat(self.handler.last_step(), Equals('pull')) + self.assertThat(self.handler.next_step(), Equals('build')) def test_stage_state(self): self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.handler.plugin.installdir, 'bin') os.makedirs(bindir) @@ -1052,7 +1179,8 @@ self.handler.stage() self.assertThat(self.handler.last_step(), Equals('stage')) - state = states.get_state(self.handler.plugin.statedir, 'stage') + self.assertThat(self.handler.next_step(), Equals('prime')) + state = self.handler.get_stage_state() self.assertTrue(state, 'Expected stage to save state YAML') self.assertTrue(type(state) is states.StageState) @@ -1071,11 +1199,43 @@ self.assertTrue(type(state.project_options) is OrderedDict) self.assertThat(len(state.project_options), Equals(0)) + def test_stage_state_with_scriptlet_metadata(self): + self.handler = self.load_part('test_part', part_properties={ + 'override-stage': 'snapcraftctl set-version override-version' + }) + + self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) + + self.handler.pull() + self.handler.build() + self.handler.stage() + + self.assertThat(self.handler.last_step(), Equals('stage')) + self.assertThat(self.handler.next_step(), Equals('prime')) + state = self.handler.get_stage_state() + + self.assertTrue(state, 'Expected stage to save state YAML') + self.assertTrue(type(state) is states.StageState) + self.assertTrue(type(state.files) is set) + self.assertTrue(type(state.directories) is set) + self.assertTrue(type(state.properties) is OrderedDict) + self.assertThat(len(state.properties), Equals(3)) + for expected in ['stage', 'filesets', 'override-stage']: + self.assertThat(state.properties, Contains(expected)) + self.assertThat( + state.properties['override-stage'], + Equals('snapcraftctl set-version override-version')) + + metadata = state.scriptlet_metadata + self.assertThat(metadata.get_version(), Equals('override-version')) + def test_stage_state_with_stage_keyword(self): self.handler.plugin.options.stage = ['bin/1'] self.handler._part_properties = {'stage': ['bin/1']} self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.handler.plugin.installdir, 'bin') os.makedirs(bindir) @@ -1086,7 +1246,8 @@ self.handler.stage() self.assertThat(self.handler.last_step(), Equals('stage')) - state = states.get_state(self.handler.plugin.statedir, 'stage') + self.assertThat(self.handler.next_step(), Equals('prime')) + state = self.handler.get_stage_state() self.assertTrue(state, 'Expected stage to save state YAML') self.assertTrue(type(state) is states.StageState) @@ -1103,9 +1264,11 @@ self.assertThat(len(state.project_options), Equals(0)) self.assertThat(self.handler.last_step(), Equals('stage')) + self.assertThat(self.handler.next_step(), Equals('prime')) def test_clean_stage_state(self): self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.stage_dir, 'bin') os.makedirs(bindir) open(os.path.join(bindir, '1'), 'w').close() @@ -1119,6 +1282,7 @@ self.handler.clean_stage({}) self.assertThat(self.handler.last_step(), Equals('build')) + self.assertThat(self.handler.next_step(), Equals('stage')) self.assertFalse(os.path.exists(bindir)) def test_clean_stage_state_multiple_parts(self): @@ -1137,6 +1301,7 @@ self.handler.clean_stage({}) self.assertThat(self.handler.last_step(), Equals('build')) + self.assertThat(self.handler.next_step(), Equals('stage')) self.assertFalse(os.path.exists(os.path.join(bindir, '1'))) self.assertFalse(os.path.exists(os.path.join(bindir, '2'))) self.assertTrue( @@ -1160,6 +1325,7 @@ }) self.assertThat(self.handler.last_step(), Equals('build')) + self.assertThat(self.handler.next_step(), Equals('stage')) self.assertFalse(os.path.exists(os.path.join(bindir, '1'))) self.assertTrue( os.path.exists(os.path.join(bindir, '2')), @@ -1176,6 +1342,7 @@ @patch('shutil.copy') def test_prime_state(self, mock_copy): self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.handler.plugin.installdir, 'bin') os.makedirs(bindir) @@ -1187,11 +1354,12 @@ self.handler.prime() self.assertThat(self.handler.last_step(), Equals('prime')) + self.assertThat(self.handler.next_step(), Equals(None)) self.get_elf_files_mock.assert_called_once_with(self.handler.primedir, {'bin/1', 'bin/2'}) self.assertFalse(mock_copy.called) - state = states.get_state(self.handler.plugin.statedir, 'prime') + state = self.handler.get_prime_state() self.assertTrue(type(state) is states.PrimeState) self.assertTrue(type(state.files) is set) @@ -1209,9 +1377,43 @@ self.assertTrue(type(state.project_options) is OrderedDict) self.assertThat(len(state.project_options), Equals(0)) + def test_prime_state_with_scriptlet_metadata(self): + self.handler = self.load_part('test_part', part_properties={ + 'override-prime': 'snapcraftctl set-version override-version' + }) + + self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) + + self.handler.pull() + self.handler.build() + self.handler.stage() + self.handler.prime() + + self.assertThat(self.handler.last_step(), Equals('prime')) + self.assertThat(self.handler.next_step(), Equals(None)) + state = self.handler.get_prime_state() + + self.assertTrue(state, 'Expected prime to save state YAML') + self.assertTrue(type(state) is states.PrimeState) + self.assertTrue(type(state.files) is set) + self.assertTrue(type(state.directories) is set) + self.assertTrue(type(state.dependency_paths) is set) + self.assertTrue(type(state.properties) is OrderedDict) + self.assertThat(len(state.properties), Equals(2)) + for expected in ['prime', 'override-prime']: + self.assertThat(state.properties, Contains(expected)) + self.assertThat( + state.properties['override-prime'], + Equals('snapcraftctl set-version override-version')) + + metadata = state.scriptlet_metadata + self.assertThat(metadata.get_version(), Equals('override-version')) + @patch('shutil.copy') def test_prime_state_with_stuff_already_primed(self, mock_copy): self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.handler.plugin.installdir, 'bin') os.makedirs(bindir) @@ -1225,6 +1427,7 @@ self.handler.prime() self.assertThat(self.handler.last_step(), Equals('prime')) + self.assertThat(self.handler.next_step(), Equals(None)) # bin/2 shouldn't be in this list as it was already primed by another # part. self.get_elf_files_mock.assert_called_once_with(self.handler.primedir, @@ -1265,6 +1468,7 @@ elf.ElfFile(path=os.path.join(self.handler.primedir, 'bin', '2')), ]) self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.handler.plugin.installdir, 'bin') os.makedirs(bindir) @@ -1276,6 +1480,7 @@ self.handler.prime() self.assertThat(self.handler.last_step(), Equals('prime')) + self.assertThat(self.handler.next_step(), Equals(None)) self.get_elf_files_mock.assert_called_once_with( self.handler.primedir, {'bin/1', 'bin/2'}) mock_migrate_files.assert_has_calls([ @@ -1330,6 +1535,7 @@ ]) self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.handler.plugin.installdir, 'bin') os.makedirs(bindir) @@ -1341,6 +1547,7 @@ self.handler.prime() self.assertThat(self.handler.last_step(), Equals('prime')) + self.assertThat(self.handler.next_step(), Equals(None)) self.get_elf_files_mock.assert_called_once_with( self.handler.primedir, {'bin/file'}) # Verify that only the part's files were migrated-- not the system @@ -1369,6 +1576,7 @@ self.get_elf_files_mock.return_value = frozenset([ elf.ElfFile(path='bin/1')]) self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.handler.plugin.installdir, 'bin') foobardir = os.path.join(self.handler.plugin.installdir, 'foo', 'bar') @@ -1386,6 +1594,7 @@ self.handler.prime() self.assertThat(self.handler.last_step(), Equals('prime')) + self.assertThat(self.handler.next_step(), Equals(None)) self.get_elf_files_mock.assert_called_once_with( self.handler.primedir, {'bin/1', 'foo/bar/baz'}) mock_migrate_files.assert_called_once_with( @@ -1404,6 +1613,7 @@ 'test_part', part_properties={'prime': ['bin/1']}) self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.handler.plugin.installdir, 'bin') os.makedirs(bindir) @@ -1415,6 +1625,7 @@ self.handler.prime() self.assertThat(self.handler.last_step(), Equals('prime')) + self.assertThat(self.handler.next_step(), Equals(None)) self.get_elf_files_mock.assert_called_once_with(self.handler.primedir, {'bin/1'}) self.assertFalse(mock_copy.called) @@ -1438,6 +1649,7 @@ def test_clean_prime_state(self): self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.prime_dir, 'bin') os.makedirs(bindir) open(os.path.join(bindir, '1'), 'w').close() @@ -1451,10 +1663,12 @@ self.handler.clean_prime({}) self.assertThat(self.handler.last_step(), Equals('stage')) + self.assertThat(self.handler.next_step(), Equals('prime')) self.assertFalse(os.path.exists(bindir)) def test_clean_prime_state_multiple_parts(self): self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.prime_dir, 'bin') os.makedirs(bindir) open(os.path.join(bindir, '1'), 'w').close() @@ -1469,6 +1683,7 @@ self.handler.clean_prime({}) self.assertThat(self.handler.last_step(), Equals('stage')) + self.assertThat(self.handler.next_step(), Equals('prime')) self.assertFalse(os.path.exists(os.path.join(bindir, '1'))) self.assertFalse(os.path.exists(os.path.join(bindir, '2'))) self.assertTrue( @@ -1477,6 +1692,7 @@ def test_clean_prime_state_common_files(self): self.assertThat(self.handler.last_step(), Equals(None)) + self.assertThat(self.handler.next_step(), Equals('pull')) bindir = os.path.join(self.prime_dir, 'bin') os.makedirs(bindir) open(os.path.join(bindir, '1'), 'w').close() @@ -1492,6 +1708,7 @@ }) self.assertThat(self.handler.last_step(), Equals('stage')) + self.assertThat(self.handler.next_step(), Equals('prime')) self.assertFalse(os.path.exists(os.path.join(bindir, '1'))) self.assertTrue( os.path.exists(os.path.join(bindir, '2')), diff -Nru snapcraft-2.40/tests/unit/pluginhandler/test_plugin_loader.py snapcraft-2.41/tests/unit/pluginhandler/test_plugin_loader.py --- snapcraft-2.40/tests/unit/pluginhandler/test_plugin_loader.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/pluginhandler/test_plugin_loader.py 2018-04-14 12:13:35.000000000 +0000 @@ -128,6 +128,10 @@ self.osrepodir = 'osrepodir' self.statedir = 'statedir' self.sourcedir = 'sourcedir' + self.build_basedir = 'build_basedir' + + def build(self): + pass plugin_mock.return_value = NonBaseOldPlugin local_load_mock.side_effect = ImportError() diff -Nru snapcraft-2.40/tests/unit/pluginhandler/test_runner.py snapcraft-2.41/tests/unit/pluginhandler/test_runner.py --- snapcraft-2.40/tests/unit/pluginhandler/test_runner.py 1970-01-01 00:00:00.000000000 +0000 +++ snapcraft-2.41/tests/unit/pluginhandler/test_runner.py 2018-04-14 12:13:35.000000000 +0000 @@ -0,0 +1,403 @@ +# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- +# +# Copyright (C) 2016-2018 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 functools +import logging +import os +import subprocess +from textwrap import dedent + +import fixtures +from unittest import mock +from testtools.matchers import Contains, FileContains, FileExists + +from snapcraft.internal import errors +from snapcraft.internal.pluginhandler import _runner +from tests import ( + fixture_setup, + unit, +) + + +def _fake_pull(): + open(os.path.join('sourcedir', 'fake-pull'), 'w').close() + + +def _fake_build(): + open(os.path.join('builddir', 'fake-build'), 'w').close() + + +def _fake_stage(): + open(os.path.join('stagedir', 'fake-stage'), 'w').close() + + +def _fake_prime(): + open(os.path.join('primedir', 'fake-prime'), 'w').close() + + +class RunnerTestCase(unit.TestCase): + + def test_pull(self): + os.mkdir('sourcedir') + + runner = _runner.Runner( + part_properties={'override-pull': 'touch pull'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + runner.pull() + + self.assertThat(os.path.join('sourcedir', 'pull'), FileExists()) + + def test_builtin_function_from_pull(self): + os.mkdir('sourcedir') + + runner = _runner.Runner( + part_properties={'override-pull': 'snapcraftctl pull'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={'pull': _fake_pull}) + + runner.pull() + + self.assertThat(os.path.join('sourcedir', 'fake-pull'), FileExists()) + + def test_prepare(self): + os.mkdir('builddir') + + runner = _runner.Runner( + part_properties={'prepare': 'touch prepare'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + runner.prepare() + + self.assertThat(os.path.join('builddir', 'prepare'), FileExists()) + + def test_builtin_function_from_prepare(self): + os.mkdir('builddir') + + runner = _runner.Runner( + part_properties={'prepare': 'snapcraftctl build'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={'build': _fake_build}) + + runner.prepare() + + self.assertThat(os.path.join('builddir', 'fake-build'), FileExists()) + + def test_snapcraftctl_alias_if_snap(self): + self.useFixture(fixture_setup.FakeSnapcraftIsASnap()) + + os.mkdir('builddir') + + runner = _runner.Runner( + part_properties={ + 'prepare': 'alias snapcraftctl > definition' + }, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + with mock.patch('os.path.exists', return_value=True): + runner.prepare() + + expected_snapcrafctl = '/snap/snapcraft/current/bin/snapcraftctl' + + self.assertThat(os.path.join('builddir', 'definition'), FileExists()) + self.assertThat( + os.path.join('builddir', 'definition'), + FileContains('snapcraftctl={!r}\n'.format(expected_snapcrafctl))) + + def test_old_build(self): + os.mkdir('builddir') + + runner = _runner.Runner( + part_properties={'build': 'touch build'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + runner.build() + + self.assertThat(os.path.join('builddir', 'build'), FileExists()) + + def test_build(self): + os.mkdir('builddir') + + runner = _runner.Runner( + part_properties={'override-build': 'touch build'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + runner.build() + + self.assertThat(os.path.join('builddir', 'build'), FileExists()) + + def test_builtin_function_from_build(self): + os.mkdir('builddir') + + runner = _runner.Runner( + part_properties={'override-build': 'snapcraftctl build'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={'build': _fake_build}) + + runner.build() + + self.assertThat(os.path.join('builddir', 'fake-build'), FileExists()) + + def test_install(self): + os.mkdir('builddir') + + runner = _runner.Runner( + part_properties={'install': 'touch install'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + runner.install() + + self.assertThat(os.path.join('builddir', 'install'), FileExists()) + + def test_builtin_function_from_install(self): + os.mkdir('builddir') + + runner = _runner.Runner( + part_properties={'install': 'snapcraftctl build'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={'build': _fake_build}) + + runner.install() + + self.assertThat(os.path.join('builddir', 'fake-build'), FileExists()) + + def test_stage(self): + os.mkdir('stagedir') + + runner = _runner.Runner( + part_properties={'override-stage': 'touch stage'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + runner.stage() + + self.assertThat(os.path.join('stagedir', 'stage'), FileExists()) + + def test_builtin_function_from_stage(self): + os.mkdir('stagedir') + + runner = _runner.Runner( + part_properties={'override-stage': 'snapcraftctl stage'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={'stage': _fake_stage}) + + runner.stage() + + self.assertThat(os.path.join('stagedir', 'fake-stage'), FileExists()) + + def test_prime(self): + os.mkdir('primedir') + + runner = _runner.Runner( + part_properties={'override-prime': 'touch prime'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + runner.prime() + + self.assertThat(os.path.join('primedir', 'prime'), FileExists()) + + def test_builtin_function_from_prime(self): + os.mkdir('primedir') + + runner = _runner.Runner( + part_properties={'override-prime': 'snapcraftctl prime'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={'prime': _fake_prime}) + + runner.prime() + + self.assertThat(os.path.join('primedir', 'fake-prime'), FileExists()) + + +class RunnerFailureTestCase(unit.TestCase): + + def test_failure_on_last_script_command_results_in_failure(self): + os.mkdir('builddir') + + script = dedent("""\ + touch success + false # this should trigger an error + """) + + runner = _runner.Runner( + part_properties={'build': script}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + self.assertRaises(errors.ScriptletRunError, runner.build) + + def test_failure_to_execute_mid_script_results_in_failure(self): + os.mkdir('builddir') + + script = dedent("""\ + false # this should trigger an error + touch success + """) + + runner = _runner.Runner( + part_properties={'build': script}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + self.assertRaises(errors.ScriptletRunError, runner.build) + + def test_snapcraftctl_no_alias_if_not_snap(self): + os.mkdir('builddir') + + runner = _runner.Runner( + part_properties={'build': 'alias snapcraftctl 2> /dev/null'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + self.assertRaises(errors.ScriptletRunError, runner.build) + + def test_snapcraftctl_errors_on_exception(self): + os.mkdir('primedir') + + class _TestException(errors.ScriptletBaseError): + fmt = "I'm an error" + + def _raise(): + raise _TestException() + + runner = _runner.Runner( + part_properties={'override-prime': 'snapcraftctl prime'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={'prime': _raise}) + + silent_popen = functools.partial( + subprocess.Popen, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + + with mock.patch('subprocess.Popen', wraps=silent_popen): + self.assertRaises(errors.ScriptletRunError, runner.prime) + + +class RunnerDeprecationTestCase(unit.TestCase): + + def setUp(self): + super().setUp() + + os.mkdir('builddir') + + def test_prepare_deprecation(self): + self.fake_logger = fixtures.FakeLogger(level=logging.INFO) + self.useFixture(self.fake_logger) + + _runner.Runner( + part_properties={'prepare': 'foo'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + self.assertThat(self.fake_logger.output, Contains( + "DEPRECATED: The 'prepare' keyword has been replaced by " + "'override-build'")) + + def test_build_deprecation(self): + self.fake_logger = fixtures.FakeLogger(level=logging.INFO) + self.useFixture(self.fake_logger) + + _runner.Runner( + part_properties={'build': 'foo'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + self.assertThat(self.fake_logger.output, Contains( + "DEPRECATED: The 'build' keyword has been replaced by " + "'override-build'")) + + def test_install_deprecation(self): + self.fake_logger = fixtures.FakeLogger(level=logging.INFO) + self.useFixture(self.fake_logger) + + _runner.Runner( + part_properties={'install': 'foo'}, + sourcedir='sourcedir', + builddir='builddir', + stagedir='stagedir', + primedir='primedir', + builtin_functions={}) + + self.assertThat(self.fake_logger.output, Contains( + "DEPRECATED: The 'install' keyword has been replaced by " + "'override-build'")) diff -Nru snapcraft-2.40/tests/unit/pluginhandler/test_scriptlets.py snapcraft-2.41/tests/unit/pluginhandler/test_scriptlets.py --- snapcraft-2.40/tests/unit/pluginhandler/test_scriptlets.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/pluginhandler/test_scriptlets.py 2018-04-14 12:13:35.000000000 +0000 @@ -1,6 +1,6 @@ # -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*- # -# Copyright (C) 2016-2018 Canonical Ltd +# Copyright (C) 2018 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,53 +14,157 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +import contextlib +import functools import os -from subprocess import CalledProcessError -from textwrap import dedent +import subprocess -from testtools.matchers import FileExists +import testtools +from testtools.matchers import Equals, FileExists +from testscenarios.scenarios import multiply_scenarios +from unittest import mock + +from snapcraft.internal import errors from tests import unit -class ScriptletTestCase(unit.TestCase): +class ScriptletSetterTestCase(unit.TestCase): + + scenarios = [ + ('set-version', {'setter': 'set-version', 'getter': 'get_version'}), + ('set-grade', {'setter': 'set-grade', 'getter': 'get_grade'}), + ] + + def test_set_in_pull(self): + handler = self.load_part('test_part', part_properties={ + 'override-pull': 'snapcraftctl {} test-value'.format( + self.setter) + }) + + handler.pull() + metadata = handler.get_pull_state().scriptlet_metadata + self.assertThat(getattr(metadata, self.getter)(), Equals('test-value')) + + def test_set_in_build(self): + handler = self.load_part('test_part', part_properties={ + 'override-build': 'snapcraftctl {} test-value'.format( + self.setter) + }) + + handler.pull() + handler.build() + metadata = handler.get_build_state().scriptlet_metadata + self.assertThat(getattr(metadata, self.getter)(), Equals('test-value')) + self.assertFalse(handler.get_pull_state().scriptlet_metadata) + + def test_set_in_stage(self): + handler = self.load_part('test_part', part_properties={ + 'override-stage': 'snapcraftctl {} test-value'.format( + self.setter) + }) + + handler.pull() + handler.build() + handler.stage() + metadata = handler.get_stage_state().scriptlet_metadata + self.assertThat(getattr(metadata, self.getter)(), Equals('test-value')) + self.assertFalse(handler.get_pull_state().scriptlet_metadata) + self.assertFalse(handler.get_build_state().scriptlet_metadata) + + def test_set_in_prime(self): + handler = self.load_part('test_part', part_properties={ + 'override-prime': 'snapcraftctl {} test-value'.format( + self.setter) + }) + + handler.pull() + handler.build() + handler.stage() + handler.prime() + metadata = handler.get_prime_state().scriptlet_metadata + self.assertThat(getattr(metadata, self.getter)(), Equals('test-value')) + self.assertFalse(handler.get_pull_state().scriptlet_metadata) + self.assertFalse(handler.get_build_state().scriptlet_metadata) + self.assertFalse(handler.get_stage_state().scriptlet_metadata) + + +class ScriptletMultipleSettersErrorTestCase(unit.TestCase): + + scriptlet_scenarios = [ + ('override-pull', {'override_pull': 'snapcraftctl {setter} 1'}), + ('override-build', {'override_build': 'snapcraftctl {setter} 2'}), + ('override-stage', {'override_stage': 'snapcraftctl {setter} 3'}), + ('override-prime', {'override_prime': 'snapcraftctl {setter} 4'}), + ] + + multiple_setters_scenarios = multiply_scenarios( + scriptlet_scenarios, scriptlet_scenarios) + + setter_scenarios = [ + ('set-version', {'setter': 'set-version'}), + ('set-grade', {'setter': 'set-grade'}), + ] + + scenarios = multiply_scenarios( + setter_scenarios, multiple_setters_scenarios) + + def test_set_multiple_times(self): + part_properties = {} + with contextlib.suppress(AttributeError): + part_properties['override-pull'] = self.override_pull.format( + setter=self.setter) + with contextlib.suppress(AttributeError): + part_properties['override-build'] = self.override_build.format( + setter=self.setter) + with contextlib.suppress(AttributeError): + part_properties['override-stage'] = self.override_stage.format( + setter=self.setter) + with contextlib.suppress(AttributeError): + part_properties['override-prime'] = self.override_prime.format( + setter=self.setter) + + # A few of these test cases result in only one of these scriptlets + # being set. In that case, we actually want to double them up (i.e. + # call set-version twice in the same scriptlet), which should still be + # an error. + if len(part_properties) == 1: + for key, value in part_properties.items(): + part_properties[key] += '\n{}'.format(value) + + handler = self.load_part('test_part', part_properties=part_properties) + + with testtools.ExpectedException(errors.ScriptletRunError): + silent_popen = functools.partial( + subprocess.Popen, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL) + + with mock.patch('subprocess.Popen', wraps=silent_popen): + handler.pull() + handler.build() + handler.stage() + handler.prime() + + +# These are deprecated +class OldScripletTestCase(unit.TestCase): def test_run_prepare_scriptlet(self): handler = self.load_part( - 'test-part', part_properties={'prepare': 'touch before-build'}) + 'test-part', part_properties={'prepare': 'touch prepare'}) handler.build() before_build_file_path = os.path.join(handler.plugin.build_basedir, - 'before-build') + 'prepare') self.assertThat(before_build_file_path, FileExists()) def test_run_install_scriptlet(self): handler = self.load_part( - 'test-part', part_properties={'install': 'touch after-build'}) + 'test-part', part_properties={'install': 'touch install'}) handler.build() after_build_file_path = os.path.join(handler.plugin.build_basedir, - 'after-build') + 'install') 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.40/tests/unit/plugins/test_catkin.py snapcraft-2.41/tests/unit/plugins/test_catkin.py --- snapcraft-2.40/tests/unit/plugins/test_catkin.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/plugins/test_catkin.py 2018-04-14 12:13:35.000000000 +0000 @@ -717,8 +717,23 @@ environment = self._verify_run_environment(plugin) - self.assertThat(environment, Contains('. {}'.format( - os.path.join(plugin.rosdir, 'snapcraft-setup.sh')))) + setup_path = os.path.join(plugin.rosdir, 'snapcraft-setup.sh') + lines_of_interest = [ + 'if [ -f {} ]; then'.format(setup_path), + '. {}'.format(setup_path), + 'fi', + ] + actual_lines = [] + + for line in environment: + line = line.strip() + if line in lines_of_interest: + actual_lines.append(line) + + self.assertThat( + actual_lines, Equals(lines_of_interest), + 'Expected snapcraft-setup.sh to be sourced after checking for its ' + 'existence') @mock.patch.object(catkin.CatkinPlugin, '_source_setup_sh', return_value='test-source-setup.sh') diff -Nru snapcraft-2.40/tests/unit/plugins/test_dotnet.py snapcraft-2.41/tests/unit/plugins/test_dotnet.py --- snapcraft-2.40/tests/unit/plugins/test_dotnet.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/plugins/test_dotnet.py 2018-04-14 12:13:35.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 json import os import subprocess import tarfile +from textwrap import dedent from unittest import mock from testtools.matchers import ( @@ -48,7 +50,7 @@ self.assertThat(schema, Not(Contains('required'))) def test_get_pull_properties(self): - expected_pull_properties = [] + expected_pull_properties = ['dotnet-runtime-version'] self.assertThat( dotnet.DotNetPlugin.get_pull_properties(), Equals(expected_pull_properties)) @@ -67,6 +69,7 @@ class Options: build_attributes = [] + dotnet_runtime_version = dotnet._RUNTIME_DEFAULT self.options = Options() self.project = snapcraft.ProjectOptions() @@ -79,6 +82,49 @@ patcher.start() self.addCleanup(patcher.stop) + def fake_urlopen(request): + return FakeResponse(request.full_url, checksum) + + class FakeResponse: + def __init__(self, url: str, checksum: str) -> None: + self._url = url + self._checksum = checksum + + def read(self): + if self._url.endswith('releases.json'): + data = json.dumps([ + {'version-runtime': dotnet._RUNTIME_DEFAULT, + 'blob-sdk': + 'https://dotnetcli.blob.core.windows.net/dotnet/' + 'Sdk/2.1.4/', + 'sdk-linux-x64': 'dotnet-sdk-2.1.4-linux-x64.tar.gz', + 'checksums-sdk': + 'https://dotnetcli.blob.core.windows.net/dotnet/' + 'checksums/2.1.4-sdk-sha.txt'}, + {'version-sdk': '2.1.104'}, + ]).encode('utf-8') + else: + # A checksum file with a list of checksums and archives. + # We fill in the computed checksum used in the pull test. + data = bytes(dedent("""\ + Hash: SHA512 + + 05fe90457a8b77ad5a5eb2f22348f53e962012a + {} dotnet-sdk-2.1.4-linux-x64.tar.gz + """).format(self._checksum), 'utf-8') + return data + + with tarfile.open('test-sdk.tar', 'w') as test_sdk_tar: + open('test-sdk', 'w').close() + test_sdk_tar.add('test-sdk') + checksum = file_utils.calculate_hash( + 'test-sdk.tar', algorithm='sha512') + + patcher = mock.patch('urllib.request.urlopen') + urlopen_mock = patcher.start() + urlopen_mock.side_effect = fake_urlopen + self.addCleanup(patcher.stop) + original_check_call = subprocess.check_call patcher = mock.patch('subprocess.check_call') self.mock_check_call = patcher.start() @@ -93,6 +139,36 @@ self.mock_check_call.side_effect = side_effect +class DotNetErrorsTestCase(unit.TestCase): + + scenarios = ( + ('DotNetBadArchitectureError', { + 'exception': dotnet.DotNetBadArchitectureError, + 'kwargs': { + 'architecture': 'wrong-arch', + 'supported': ['arch'], + }, + 'expected_message': ( + "Failed to prepare the .NET SDK: " + "The architecture 'wrong-arch' is not supported. " + "Supported architectures are: 'arch'.")}), + ('DotNetBadReleaseDataError', { + 'exception': dotnet.DotNetBadReleaseDataError, + 'kwargs': { + 'version': 'test', + }, + 'expected_message': ( + "Failed to prepare the .NET SDK: " + "An error occurred while fetching the version details " + "for 'test'. Check that the version is correct.")}), + ) + + def test_error_formatting(self): + self.assertThat( + str(self.exception(**self.kwargs)), + Equals(self.expected_message)) + + class DotNetProjectTestCase(DotNetProjectBaseTestCase): def test_init_with_non_amd64_architecture(self): @@ -101,25 +177,14 @@ new_callable=mock.PropertyMock, return_value='non-amd64'): error = self.assertRaises( - NotImplementedError, + dotnet.DotNetBadArchitectureError, dotnet.DotNetPlugin, 'test-part', self.options, self.project) - self.assertThat( - str(error), - Equals("This plugin does not support architecture 'non-amd64'")) + self.assertThat(error.architecture, Equals('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) - + plugin = dotnet.DotNetPlugin( + 'test-part', self.options, self.project) with mock.patch.object( sources.Tar, 'download', return_value='test-sdk.tar'): plugin.pull() diff -Nru snapcraft-2.40/tests/unit/plugins/test_kernel.py snapcraft-2.41/tests/unit/plugins/test_kernel.py --- snapcraft-2.40/tests/unit/plugins/test_kernel.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/plugins/test_kernel.py 2018-04-14 12:13:35.000000000 +0000 @@ -21,7 +21,7 @@ from unittest import mock import fixtures -from testtools.matchers import Equals, FileContains, HasLength +from testtools.matchers import Contains, Equals, FileContains, HasLength from textwrap import dedent @@ -283,9 +283,9 @@ modprobe_cmd = ['modprobe', '-n', '--show-depends', '-d', plugin.installdir, '-S', '4.4', ] self.run_output_mock.assert_has_calls([ - mock.call(modprobe_cmd + ['squashfs'])]) + mock.call(modprobe_cmd + ['squashfs'], env=mock.ANY)]) self.run_output_mock.assert_has_calls([ - mock.call(modprobe_cmd + ['vfat'])]) + mock.call(modprobe_cmd + ['vfat'], env=mock.ANY)]) def test_pack_initrd_modules_return_same_deps(self): self.options.kernel_initrd_modules = [ @@ -318,12 +318,12 @@ modprobe_cmd = ['modprobe', '-n', '--show-depends', '-d', plugin.installdir, '-S', '4.4', ] self.run_output_mock.assert_has_calls([ - mock.call(modprobe_cmd + ['squashfs'])]) + mock.call(modprobe_cmd + ['squashfs'], env=mock.ANY)]) self.run_output_mock.assert_has_calls([ - mock.call(modprobe_cmd + ['vfat'])]) + mock.call(modprobe_cmd + ['vfat'], env=mock.ANY)]) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_build_with_kconfigfile(self): self.options.kconfigfile = 'config' @@ -363,7 +363,7 @@ self._assert_common_assets(plugin.installdir) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_build_verbose_with_kconfigfile(self): fake_logger = fixtures.FakeLogger(level=logging.DEBUG) @@ -421,7 +421,7 @@ self._assert_common_assets(plugin.installdir) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_check_config(self): fake_logger = fixtures.FakeLogger(level=logging.WARNING) @@ -446,7 +446,7 @@ self.assertIn('CONFIG_{}'.format(warn), fake_logger.output) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_check_initrd(self): fake_logger = fixtures.FakeLogger(level=logging.WARNING) @@ -471,7 +471,7 @@ fake_logger.output) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_build_with_kconfigfile_and_kconfigs(self): self.options.kconfigfile = 'config' @@ -524,7 +524,7 @@ self._assert_common_assets(plugin.installdir) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_build_with_defconfig_and_kconfigs(self): self.options.kdefconfig = ['defconfig'] @@ -584,7 +584,7 @@ self._assert_common_assets(plugin.installdir) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_build_with_two_defconfigs(self): self.options.kdefconfig = ['defconfig', 'defconfig2'] @@ -627,7 +627,7 @@ self._assert_common_assets(plugin.installdir) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_build_with_kconfigfile_and_dtbs(self): self.options.kconfigfile = 'config' @@ -689,7 +689,7 @@ Equals("No match for dtb 'fake-dtb.dtb' was found")) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_build_with_kconfigfile_and_modules(self): self.options.kconfigfile = 'config' @@ -724,6 +724,10 @@ self.run_output_mock.side_effect = fake_output + # Set a path that doesn't contain '/sbin' so we can verify that it's + # added to the modprobe call. + self.useFixture(fixtures.EnvironmentVariable('PATH', '/usr/bin')) + plugin.build() self._assert_generic_check_call(plugin.builddir, plugin.installdir, @@ -742,11 +746,22 @@ plugin.installdir, 'lib', 'firmware'))]) ]) + class _check_env: + def __init__(self, test): + self.test = test + + def __eq__(self, other): + self.test.assertThat(other, Contains('PATH')) + paths = other['PATH'].split(':') + self.test.assertThat(paths, Contains('/sbin')) + return True + self.assertThat(self.run_output_mock.call_count, Equals(1)) self.run_output_mock.assert_has_calls([ mock.call([ 'modprobe', '-n', '--show-depends', '-d', - plugin.installdir, '-S', '4.4.2', 'my-fake-module'])]) + plugin.installdir, '-S', '4.4.2', 'my-fake-module'], + env=_check_env(self))]) config_file = os.path.join(plugin.builddir, '.config') self.assertTrue(os.path.exists(config_file)) @@ -758,7 +773,7 @@ self._assert_common_assets(plugin.installdir) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_build_with_kconfigfile_and_firmware(self): self.options.kconfigfile = 'config' @@ -812,7 +827,7 @@ plugin.installdir, 'firmware', 'fake-fw-dir'))) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_build_with_kconfigfile_and_no_firmware(self): self.options.kconfigfile = 'config' @@ -846,7 +861,7 @@ self.assertTrue(os.path.exists(config_file)) @mock.patch.object( - snapcraft._options.ProjectOptions, + snapcraft.ProjectOptions, 'kernel_arch', new='not_arm') def test_build_with_kconfigflavour(self): arch = self.project_options.deb_arch diff -Nru snapcraft-2.40/tests/unit/plugins/test_nodejs.py snapcraft-2.41/tests/unit/plugins/test_nodejs.py --- snapcraft-2.40/tests/unit/plugins/test_nodejs.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/plugins/test_nodejs.py 2018-04-14 12:13:35.000000000 +0000 @@ -41,6 +41,7 @@ node_packages = [] node_engine = nodejs._NODEJS_VERSION npm_run = [] + npm_flags = [] node_package_manager = 'npm' source = '.' self.options = Options() @@ -171,6 +172,61 @@ self.run_mock.assert_has_calls(expected_run_calls) self.tar_mock.assert_has_calls(expected_tar_calls) + def test_build_with_npm_flags(self): + self.options.node_package_manager = self.package_manager + self.options.npm_flags = ['--test-flag'] + + open('package.json', 'w').close() + + plugin = nodejs.NodePlugin('test-part', self.options, + self.project_options) + + os.makedirs(plugin.builddir) + open(os.path.join(plugin.builddir, 'package.json'), 'w').close() + + plugin.build() + + if self.package_manager == 'npm': + cmd = ['npm', '--test-flag', '--cache-min=Infinity', 'install'] + expected_run_calls = [ + mock.call(cmd, cwd=plugin.builddir), + mock.call(cmd + ['--global'], cwd=plugin.builddir), + ] + expected_tar_calls = [ + mock.call(self.nodejs_url, plugin._npm_dir), + mock.call().provision( + plugin.installdir, clean_target=False, keep_tarball=True), + ] + else: + cmd = [ + os.path.join(plugin.partdir, 'npm', 'bin', 'yarn'), + '--test-flag'] + if self.http_proxy is not None: + cmd.extend(['--proxy', self.http_proxy]) + if self.https_proxy is not None: + cmd.extend(['--https-proxy', self.https_proxy]) + expected_run_calls = [ + mock.call(cmd + + ['global', 'add', + 'file:{}'.format(plugin.builddir), + '--offline', '--prod', + '--global-folder', plugin.installdir, + '--prefix', plugin.installdir], + cwd=plugin.builddir) + ] + expected_tar_calls = [ + mock.call(self.nodejs_url, plugin._npm_dir), + mock.call('https://yarnpkg.com/latest.tar.gz', + plugin._npm_dir), + mock.call().provision(plugin.installdir, clean_target=False, + keep_tarball=True), + mock.call().provision(plugin._npm_dir, + clean_target=False, keep_tarball=True), + ] + + self.run_mock.assert_has_calls(expected_run_calls) + self.tar_mock.assert_has_calls(expected_tar_calls) + def test_pull_and_build_node_packages_sources(self): self.options.node_packages = ['my-pkg'] self.options.node_package_manager = self.package_manager @@ -325,7 +381,7 @@ Equals('architecture not supported (fantasy-arch)')) def test_get_build_properties(self): - expected_build_properties = ['node-packages', 'npm-run'] + expected_build_properties = ['node-packages', 'npm-run', 'npm-flags'] resulting_build_properties = nodejs.NodePlugin.get_build_properties() self.assertThat(resulting_build_properties, diff -Nru snapcraft-2.40/tests/unit/plugins/test_python.py snapcraft-2.41/tests/unit/plugins/test_python.py --- snapcraft-2.40/tests/unit/plugins/test_python.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/plugins/test_python.py 2018-04-14 12:13:35.000000000 +0000 @@ -28,7 +28,7 @@ ) -def setup_directories(plugin, python_version): +def setup_directories(plugin, python_version, create_setup_py=True): version = '2.7' if python_version == 'python2' else '3.5' os.makedirs(plugin.sourcedir) os.makedirs(plugin.builddir) @@ -39,7 +39,8 @@ os.makedirs(os.path.join(python_lib_path, 'dist-packages')) os.makedirs(python_include_path) - open(os.path.join(plugin.sourcedir, 'setup.py'), 'w').close() + if create_setup_py: + open(os.path.join(plugin.sourcedir, 'setup.py'), 'w').close() site_path = os.path.join(plugin.installdir, 'lib', 'python' + version, 'site-packages') @@ -79,6 +80,10 @@ self.mock_setup_tools = patcher.start() self.addCleanup(patcher.stop) + patcher = mock.patch('snapcraft.internal.os_release.OsRelease') + self.mock_os_release = patcher.start() + self.addCleanup(patcher.stop) + class PythonPluginTestCase(BasePythonPluginTestCase): @@ -363,6 +368,78 @@ plugin.get_manifest()['constraints-contents'], Equals('testpackage1==1.0\ntestpackage2==1.2')) + def test_plugin_stage_packages_python2_xenial(self): + self.options.python_version = 'python2' + self.mock_os_release.return_value.version_codename.return_value = ( + 'xenial') + + plugin = python.PythonPlugin('test-part', self.options, + self.project_options) + self.assertThat( + plugin.plugin_stage_packages, + Equals(['python'])) + + def test_plugin_stage_packages_python3_xenial(self): + self.options.python_version = 'python3' + self.mock_os_release.return_value.version_codename.return_value = ( + 'xenial') + + plugin = python.PythonPlugin('test-part', self.options, + self.project_options) + self.assertThat( + plugin.plugin_stage_packages, + Equals(['python3'])) + + def test_plugin_stage_packages_python2_bionic(self): + self.options.python_version = 'python2' + self.mock_os_release.return_value.version_codename.return_value = ( + 'bionic') + + plugin = python.PythonPlugin('test-part', self.options, + self.project_options) + self.assertThat( + plugin.plugin_stage_packages, + Equals(['python', 'python-distutils'])) + + def test_plugin_stage_packages_python3_bionic(self): + self.options.python_version = 'python3' + self.mock_os_release.return_value.version_codename.return_value = ( + 'bionic') + + plugin = python.PythonPlugin('test-part', self.options, + self.project_options) + self.assertThat( + plugin.plugin_stage_packages, + Equals(['python3', 'python3-distutils'])) + + def test_no_python_packages_does_nothing(self): + # This should be an error but given that we default to + # 'source: .' and now that pip 10 has been released + # we run into the need of fixing this situation. + self.mock_pip.return_value.list.return_value = dict() + + self.useFixture(fixture_setup.CleanEnvironment()) + plugin = python.PythonPlugin('test-part', self.options, + self.project_options) + setup_directories(plugin, self.options.python_version, + create_setup_py=False) + + pip_wheel = self.mock_pip.return_value.wheel + pip_wheel.return_value = [] + + plugin.build() + + # Pip should not attempt to download again in build (only pull) + pip_download = self.mock_pip.return_value.download + pip_download.assert_not_called() + + pip_wheel.assert_called_once_with( + [], constraints=None, process_dependency_links=False, + requirements=None, setup_py_dir=None) + + pip_install = self.mock_pip.return_value.install + pip_install.assert_not_called() + class FileMissingPythonPluginTest(BasePythonPluginTestCase): diff -Nru snapcraft-2.40/tests/unit/project_loader/test_config.py snapcraft-2.41/tests/unit/project_loader/test_config.py --- snapcraft-2.40/tests/unit/project_loader/test_config.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/project_loader/test_config.py 2018-04-14 12:13:35.000000000 +0000 @@ -25,8 +25,10 @@ import fixtures from testtools.matchers import Contains, Equals, MatchesRegex, Not, StartsWith +from testscenarios.scenarios import multiply_scenarios import snapcraft +from snapcraft.project._project_info import ProjectInfo from snapcraft.internal import ( dirs, project_loader, @@ -59,6 +61,150 @@ self.deb_arch = snapcraft.ProjectOptions().deb_arch +class ProjectInfoTestCase(YamlBaseTestCase): + + def setUp(self): + super().setUp() + + fake_logger = fixtures.FakeLogger(level=logging.ERROR) + self.useFixture(fake_logger) + + def test_properties(self): + info = ProjectInfo({ + 'name': 'foo', 'version': '1', + 'summary': 'bar', 'description': 'baz', + 'confinement': 'strict' + }) + self.assertThat(info.name, Equals('foo')) + self.assertThat(info.version, Equals('1')) + self.assertThat(info.summary, Equals('bar')) + self.assertThat(info.description, Equals('baz')) + self.assertThat(info.confinement, Equals('strict')) + + +class ProjectTestCase(YamlBaseTestCase): + + def setUp(self): + super().setUp() + + fake_logger = fixtures.FakeLogger(level=logging.ERROR) + self.useFixture(fake_logger) + + def test_project_with_arguments(self): + project = snapcraft.project.Project( + use_geoip=True, parallel_builds=False, + target_deb_arch='armhf', debug=True) + self.assertThat(project.use_geoip, Equals(True)) + self.assertThat(project.parallel_builds, Equals(False)) + self.assertThat(project.deb_arch, Equals('armhf')) + self.assertThat(project.debug, Equals(True)) + + def test_project_from_config(self): + self.make_snapcraft_yaml("""name: foo +version: "1" +summary: bar +description: baz +confinement: strict + +parts: + part1: + plugin: go +""") + + c = _config.Config() + project = c._project_options + self.assertThat(c.data['name'], Equals(project.info.name)) + self.assertThat(c.data['version'], Equals(project.info.version)) + self.assertThat(c.data['summary'], Equals(project.info.summary)) + self.assertThat(c.data['description'], + Equals(project.info.description)) + self.assertThat(c.data['confinement'], + Equals(project.info.confinement)) + + # API of both Project and ProjectOptions must be available + self.assertTrue(isinstance(project, + snapcraft.project.Project)) + self.assertTrue(isinstance(project, snapcraft.ProjectOptions)) + + def test_project_from_config_without_summary(self): + self.make_snapcraft_yaml("""name: foo +version: "1" +description: baz +adopt-info: part1 +confinement: strict + +parts: + part1: + plugin: go +""") + + c = _config.Config() + project = c._project_options + self.assertThat(project.info.summary, Equals(None)) + + def test_project_from_config_without_description(self): + self.make_snapcraft_yaml("""name: foo +version: "1" +summary: bar +adopt-info: part1 +confinement: strict + +parts: + part1: + plugin: go +""") + + c = _config.Config() + project = c._project_options + self.assertThat(project.info.description, Equals(None)) + + def test_project_from_config_without_version(self): + self.make_snapcraft_yaml("""name: foo +summary: bar +description: baz +adopt-info: part1 +confinement: strict + +parts: + part1: + plugin: go +""") + + c = _config.Config() + project = c._project_options + self.assertThat(project.info.version, Equals(None)) + + def test_project_passed_to_config(self): + self.make_snapcraft_yaml("""name: foo +version: "1" +summary: bar +description: baz +confinement: strict + +parts: + part1: + plugin: go +""") + + project = snapcraft.project.Project() + c = _config.Config(project) + self.assertThat(c._project_options, Equals(project)) + + def test_no_info_set(self): + project = snapcraft.project.Project() + self.assertThat(project.info, Equals(None)) + + def test_set_info(self): + project = snapcraft.project.Project() + info = ProjectInfo({ + 'name': 'foo', 'version': '1', + 'summary': 'bar', 'description': 'baz', + 'confinement': 'strict' + }) + project.info = info + self.assertThat(project.info, Equals(info)) + + class YamlTestCase(YamlBaseTestCase): def setUp(self): @@ -575,55 +721,6 @@ " only use ASCII lowercase letters, numbers, and hyphens," " and must have at least one letter.")) - def test_yaml_missing_confinement_must_log(self): - fake_logger = fixtures.FakeLogger(level=logging.WARNING) - self.useFixture(fake_logger) - - self.make_snapcraft_yaml("""name: test -version: "1" -summary: test -description: nothing - -parts: - part1: - plugin: go - stage-packages: [fswebcam] -""") - c = _config.Config() - - # Verify the default is "strict" - self.assertTrue('confinement' in c.data, - 'Expected "confinement" property to be in snap.yaml') - self.assertThat(c.data['confinement'], Equals('strict')) - self.assertTrue( - '"confinement" property not specified: defaulting to "strict"' - in fake_logger.output, 'Missing confinement hint in output') - - def test_yaml_missing_grade_must_log(self): - fake_logger = fixtures.FakeLogger(level=logging.WARNING) - self.useFixture(fake_logger) - - self.make_snapcraft_yaml("""name: test -version: "1" -summary: test -description: nothing -confinement: strict - -parts: - part1: - plugin: go - stage-packages: [fswebcam] -""") - c = _config.Config() - - # Verify the default is "stable" - self.assertTrue('grade' in c.data, - 'Expected "grade" property to be in snap.yaml') - self.assertThat(c.data['grade'], Equals('stable')) - self.assertTrue( - '"grade" property not specified: defaulting to "stable"' - in fake_logger.output, 'Missing grade hint in output') - def test_tab_in_yaml(self): fake_logger = fixtures.FakeLogger(level=logging.ERROR) self.useFixture(fake_logger) @@ -2166,6 +2263,60 @@ message=self.data) +class OldConflictsWithNewScriptletTestCase(ValidationTestCase): + + old_scriptlet_scenarios = [ + ('prepare', { + 'old_keyword': 'prepare', + 'old_value': ['test-prepare'], + }), + ('build', { + 'old_keyword': 'build', + 'old_value': ['test-build'], + }), + ('install', { + 'old_keyword': 'install', + 'old_value': ['test-install'], + }), + ] + + new_scriptlet_scenarios = [ + ('override-pull', { + 'new_keyword': 'override-pull', + 'new_value': ['test-override-pull'], + }), + ('override-build', { + 'new_keyword': 'override-build', + 'new_value': ['test-override-build'], + }), + ('override-stage', { + 'new_keyword': 'override-stage', + 'new_value': ['test-override-stage'], + }), + ('override-prime', { + 'new_keyword': 'override-prime', + 'new_value': ['test-override-prime'], + }), + ] + + scenarios = multiply_scenarios( + old_scriptlet_scenarios, new_scriptlet_scenarios) + + def test_both_old_and_new_keywords_specified(self): + self.data['parts']['part1'][self.old_keyword] = self.old_value + self.data['parts']['part1'][self.new_keyword] = self.new_value + + raised = self.assertRaises( + errors.YamlValidationError, + project_loader.Validator(self.data).validate) + + self.assertThat(str(raised), MatchesRegex( + (".*The 'parts/part1' property does not match the required " + "schema: Parts cannot contain both {0!r} and 'override-\*' " + "keywords. Use 'override-build' instead of {0!r}.*").format( + self.old_keyword))) + + class DaemonDependencyTestCase(ValidationBaseTestCase): scenarios = [ @@ -2198,7 +2349,7 @@ class RequiredPropertiesTestCase(ValidationBaseTestCase): scenarios = [(key, dict(key=key)) for - key in ['name', 'version', 'parts']] + key in ['name', 'parts']] def test_required_properties(self): data = self.data.copy() diff -Nru snapcraft-2.40/tests/unit/repo/test_deb.py snapcraft-2.41/tests/unit/repo/test_deb.py --- snapcraft-2.40/tests/unit/repo/test_deb.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/repo/test_deb.py 2018-04-14 12:13:35.000000000 +0000 @@ -336,3 +336,14 @@ errors.PackageBrokenError, repo.Ubuntu.install_build_packages, ['package-not-installable']) + + @patch('subprocess.check_call') + def test_broken_package_apt_install(self, mock_check_call): + mock_check_call.side_effect = CalledProcessError(100, 'apt-get') + self.fake_apt_cache.add_packages(('package-not-installable',)) + raised = self.assertRaises( + errors.BuildPackagesNotInstalledError, + repo.Ubuntu.install_build_packages, + ['package-not-installable']) + self.assertThat(raised.packages, + Equals('package-not-installable')) diff -Nru snapcraft-2.40/tests/unit/sources/test_local.py snapcraft-2.41/tests/unit/sources/test_local.py --- snapcraft-2.40/tests/unit/sources/test_local.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/sources/test_local.py 2018-04-14 12:13:35.000000000 +0000 @@ -60,22 +60,36 @@ self.assertGreater( os.stat(os.path.join('destination', 'dir', 'file')).st_nlink, 1) - def test_pull_with_existing_source_link_creates_symlink(self): + def test_pull_with_existing_source_tree_creates_hardlinks(self): os.makedirs(os.path.join('src', 'dir')) open(os.path.join('src', 'dir', 'file'), 'w').close() - # Note that this is a symlink now instead of a directory - os.symlink('dummy', 'destination') + os.mkdir('destination') + open(os.path.join('destination', 'existing-file'), 'w').close() local = sources.Local('src', 'destination') local.pull() + # Verify that the directories are not symlinks, but the file is a + # hardlink. Also verify that existing-file still exists. self.assertFalse(os.path.islink('destination')) self.assertFalse(os.path.islink(os.path.join('destination', 'dir'))) + self.assertThat( + os.path.join('destination', 'existing-file'), FileExists()) self.assertGreater( os.stat(os.path.join('destination', 'dir', 'file')).st_nlink, 1) - def test_pull_with_existing_source_file_wipes_and_creates_hardlinks(self): + def test_pull_with_existing_source_link_error(self): + os.makedirs(os.path.join('src', 'dir')) + open(os.path.join('src', 'dir', 'file'), 'w').close() + + # Note that this is a symlink now instead of a directory + os.symlink('dummy', 'destination') + + local = sources.Local('src', 'destination') + self.assertRaises(NotADirectoryError, local.pull) + + def test_pull_with_existing_source_file_error(self): os.makedirs(os.path.join('src', 'dir')) open(os.path.join('src', 'dir', 'file'), 'w').close() @@ -83,13 +97,7 @@ open('destination', 'w').close() local = sources.Local('src', 'destination') - local.pull() - - self.assertFalse(os.path.isfile('destination')) - self.assertFalse(os.path.islink('destination')) - self.assertFalse(os.path.islink(os.path.join('destination', 'dir'))) - self.assertGreater( - os.stat(os.path.join('destination', 'dir', 'file')).st_nlink, 1) + self.assertRaises(NotADirectoryError, local.pull) def test_pulling_twice_with_existing_source_dir_recreates_hardlinks(self): os.makedirs(os.path.join('src', 'dir')) diff -Nru snapcraft-2.40/tests/unit/sources/test_sources.py snapcraft-2.41/tests/unit/sources/test_sources.py --- snapcraft-2.40/tests/unit/sources/test_sources.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/sources/test_sources.py 2018-04-14 12:13:35.000000000 +0000 @@ -148,6 +148,7 @@ class GetSourceTestClass(unit.TestCase): def test_get(self): + open('file', 'w').close() class Options: source = '.' @@ -155,3 +156,4 @@ sources.get('src', 'useless-arg', Options()) self.assertTrue(os.path.isdir('src')) + self.assertTrue(os.path.isfile(os.path.join('src', 'file'))) diff -Nru snapcraft-2.40/tests/unit/states/test_build.py snapcraft-2.41/tests/unit/states/test_build.py --- snapcraft-2.40/tests/unit/states/test_build.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/states/test_build.py 2018-04-14 12:13:35.000000000 +0000 @@ -58,13 +58,14 @@ 'build-packages': 'test-build-packages', 'disable-parallel': 'test-disable-parallel', 'organize': {'baz': 'qux'}, + 'override-build': 'touch override-build', 'prepare': 'touch prepare', 'build': 'touch build', 'install': 'touch install', }) properties = self.state.properties_of_interest(self.part_properties) - self.assertThat(len(properties), Equals(9)) + self.assertThat(len(properties), Equals(10)) self.assertThat(properties['foo'], Equals('bar')) self.assertThat(properties['after'], Equals('test-after')) self.assertThat( @@ -74,6 +75,8 @@ self.assertThat( properties['disable-parallel'], Equals('test-disable-parallel')) self.assertThat(properties['organize'], Equals({'baz': 'qux'})) + self.assertThat( + properties['override-build'], Equals('touch override-build')) self.assertThat(properties['prepare'], Equals('touch prepare')) self.assertThat(properties['build'], Equals('touch build')) self.assertThat(properties['install'], Equals('touch install')) diff -Nru snapcraft-2.40/tests/unit/states/test_prime.py snapcraft-2.41/tests/unit/states/test_prime.py --- snapcraft-2.40/tests/unit/states/test_prime.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/states/test_prime.py 2018-04-14 12:13:35.000000000 +0000 @@ -33,7 +33,10 @@ self.files = {'foo'} self.directories = {'bar'} self.dependency_paths = {'baz'} - self.part_properties = {'prime': ['qux']} + self.part_properties = { + 'override-prime': 'touch override-prime', + 'prime': ['qux'], + } self.state = snapcraft.internal.states.PrimeState( self.files, self.directories, self.dependency_paths, @@ -55,7 +58,9 @@ def test_properties_of_interest(self): properties = self.state.properties_of_interest(self.part_properties) - self.assertThat(len(properties), Equals(1)) + self.assertThat(len(properties), Equals(2)) + self.assertThat( + properties['override-prime'], Equals('touch override-prime')) self.assertThat(properties['prime'], Equals(['qux'])) def test_project_options_of_interest(self): diff -Nru snapcraft-2.40/tests/unit/states/test_pull.py snapcraft-2.41/tests/unit/states/test_pull.py --- snapcraft-2.40/tests/unit/states/test_pull.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/states/test_pull.py 2018-04-14 12:13:35.000000000 +0000 @@ -52,6 +52,7 @@ def test_properties_of_interest(self): self.part_properties.update({ + 'override-pull': 'touch override-pull', 'plugin': 'test-plugin', 'parse-info': 'test-parse-info', 'stage-packages': ['test-stage-package'], @@ -65,8 +66,10 @@ }) properties = self.state.properties_of_interest(self.part_properties) - self.assertThat(len(properties), Equals(11)) + self.assertThat(len(properties), Equals(12)) self.assertThat(properties['foo'], Equals('bar')) + self.assertThat( + properties['override-pull'], Equals('touch override-pull')) self.assertThat(properties['plugin'], Equals('test-plugin')) self.assertThat(properties['parse-info'], Equals('test-parse-info')) self.assertThat( diff -Nru snapcraft-2.40/tests/unit/states/test_stage.py snapcraft-2.41/tests/unit/states/test_stage.py --- snapcraft-2.40/tests/unit/states/test_stage.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/states/test_stage.py 2018-04-14 12:13:35.000000000 +0000 @@ -32,7 +32,11 @@ self.project = Project() self.files = {'foo'} self.directories = {'bar'} - self.part_properties = {'stage': ['baz'], 'filesets': {'qux': 'quux'}} + self.part_properties = { + 'filesets': {'qux': 'quux'}, + 'override-stage': 'touch override-stage', + 'stage': ['baz'], + } self.state = snapcraft.internal.states.StageState( self.files, self.directories, self.part_properties, self.project) @@ -52,9 +56,11 @@ def test_properties_of_interest(self): properties = self.state.properties_of_interest(self.part_properties) - self.assertThat(len(properties), Equals(2)) - self.assertThat(properties['stage'], Equals(['baz'])) + self.assertThat(len(properties), Equals(3)) self.assertThat(properties['filesets'], Equals({'qux': 'quux'})) + self.assertThat( + properties['override-stage'], Equals('touch override-stage')) + self.assertThat(properties['stage'], Equals(['baz'])) def test_project_options_of_interest(self): self.assertFalse(self.state.project_options_of_interest(self.project)) diff -Nru snapcraft-2.40/tests/unit/store/test_store_client.py snapcraft-2.41/tests/unit/store/test_store_client.py --- snapcraft-2.40/tests/unit/store/test_store_client.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/store/test_store_client.py 2018-04-14 12:13:35.000000000 +0000 @@ -866,6 +866,35 @@ Equals('The store was unable to accept this snap.\n' ' - Duplicate snap already uploaded')) + def test_braces_in_error_messages_are_literals(self): + self.client.login('dummy', 'test correct password') + self.client.register('test-scan-error-with-braces') + tracker = self.client.upload( + 'test-scan-error-with-braces', self.snap_path) + self.assertTrue(isinstance(tracker, storeapi._status_tracker. + StatusTracker)) + result = tracker.track() + expected_result = { + 'code': 'processing_error', + 'revision': '1', + 'url': '/dev/click-apps/5349/rev/1', + 'can_release': False, + 'processed': True, + 'errors': [ + {'message': 'Error message with {braces}'}, + ] + } + self.assertThat(result, Equals(expected_result)) + + raised = self.assertRaises( + errors.StoreReviewError, + tracker.raise_for_code) + + self.assertThat( + str(raised), + Equals('The store was unable to accept this snap.\n' + ' - Error message with {braces}')) + def test_push_unregistered_snap(self): self.client.login('dummy', 'test correct password') raised = self.assertRaises( @@ -1018,13 +1047,20 @@ self.assertThat( str(raised), Equals('Could not close channel: 200 OK')) - self.assertThat( - self.fake_logger.output.splitlines()[-3:], - Equals([ - 'Invalid response from the server on channel closing:', - '200 OK', - 'b\'plain data\'', - ])) + + expected_lines = [ + 'Invalid response from the server on channel closing:', + '200 OK', + 'b\'plain data\'', + ] + + actual_lines = [] + for line in self.fake_logger.output.splitlines(): + line = line.strip() + if line in expected_lines: + actual_lines.append(line) + + self.assertThat(actual_lines, Equals(expected_lines)) def test_close_broken_store_json(self): self.client.login('dummy', 'test correct password') @@ -1034,13 +1070,20 @@ self.assertThat( str(raised), Equals('Could not close channel: 200 OK')) - self.assertThat( - self.fake_logger.output.splitlines()[-3:], - Equals([ - 'Invalid response from the server on channel closing:', - '200 OK', - 'b\'{"closed_channels": ["broken-json"]}\'', - ])) + + expected_lines = [ + 'Invalid response from the server on channel closing:', + '200 OK', + 'b\'{"closed_channels": ["broken-json"]}\'', + ] + + actual_lines = [] + for line in self.fake_logger.output.splitlines(): + line = line.strip() + if line in expected_lines: + actual_lines.append(line) + + self.assertThat(actual_lines, Equals(expected_lines)) def test_close_successfully(self): # Successfully closing a channels returns 'closed_channels' @@ -1457,6 +1500,21 @@ result = self.client.push_metadata('basic', metadata, True) self.assertIsNone(result) + def test_braces_in_error_messages_are_literals(self): + self._setup_snap() + metadata = {'test-conflict-with-braces': 'value'} + raised = self.assertRaises( + errors.StoreMetadataError, + self.client.push_metadata, 'basic', metadata, False) + should = """ + Metadata not pushed! + Conflict in 'test-conflict-with-braces' field: + In snapcraft.yaml: 'value' + In the Store: 'value with {braces}' + You can repeat the push-metadata command with --force to force the local values into the Store + """ # NOQA + self.assertThat(str(raised), Equals(dedent(should).strip())) + class PushBinaryMetadataTestCase(StoreTestCase): @@ -1532,6 +1590,23 @@ result = self.client.push_binary_metadata('basic', metadata, True) self.assertIsNone(result) + def test_braces_in_error_messages_are_literals(self): + self._setup_snap() + with tempfile.NamedTemporaryFile(suffix='conflict-with-braces') as f: + filename = os.path.basename(f.name) + metadata = {'icon': f} + raised = self.assertRaises( + errors.StoreMetadataError, + self.client.push_binary_metadata, 'basic', metadata, False) + should = """ + Metadata not pushed! + Conflict in 'icon' field: + In snapcraft.yaml: '{}' + In the Store: 'original icon with {{braces}}' + You can repeat the push-metadata command with --force to force the local values into the Store + """.format(filename) # NOQA + self.assertThat(str(raised), Equals(dedent(should).strip())) + class SnapNotFoundTestCase(StoreTestCase): diff -Nru snapcraft-2.40/tests/unit/test_config.py snapcraft-2.41/tests/unit/test_config.py --- snapcraft-2.40/tests/unit/test_config.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/test_config.py 2018-04-14 12:13:35.000000000 +0000 @@ -17,9 +17,10 @@ import fixtures import os -from testtools.matchers import Equals +from testtools.matchers import Contains, Equals, FileContains from snapcraft import config +from snapcraft.storeapi import errors from tests import unit @@ -77,6 +78,41 @@ self.assertThat(new_conf.parser.get('keep_me', 'foo'), Equals('bar')) self.assertTrue(conf.is_empty()) + def test_save_encoded(self): + conf = config.Config() + conf.set('bar', 'baz') + conf.save(encode=True) + new_conf = config.Config() + self.assertThat(new_conf.get('bar'), Equals('baz')) + + def test_save_encoded_to_file(self): + conf = config.Config() + conf.set('bar', 'baz') + with open('test-config', 'w') as f: + conf.save(config_fd=f, encode=True) + f.flush() + + self.assertThat('test-config', FileContains( + 'W2xvZ2luLnVidW50dS5jb21dCmJhciA9IGJhegoK')) + + new_conf = config.Config() + with open('test-config', 'r') as f: + new_conf.load(config_fd=f) + self.assertThat(new_conf.get('bar'), Equals('baz')) + + def test_load_invalid_config(self): + with open('test-config', 'w') as f: + f.write('invalid config') + f.flush() + + conf = config.Config() + with open('test-config', 'r') as f: + raised = self.assertRaises( + errors.InvalidLoginConfig, conf.load, config_fd=f) + + self.assertThat(str(raised), Contains( + 'File contains no section headers')) + class TestOptions(unit.TestCase): diff -Nru snapcraft-2.40/tests/unit/test_elf.py snapcraft-2.41/tests/unit/test_elf.py --- snapcraft-2.40/tests/unit/test_elf.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/test_elf.py 2018-04-14 12:13:35.000000000 +0000 @@ -16,6 +16,7 @@ import fixtures import logging import os +import subprocess import tempfile from textwrap import dedent import sys @@ -420,20 +421,33 @@ root_path='/fake') elf_patcher.patch(elf_file=elf_file) - def test_patchelf_from_snap_used_if_using_snap(self): - self.useFixture(fixtures.EnvironmentVariable( - 'SNAP', '/snap/snapcraft/current')) - self.useFixture(fixtures.EnvironmentVariable( - 'SNAP_NAME', 'snapcraft')) - # The base_path does not matter here as there are not files to - # be crawled for. - elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', - root_path='/fake') + def test_tools_from_snap_used_if_using_snap(self): + self.useFixture(fixture_setup.FakeSnapcraftIsASnap()) + + real_exists = os.path.exists + + def _fake_exists(path): + if path == '/snap/snapcraft/current/bin/patchelf': + return True + elif path == '/snap/snapcraft/current/usr/bin/strip': + return True + else: + return real_exists(path) + + with mock.patch('os.path.exists', side_effect=_fake_exists): + # The base_path does not matter here as there are not files to + # be crawled for. + elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', + root_path='/fake') expected_patchelf = os.path.join('/snap', 'snapcraft', 'current', 'bin', 'patchelf') self.assertThat(elf_patcher._patchelf_cmd, Equals(expected_patchelf)) + expected_strip = os.path.join('/snap', 'snapcraft', 'current', + 'usr', 'bin', 'strip') + self.assertThat(elf_patcher._strip_cmd, Equals(expected_strip)) + class TestPatcherErrors(TestElfBase): @@ -459,9 +473,95 @@ elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', root_path='/fake') - self.assertRaises(errors.PatcherNewerPatchelfError, - elf_patcher.patch, - elf_file=elf_file) + with mock.patch('subprocess.check_call', + wraps=subprocess.check_call) as mock_check_call: + self.assertRaises(errors.PatcherNewerPatchelfError, + elf_patcher.patch, + elf_file=elf_file) + + # Test that .note.go.buildid is stripped off + mock_check_call.assert_has_calls([ + mock.call([ + 'patchelf', '--set-interpreter', '/lib/fake-ld', + mock.ANY]), + mock.call([ + 'strip', '--remove-section', '.note.go.buildid', + mock.ANY]), + mock.call([ + 'patchelf', '--set-interpreter', '/lib/fake-ld', + mock.ANY]), + ]) + + def test_patch_uses_snapped_strip(self): + self.useFixture(fixture_setup.FakeSnapcraftIsASnap()) + self.fake_elf = fixture_setup.FakeElf(root_path=self.path, + patchelf_version='0.8') + self.useFixture(self.fake_elf) + + elf_file = self.fake_elf['fake_elf-bad-patchelf'] + + real_check_call = subprocess.check_call + real_check_output = subprocess.check_output + real_exists = os.path.exists + + def _fake_check_call(*args, **kwargs): + if 'patchelf' in args[0][0]: + self.assertThat( + args[0][0], Equals('/snap/snapcraft/current/bin/patchelf')) + args[0][0] = 'patchelf' + elif 'strip' in args[0][0]: + self.assertThat( + args[0][0], Equals( + '/snap/snapcraft/current/usr/bin/strip')) + args[0][0] = 'strip' + real_check_call(*args, **kwargs) + + def _fake_check_output(*args, **kwargs): + if 'patchelf' in args[0][0]: + self.assertThat( + args[0][0], Equals('/snap/snapcraft/current/bin/patchelf')) + args[0][0] = 'patchelf' + elif 'strip' in args[0][0]: + self.assertThat( + args[0][0], Equals( + '/snap/snapcraft/current/usr/bin/strip')) + args[0][0] = 'strip' + return real_check_output(*args, **kwargs) + + def _fake_exists(path): + if path == '/snap/snapcraft/current/bin/patchelf': + return True + elif path == '/snap/snapcraft/current/usr/bin/strip': + return True + else: + return real_exists(path) + + with mock.patch('subprocess.check_call') as mock_check_call: + with mock.patch('subprocess.check_output') as mock_check_output: + with mock.patch('os.path.exists', side_effect=_fake_exists): + mock_check_call.side_effect = _fake_check_call + mock_check_output.side_effect = _fake_check_output + + # The base_path does not matter here as there are not files + # for which to crawl. + elf_patcher = elf.Patcher(dynamic_linker='/lib/fake-ld', + root_path='/fake') + self.assertRaises(errors.PatcherNewerPatchelfError, + elf_patcher.patch, + elf_file=elf_file) + + # Test that .note.go.buildid is stripped off + mock_check_call.assert_has_calls([ + mock.call([ + 'patchelf', '--set-interpreter', '/lib/fake-ld', + mock.ANY]), + mock.call([ + 'strip', '--remove-section', '.note.go.buildid', + mock.ANY]), + mock.call([ + 'patchelf', '--set-interpreter', '/lib/fake-ld', + mock.ANY]), + ]) class TestSonameCache(unit.TestCase): diff -Nru snapcraft-2.40/tests/unit/test_errors.py snapcraft-2.41/tests/unit/test_errors.py --- snapcraft-2.40/tests/unit/test_errors.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/test_errors.py 2018-04-14 12:13:35.000000000 +0000 @@ -301,7 +301,9 @@ 'exception': errors.InvalidContainerImageInfoError, 'kwargs': {'image_info': 'test-image-info'}, 'expected_message': ( - 'Error parsing the container image info: test-image-info')}), + 'Failed to parse container image info: ' + 'SNAPCRAFT_IMAGE_INFO is not a valid JSON string: ' + 'test-image-info')}), # meta errors. ('AdoptedPartMissingError', { 'exception': meta_errors.AdoptedPartMissingError, @@ -325,6 +327,15 @@ "Missing required key(s) in snapcraft.yaml: " "'test-key1' and 'test-key2'. Either specify the missing " "key(s), or use 'adopt-info' to get them from a part.")}), + ('AmbiguousPassthroughKeyError', { + 'exception': meta_errors.AmbiguousPassthroughKeyError, + 'kwargs': {'keys': ['key1', 'key2']}, + 'expected_message': ( + "Failed to generate snap metadata: " + "The following keys are specified in their regular location " + "as well as in passthrough: 'key1' and 'key2'. " + "Remove duplicate keys."), + }), ('MissingMetadataFileError', { 'exception': errors.MissingMetadataFileError, 'kwargs': {'part_name': 'test-part', 'path': 'test/path'}, @@ -417,7 +428,7 @@ "Verify that the part is using the correct parameters and try " "again." ) - }) + }), ) def test_error_formatting(self): diff -Nru snapcraft-2.40/tests/unit/test_lifecycle.py snapcraft-2.41/tests/unit/test_lifecycle.py --- snapcraft-2.40/tests/unit/test_lifecycle.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/test_lifecycle.py 2018-04-14 12:13:35.000000000 +0000 @@ -683,6 +683,19 @@ os.path.join('prime', 'snap', '.snapcraft'), Not(DirExists())) + def test_non_prime_and_no_version(self): + snapcraft_yaml = fixture_setup.SnapcraftYaml( + self.path, version=None) + snapcraft_yaml.data['adopt-info'] = 'test-part' + snapcraft_yaml.update_part( + 'test-part', { + 'plugin': 'nil', + 'override-build': 'snapcraftctl set-version 1.0'}) + self.useFixture(snapcraft_yaml) + + # This should not fail + lifecycle.execute('pull', self.project_options) + class DirtyBuildScriptletTestCase(BaseLifecycleTestCase): diff -Nru snapcraft-2.40/tests/unit/test_lxd.py snapcraft-2.41/tests/unit/test_lxd.py --- snapcraft-2.40/tests/unit/test_lxd.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/test_lxd.py 2018-04-14 12:13:35.000000000 +0000 @@ -22,6 +22,7 @@ from unittest.mock import ( call, patch, + ANY, ) import fixtures @@ -29,10 +30,11 @@ from testtools.matchers import Contains, Equals from snapcraft import ProjectOptions +from snapcraft.project._project_options import _get_deb_arch from snapcraft.internal import lxd from snapcraft.internal.errors import ( ContainerConnectionError, - ContainerRunError, + InvalidContainerImageInfoError, SnapdError, SnapcraftEnvironmentError, ) @@ -118,10 +120,7 @@ '{}/root/build_project/project.tar'.format(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(['python3', '-c', ANY]), call(['apt-get', 'update']), call(['apt-get', 'install', 'squashfuse', '-y']), call(['mkdir', project_folder]), @@ -138,6 +137,10 @@ 'snap.snap']), call(['lxc', 'stop', '-f', container_name]), ]) + self.fake_lxd.check_output_mock.assert_has_calls([ + call(['lxc', 'image', 'list', '--format=json', + 'ubuntu:xenial/{}'.format(_get_deb_arch(self.server))]), + ]) def test_failed_container_never_created(self): def call_effect(*args, **kwargs): @@ -183,13 +186,36 @@ 'test_build_info_value']), ]) + def test_image_info_merged(self): + test_image_info = '{"build_url": "test-build-url"}' + self.useFixture( + fixtures.EnvironmentVariable( + 'SNAPCRAFT_IMAGE_INFO', test_image_info)) + self.make_containerbuild().execute() + self.fake_lxd.check_call_mock.assert_has_calls([ + call(['lxc', 'config', 'set', self.fake_lxd.name, + 'environment.SNAPCRAFT_IMAGE_INFO', + '{"fingerprint": "test-fingerprint", ' + '"architecture": "test-architecture", ' + '"created_at": "test-created-at", ' + '"build_url": "test-build-url"}']), + ]) + + def test_image_info_invalid(self): + test_image_info = 'not-json' + self.useFixture( + fixtures.EnvironmentVariable( + 'SNAPCRAFT_IMAGE_INFO', test_image_info)) + self.assertRaises(InvalidContainerImageInfoError, + self.make_containerbuild().execute) + 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, + self.assertRaises(ContainerConnectionError, builder._wait_for_network) def test_failed_build_with_debug(self): @@ -767,7 +793,7 @@ kwargs=dict(cmd='testcmd', returncode=1, output='test output'), expected_warn=( "Failed to get container image info: " - "`lxc image list --format=json ubuntu:xenial` " + "`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( diff -Nru snapcraft-2.40/tests/unit/test_meta.py snapcraft-2.41/tests/unit/test_meta.py --- snapcraft-2.40/tests/unit/test_meta.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/test_meta.py 2018-04-14 12:13:35.000000000 +0000 @@ -15,6 +15,7 @@ # along with this program. If not, see . import configparser +import contextlib import logging import os from unittest.mock import patch @@ -91,9 +92,12 @@ for part in self.config.parts.all_parts: part.pull() part.build() + part.stage() + part.prime() _snap_packaging.create_snap_packaging( - self.config.data, self.config.parts, self.project_options, 'dummy') + self.config.data, self.config.parts, self.project_options, 'dummy', + self.config.original_snapcraft_yaml, self.config.validator.schema) self.assertTrue( os.path.exists(self.snap_yaml), 'snap.yaml was not created') @@ -148,7 +152,8 @@ config = project_loader.load_config() _snap_packaging.create_snap_packaging( - self.config_data, config.parts, self.project_options, 'dummy') + self.config_data, config.parts, self.project_options, 'dummy', + config.original_snapcraft_yaml, config.validator.schema) expected_gadget = os.path.join(self.meta_dir, 'gadget.yaml') self.assertTrue(os.path.exists(expected_gadget)) @@ -170,7 +175,9 @@ self.config_data, config.parts, self.project_options, - 'dummy' + 'dummy', + config.original_snapcraft_yaml, + config.validator.schema ) def test_create_meta_with_declared_icon(self): @@ -261,11 +268,13 @@ config = project_loader.load_config() _snap_packaging.create_snap_packaging( - self.config_data, config.parts, self.project_options, 'dummy') + self.config_data, config.parts, self.project_options, 'dummy', + config.original_snapcraft_yaml, config.validator.schema) # Running again should be good _snap_packaging.create_snap_packaging( - self.config_data, config.parts, self.project_options, 'dummy') + self.config_data, config.parts, self.project_options, 'dummy', + config.original_snapcraft_yaml, config.validator.schema) def test_create_meta_with_icon_in_setup(self): gui_path = os.path.join('setup', 'gui') @@ -475,6 +484,131 @@ "Expected generated 'bar' hook to not contain 'plugs'") +class PassthroughBaseTestCase(CreateBaseTestCase): + + def setUp(self): + super().setUp() + + self.config_data = { + 'name': 'my-package', + 'version': '1.0', + 'grade': 'stable', + 'description': 'my description', + 'summary': 'my summary', + 'parts': { + 'test-part': { + 'plugin': 'nil', + } + } + } + + +class PassthroughErrorTestCase(PassthroughBaseTestCase): + + def test_ambiguous_key_fails(self): + self.config_data['confinement'] = 'devmode' + self.config_data['passthrough'] = {'confinement': 'next-generation'} + raised = self.assertRaises( + meta_errors.AmbiguousPassthroughKeyError, + self.generate_meta_yaml) + self.assertThat(raised.keys, Equals("'confinement'")) + + def test_app_ambiguous_key_fails(self): + self.config_data['apps'] = {'foo': { + 'command': 'echo', 'daemon': 'simple', + 'passthrough': {'daemon': 'complex'}}} + raised = self.assertRaises( + meta_errors.AmbiguousPassthroughKeyError, + self.generate_meta_yaml) + self.assertThat(raised.keys, Equals("'daemon'")) + + def test_hook_ambiguous_key_fails(self): + self.config_data['hooks'] = {'foo': { + 'plugs': ['network'], + 'passthrough': {'plugs': ['network']}}} + raised = self.assertRaises( + meta_errors.AmbiguousPassthroughKeyError, + self.generate_meta_yaml) + self.assertThat(raised.keys, Equals("'plugs'")) + + def test_warn_once_only(self): + fake_logger = fixtures.FakeLogger(level=logging.INFO) + self.useFixture(fake_logger) + + self.config_data['confinement'] = 'devmode' + self.config_data['passthrough'] = {'foo': 'bar', 'spam': 'eggs'} + self.config_data['apps'] = {'foo': { + 'command': 'echo', + 'passthrough': {'foo': 'bar', 'spam': 'eggs'}}} + self.config_data['hooks'] = {'foo': { + 'plugs': ['network'], + 'passthrough': {'foo': 'bar', 'spam': 'eggs'}}} + self.generate_meta_yaml() + self.assertThat( + fake_logger.output, + Equals("The 'passthrough' property is being used to propagate " + "experimental properties to snap.yaml that have not been " + "validated. The snap cannot be released to the store.\n")) + + +class PassthroughPropagateTestCase(PassthroughBaseTestCase): + + scenarios = [ + ('new', dict( + snippet={'passthrough': {'spam': 'eggs'}}, + section=None, key='spam', value='eggs')), + ('different type', dict( + # This is normally an array of strings + snippet={'passthrough': {'architectures': 'all'}}, + section=None, key='architectures', value='all')), + ('with default', dict( + snippet={'passthrough': {'confinement': 'next-generation'}}, + section=None, key='confinement', value='next-generation')), + ('app, new', dict( + snippet={'apps': {'foo': { + 'command': 'echo', + 'passthrough': {'spam': 'eggs'}}}}, + section='apps', name='foo', key='spam', value='eggs')), + ('app, different type', dict( + snippet={'apps': {'foo': { + 'command': 'echo', + # This is normally an array of strings + 'passthrough': {'aliases': 'foo'}}}}, + section='apps', name='foo', key='aliases', value='foo')), + # Note: There are currently no app properties with defaults + ('hook, new', dict( + snippet={'hooks': {'foo': { + 'plugs': ['network'], + 'passthrough': {'spam': 'eggs'}}}}, + section='hooks', name='foo', key='spam', value='eggs')), + ('hook, different type', dict( + snippet={'hooks': {'foo': { + # This is normally an array of strings + 'passthrough': {'plugs': 'network'}}}}, + section='hooks', name='foo', key='plugs', value='network')), + # Note: There are currently no hook properties with defaults + ] + + def test_propagate(self): + fake_logger = fixtures.FakeLogger(level=logging.INFO) + self.useFixture(fake_logger) + + self.config_data.update(self.snippet) + y = self.generate_meta_yaml() + if self.section: + y = y[self.section][self.name] + self.assertThat( + y, Contains(self.key), + 'Expected {!r} property to be propagated to snap.yaml' + .format(self.key)) + self.assertThat(y[self.key], Equals(self.value)) + self.assertThat( + fake_logger.output, + Contains("The 'passthrough' property is being used to propagate " + "experimental properties to snap.yaml that have not been " + "validated. The snap cannot be released to the store.\n")) + + class CreateMetadataFromSourceBaseTestCase(CreateBaseTestCase): def setUp(self): @@ -501,15 +635,14 @@ open('test-metadata-file', 'w').close() -class CreateMetadataFromSourceErrorsTestCase( - CreateMetadataFromSourceBaseTestCase): +class CreateMetadataFromSourceTestCase(CreateMetadataFromSourceBaseTestCase): def test_create_metadata_with_missing_parse_info(self): del self.config_data['summary'] del self.config_data['parts']['test-part']['parse-info'] raised = self.assertRaises( meta_errors.AdoptedPartNotParsingInfo, - self.generate_meta_yaml) + self.generate_meta_yaml, build=True) self.assertThat(raised.part, Equals('test-part')) def test_create_metadata_with_wrong_adopt_info(self): @@ -520,6 +653,9 @@ self.assertThat(raised.part, Equals('wrong-part')) def test_metadata_doesnt_overwrite_specified(self): + fake_logger = fixtures.FakeLogger(level=logging.WARNING) + self.useFixture(fake_logger) + def _fake_extractor(file_path): return extractors.ExtractedMetadata( summary='extracted summary', @@ -536,6 +672,13 @@ self.assertThat( y['description'], Equals(self.config_data['description'])) + # Verify that we warn that the YAML took precedence over the extracted + # metadata for summary and description + self.assertThat(fake_logger.output, Contains( + "The 'description' and 'summary' properties are specified in " + "adopted info as well as the YAML: taking the properties from the " + "YAML")) + def test_metadata_with_unexisting_icon(self): def _fake_extractor(file_path): return extractors.ExtractedMetadata( @@ -645,6 +788,140 @@ self.assertThat(expected_desktop, FileContains(desktop_content)) +class ScriptletsMetadataTestCase(CreateMetadataFromSourceBaseTestCase): + + scenarios = [ + ('set-version', { + 'keyword': 'version', + 'original': 'original-version', + 'value': 'test-version', + 'setter': 'set-version'}), + ('set-grade', { + 'keyword': 'grade', + 'original': 'stable', + 'value': 'devel', + 'setter': 'set-grade'}), + ] + + def test_scriptlets_satisfy_required_property(self): + with contextlib.suppress(KeyError): + del self.config_data[self.keyword] + + del self.config_data['parts']['test-part']['parse-info'] + self.config_data['parts']['test-part']['override-prime'] = ( + 'snapcraftctl {} {}'.format(self.setter, self.value)) + + generated = self.generate_meta_yaml(build=True) + + self.assertThat(generated[self.keyword], Equals(self.value)) + + def test_scriptlets_no_overwrite_existing_property(self): + self.config_data[self.keyword] = self.original + fake_logger = fixtures.FakeLogger(level=logging.WARNING) + self.useFixture(fake_logger) + + del self.config_data['parts']['test-part']['parse-info'] + self.config_data['parts']['test-part']['override-prime'] = ( + 'snapcraftctl {} {}'.format(self.setter, self.value)) + + generated = self.generate_meta_yaml(build=True) + + self.assertThat(generated[self.keyword], Equals(self.original)) + + # Since the specified version took precedence over the scriptlet-set + # version, verify that we warned + self.assertThat(fake_logger.output, Contains( + "The {!r} property is specified in adopted info as well as " + "the YAML: taking the property from the YAML".format( + self.keyword))) + + def test_scriptlets_overwrite_extracted_metadata(self): + with contextlib.suppress(KeyError): + del self.config_data[self.keyword] + + self.config_data['parts']['test-part']['override-build'] = ( + 'snapcraftctl build && snapcraftctl {} {}'.format( + self.setter, self.value)) + + def _fake_extractor(file_path): + return extractors.ExtractedMetadata( + **{self.keyword: 'extracted-value'}) + + self.useFixture(fixture_setup.FakeMetadataExtractor( + 'fake', _fake_extractor)) + + generated = self.generate_meta_yaml(build=True) + + self.assertThat(generated[self.keyword], Equals(self.value)) + + def test_scriptlets_overwrite_extracted_metadata_regardless_of_order(self): + with contextlib.suppress(KeyError): + del self.config_data[self.keyword] + + self.config_data['parts']['test-part']['override-pull'] = ( + 'snapcraftctl {} {} && snapcraftctl pull'.format( + self.setter, self.value)) + + def _fake_extractor(file_path): + return extractors.ExtractedMetadata( + **{self.keyword: 'extracted-value'}) + + self.useFixture(fixture_setup.FakeMetadataExtractor( + 'fake', _fake_extractor)) + + generated = self.generate_meta_yaml(build=True) + + self.assertThat(generated[self.keyword], Equals(self.value)) + + +class InvalidMetadataTestCase(CreateMetadataFromSourceBaseTestCase): + + scenarios = [ + ('version', { + 'keyword': 'version', + 'setter': 'set-version', + 'value': '.invalid-'}), + ('grade', { + 'keyword': 'grade', + 'setter': 'set-grade', + 'value': 'invalid'}), + ] + + def test_invalid_scriptlet_metadata(self): + with contextlib.suppress(KeyError): + del self.config_data[self.keyword] + + del self.config_data['parts']['test-part']['parse-info'] + + self.config_data['parts']['test-part']['override-prime'] = ( + 'snapcraftctl {} {}'.format(self.setter, self.value)) + + raised = self.assertRaises( + project_loader.errors.YamlValidationError, self.generate_meta_yaml, + build=True) + self.assertThat(str(raised), Contains( + 'Issues while validating properties: The {!r} property does not ' + 'match the required schema'.format(self.keyword))) + + def test_invalid_extracted_metadata(self): + with contextlib.suppress(KeyError): + del self.config_data[self.keyword] + + def _fake_extractor(file_path): + return extractors.ExtractedMetadata( + **{self.keyword: self.value}) + + self.useFixture(fixture_setup.FakeMetadataExtractor( + 'fake', _fake_extractor)) + + raised = self.assertRaises( + project_loader.errors.YamlValidationError, self.generate_meta_yaml, + build=True) + self.assertThat(str(raised), Contains( + 'Issues while validating properties: The {!r} property does not ' + 'match the required schema'.format(self.keyword))) + + class WriteSnapDirectoryTestCase(CreateBaseTestCase): def test_write_snap_directory(self): @@ -746,16 +1023,31 @@ class CreateWithConfinementTestCase(CreateBaseTestCase): scenarios = [(confinement, dict(confinement=confinement)) for - confinement in ['strict', 'devmode', 'classic']] + confinement in ['', 'strict', 'devmode', 'classic']] def test_create_meta_with_confinement(self): - self.config_data['confinement'] = self.confinement + fake_logger = fixtures.FakeLogger(level=logging.INFO) + self.useFixture(fake_logger) + + if self.confinement: + self.config_data['confinement'] = self.confinement + else: + del self.config_data['confinement'] y = self.generate_meta_yaml() self.assertTrue( 'confinement' in y, 'Expected "confinement" property to be in snap.yaml') - self.assertThat(y['confinement'], Equals(self.confinement)) + + if self.confinement: + self.assertThat(y['confinement'], Equals(self.confinement)) + else: + # Ensure confinement defaults to strict if not specified. Also + # verify that a warning is printed + self.assertThat(y['confinement'], Equals('strict')) + self.assertThat(fake_logger.output, Contains( + "'confinement' property not specified: defaulting to " + "'strict'")) class EnsureFilePathsTestCase(CreateBaseTestCase): @@ -811,16 +1103,28 @@ class CreateWithGradeTestCase(CreateBaseTestCase): scenarios = [(grade, dict(grade=grade)) for - grade in ['stable', 'devel']] + grade in ['', 'stable', 'devel']] def test_create_meta_with_grade(self): - self.config_data['grade'] = self.grade + fake_logger = fixtures.FakeLogger(level=logging.INFO) + self.useFixture(fake_logger) + + if self.grade: + self.config_data['grade'] = self.grade y = self.generate_meta_yaml() self.assertTrue( 'grade' in y, 'Expected "grade" property to be in snap.yaml') - self.assertThat(y['grade'], Equals(self.grade)) + + if self.grade: + self.assertThat(y['grade'], Equals(self.grade)) + else: + # Ensure that grade always defaults to stable, even if not + # specified. Also verify that a warning is printed + self.assertThat(y['grade'], Equals('stable')) + self.assertThat(fake_logger.output, Contains( + "'grade' property not specified: defaulting to 'stable'")) # TODO this needs more tests. @@ -833,7 +1137,8 @@ self.packager = _snap_packaging._SnapPackaging( {'confinement': 'devmode'}, ProjectOptions(), - 'dummy' + 'dummy', + {'confinement': 'devmode'} ) self.packager._is_host_compatible_with_base = True diff -Nru snapcraft-2.40/tests/unit/test_options.py snapcraft-2.41/tests/unit/test_options.py --- snapcraft-2.40/tests/unit/test_options.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/test_options.py 2018-04-14 12:13:35.000000000 +0000 @@ -21,6 +21,10 @@ from testtools.matchers import Equals import snapcraft +from snapcraft.project._project_options import ( + _get_platform_architecture, + _32BIT_USERSPACE_ARCHITECTURE, +) from snapcraft.internal import common from snapcraft.internal.errors import SnapcraftEnvironmentError from tests import unit @@ -142,9 +146,8 @@ self, mock_platform_machine, mock_platform_architecture): mock_platform_machine.return_value = self.machine mock_platform_architecture.return_value = self.architecture - platform_arch = snapcraft._options._get_platform_architecture() - userspace_conversions = \ - snapcraft._options._32BIT_USERSPACE_ARCHITECTURE + platform_arch = _get_platform_architecture() + userspace_conversions = _32BIT_USERSPACE_ARCHITECTURE if self.architecture[0] == '32bit' and \ self.machine in userspace_conversions: diff -Nru snapcraft-2.40/tests/unit/test_target_arch.py snapcraft-2.41/tests/unit/test_target_arch.py --- snapcraft-2.40/tests/unit/test_target_arch.py 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tests/unit/test_target_arch.py 2018-04-14 12:13:35.000000000 +0000 @@ -16,7 +16,7 @@ from testtools.matchers import Equals -import snapcraft +from snapcraft.project._project_options import _find_machine from tests import unit @@ -59,5 +59,5 @@ ] def test_find_machine(self): - machine = snapcraft._options._find_machine(self.machine) + machine = _find_machine(self.machine) self.assertThat(machine, Equals(self.expected_machine)) diff -Nru snapcraft-2.40/tools/travis/run_lxd_container.sh snapcraft-2.41/tools/travis/run_lxd_container.sh --- snapcraft-2.40/tools/travis/run_lxd_container.sh 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/tools/travis/run_lxd_container.sh 2018-04-14 12:13:35.000000000 +0000 @@ -28,11 +28,13 @@ script_path="$(dirname "$0")" project_path="$(readlink -f "$script_path/../..")" name="$1" +image="$LXD_IMAGE" +[ -z "$image" ] && image="ubuntu:xenial" lxc="/snap/bin/lxc" echo "Starting the LXD container." -$lxc launch --ephemeral --config security.nesting=true ubuntu:xenial "$name" +$lxc launch --ephemeral --config security.nesting=true "$image" "$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. diff -Nru snapcraft-2.40/.travis.yml snapcraft-2.41/.travis.yml --- snapcraft-2.40/.travis.yml 2018-03-19 12:40:43.000000000 +0000 +++ snapcraft-2.41/.travis.yml 2018-04-14 12:13:35.000000000 +0000 @@ -27,6 +27,12 @@ if: type != cron script: sudo ./tools/travis/run_tests.sh tests/integration/general - if: type != cron + script: LXD_IMAGE="ubuntu-daily:bionic" sudo ./tools/travis/run_tests.sh tests/integration/general + - if: type != cron + script: sudo ./tools/travis/run_tests.sh tests/integration/lifecycle + - if: type != cron + script: sudo ./tools/travis/run_tests.sh tests/integration/sources + - if: type != cron script: - "travis_wait 30 sleep 1800 &" - sudo ./tools/travis/run_tests.sh tests/integration/plugins