diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
index 7573a35dac..23e4c4c566 100644
--- a/.github/workflows/publish.yaml
+++ b/.github/workflows/publish.yaml
@@ -19,22 +19,14 @@ jobs:
fi
- if: steps.decisions.outputs.PUBLISH == 'true'
- name: Remove Docker
- run: |
- # https://github.com/canonical/lxd-cloud/blob/f20a64a8af42485440dcbfd370faf14137d2f349/test/includes/lxd.sh#L13-L23
- sudo rm -rf /etc/docker
- sudo apt-get purge moby-buildx moby-engine moby-cli moby-compose moby-containerd moby-runc -y
- sudo iptables -P FORWARD ACCEPT
-
- - if: steps.decisions.outputs.PUBLISH == 'true'
- name: Checkout Snapcraft
+ name: Checkout Snapcraft
uses: actions/checkout@v2
with:
# Fetch all of history so Snapcraft can determine its own version from git.
fetch-depth: 0
- if: steps.decisions.outputs.PUBLISH == 'true'
- uses: snapcore/action-build@v1.0.9
+ uses: snapcore/action-build@v1
name: Build Snapcraft Snap
id: build
with:
diff --git a/.github/workflows/spread.yml b/.github/workflows/spread.yml
index 0107f0815f..3d4a873b1e 100644
--- a/.github/workflows/spread.yml
+++ b/.github/workflows/spread.yml
@@ -12,15 +12,9 @@ jobs:
with:
fetch-depth: 0
- - name: Remove Docker
- run: |
- # https://github.com/canonical/lxd-cloud/blob/f20a64a8af42485440dcbfd370faf14137d2f349/test/includes/lxd.sh#L13-L23
- sudo rm -rf /etc/docker
- sudo apt-get purge moby-buildx moby-engine moby-cli moby-compose moby-containerd moby-runc -y
- sudo iptables -P FORWARD ACCEPT
- name: Build snapcraft snap
id: build-snapcraft
- uses: snapcore/action-build@v1.0.9
+ uses: snapcore/action-build@v1
- name: Upload snapcraft snap
uses: actions/upload-artifact@v2
@@ -53,6 +47,7 @@ jobs:
uses: actions/checkout@v2
with:
fetch-depth: 0
+ submodules: true
- name: Download snapcraft snap
uses: actions/download-artifact@v2
@@ -102,6 +97,7 @@ jobs:
uses: actions/checkout@v2
with:
fetch-depth: 0
+ submodules: true
- if: steps.decisions.outputs.RUN == 'true'
name: Download snapcraft snap
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
deleted file mode 100644
index 6e23443b5e..0000000000
--- a/.github/workflows/tests.yaml
+++ /dev/null
@@ -1,90 +0,0 @@
-name: Python Environment Tests
-on:
- push:
- branches:
- - "main"
- - "snapcraft/7.0"
- - "release/*"
- - "hotfix/*"
- pull_request:
-
-jobs:
- linters:
- runs-on: ubuntu-20.04
- steps:
- - name: Checkout code
- uses: actions/checkout@v2
- with:
- fetch-depth: 0
- - name: Set up Python 3.10
- uses: actions/setup-python@v2
- with:
- python-version: "3.10"
- - name: Install dependencies
- run: |
- sudo apt update
- sudo apt install -y libapt-pkg-dev libyaml-dev xdelta3 shellcheck
- pip install -U -r requirements.txt -r requirements-devel.txt
- pip install .
- - name: Run black
- run: |
- make test-black
- - name: Run codespell
- run: |
- make test-codespell
- - name: Run flake8
- run: |
- make test-flake8
- - name: Run isort
- run: |
- make test-isort
- - name: Run mypy
- run: |
- make test-mypy
- - name: Run pydocstyle
- run: |
- make test-pydocstyle
- - name: Run pyright
- run: |
- sudo snap install --classic node
- sudo snap install --classic pyright
- make test-pyright
- - name: Run pylint
- env:
- SNAPCRAFT_IGNORE_YAML_BINDINGS: "1"
- run: |
- make test-pylint
- - name: Run shellcheck
- run: |
- make test-shellcheck
-
- tests:
- strategy:
- fail-fast: false
- matrix:
- python-version: ["3.8", "3.10"]
-
- runs-on: ubuntu-20.04
- steps:
- - name: Checkout code
- uses: actions/checkout@v2
- with:
- fetch-depth: 0
- - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }}
- uses: actions/setup-python@v2
- with:
- python-version: ${{ matrix.python-version }}
- - name: Install dependencies
- run: |
- sudo apt update
- sudo apt install -y libapt-pkg-dev libyaml-dev xdelta3 shellcheck
- pip install -U wheel setuptools pip
- pip install -U -r requirements.txt -r requirements-devel.txt
- pip install .
- - name: Run unit tests
- env:
- SNAPCRAFT_IGNORE_YAML_BINDINGS: "1"
- run: |
- make test-units
- - name: Upload code coverage
- uses: codecov/codecov-action@v1
diff --git a/.github/workflows/tox.yaml b/.github/workflows/tox.yaml
new file mode 100644
index 0000000000..81ea93088d
--- /dev/null
+++ b/.github/workflows/tox.yaml
@@ -0,0 +1,89 @@
+name: Tox
+on:
+ push:
+ branches:
+ - "main"
+ - "snapcraft/7.0"
+ - "release/*"
+ - "hotfix/*"
+ pull_request:
+
+jobs:
+ linters:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Setup Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+ - name: Install dependencies
+ run: |
+ echo "::group::Begin snap install"
+ echo "Installing snaps in the background while running apt and pip..."
+ sudo snap install --no-wait --classic pyright
+ sudo snap install --no-wait shellcheck
+ echo "::endgroup::"
+ echo "::group::apt-get update"
+ sudo apt-get update
+ echo "::endgroup::"
+ echo "::group::apt-get install..."
+ sudo apt-get install --yes libapt-pkg-dev libyaml-dev xdelta3
+ echo "::endgroup::"
+ echo "::group::pip install"
+ python -m pip install 'tox>=4.0.11<5.0' tox-gh
+ echo "::endgroup::"
+ echo "::group::Create virtual environments for linting processes."
+ tox run-parallel --parallel all --parallel-no-spinner -m lint --notest
+ echo "::endgroup::"
+ echo "::group::Wait for snap to complete"
+ snap watch --last=install
+ echo "::endgroup::"
+ - name: Run Linters
+ run: tox run --skip-pkg-install -m lint
+ tests:
+ strategy:
+ fail-fast: false # Run all the tests to their conclusions.
+ matrix:
+ platform: [ubuntu-20.04, ubuntu-22.04]
+ runs-on: ${{ matrix.platform }}
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Set up Python versions on ${{ matrix.platform }}
+ uses: actions/setup-python@v4
+ with:
+ python-version: |
+ 3.8
+ 3.10
+ - name: Install dependencies
+ run: |
+ echo "::group::apt-get update"
+ sudo apt-get update
+ echo "::endgroup::"
+ echo "::group::apt-get install..."
+ sudo apt-get install -y libapt-pkg-dev libyaml-dev xdelta3
+ echo "::endgroup::"
+ echo "::group::pip install"
+ python -m pip install 'tox>=4.0.11<5.0' tox-gh
+ echo "::endgroup::"
+ mkdir -p results
+ - name: Setup Tox environments
+ run: tox run-parallel --parallel auto --parallel-no-spinner --parallel-live -m ci --notest
+ - name: Test with tox
+ run: tox run-parallel --parallel all --parallel-no-spinner --result-json results/tox-${{ matrix.platform }}.json -m ci --skip-pkg-install -- --no-header --quiet -rN
+ - name: Upload code coverage
+ uses: codecov/codecov-action@v3
+ with:
+ directory: ./results/
+ files: coverage*.xml
+ - name: Upload test results
+ if: always()
+ uses: actions/upload-artifact@v3
+ with:
+ name: test-results-${{ matrix.platform }}
+ path: results/
diff --git a/.gitignore b/.gitignore
index 0d8e9104d4..e20d72cf8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
build
Cargo.lock
.coverage**
+coverage-*
demos/*/parts/
demos/*/prime/
demos/**/*.snap
@@ -21,6 +22,7 @@ pip-wheel-metadata/
prime
*.pyc
__pycache__
+.pytest_cache
*.snap
snap/.snapcraft/
.spread-reuse.*
@@ -29,6 +31,8 @@ stage
target
tests/unit/snap/
tests/unit/stage/
+test-results*
+.tox
.vscode
venv
.venv
diff --git a/.gitmodules b/.gitmodules
index e357675a48..776cc5cd70 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "docs"]
path = docs
url = https://github.com/CanonicalLtd/snappy-docs.git
+[submodule "tests/spread/tools/snapd-testing-tools"]
+ path = tests/spread/tools/snapd-testing-tools
+ url = https://github.com/snapcore/snapd-testing-tools.git
diff --git a/HACKING.md b/HACKING.md
index d07d5f1850..a7b33fe922 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -2,18 +2,18 @@
## Setting up a development environment
-We want to make sure everyone develops using a consistent base, to ensure that these instructions rely on LXD (use whatever is convenient as long as you do not stray away from an Ubuntu 16.04 LTS base)
+We want to make sure everyone develops using a consistent base, to ensure that these instructions rely on LXD (use whatever is convenient as long as you do not stray away from an Ubuntu LTS base)
-Clone these sources and make it your working directory:
+Clone the snapcraft repository and its submodules and make it your working directory:
-```
-git clone https://github.com/snapcore/snapcraft.git
+```shell
+git clone https://github.com/snapcore/snapcraft.git --recurse-submodules
cd snapcraft
```
If you already have LXD setup you can skip this part, if not, run:
-```
+```shell
sudo snap install lxd
sudo lxd init --auto --storage-backend=dir
sudo adduser "$USER" lxd
@@ -22,17 +22,17 @@ newgrp lxd
Setup the environment by running:
-```
+```shell
./tools/environment-setup.sh
```
To work inside this environment, run:
-```
+```shell
lxc exec snapcraft-dev -- sudo -iu ubuntu bash
```
-Import your keys (`ssh-import-id`) and add a `Host` entry to your ssh config if you are interested in [Code's](https://snapcraft.io/code) [Remote-SSH]() plugin.
+Import your keys (`ssh-import-id`) and add a `Host` entry to your ssh config if you are interested in [Code's](https://snapcraft.io/code) [Remote-SSH](https://code.visualstudio.com/docs/remote/ssh) plugin.
### Testing
diff --git a/Makefile b/Makefile
index 9448225d85..e863edffcf 100644
--- a/Makefile
+++ b/Makefile
@@ -3,7 +3,7 @@ SOURCES_LEGACY=snapcraft_legacy tests/legacy
.PHONY: autoformat-black
autoformat-black:
- black $(SOURCES) $(SOURCES_LEGACY)
+ tox run -e format
.PHONY: freeze-requirements
freeze-requirements:
@@ -11,56 +11,55 @@ freeze-requirements:
.PHONY: test-black
test-black:
- black --check --diff $(SOURCES) $(SOURCES_LEGACY)
+ tox run -e black
.PHONY: test-codespell
test-codespell:
- codespell --quiet-level 4 --ignore-words-list crate,keyserver,comandos,ro --skip '*.tar,*.xz,*.zip,*.bz2,*.7z,*.gz,*.deb,*.rpm,*.snap,*.gpg,*.pyc,*.png,*.ico,*.jar,*.so,changelog,.git,.hg,.mypy_cache,.tox,.venv,venv,_build,buck-out,__pycache__,build,dist,.vscode,parts,stage,prime,test_appstream.py,./snapcraft.spec,./.direnv,./.pytest_cache'
-
-.PHONY: test-flake8
-test-flake8:
- python3 -m flake8 $(SOURCES) $(SOURCES_LEGACY)
+ tox run -e codespell
.PHONY: test-isort
test-isort:
- isort --check $(SOURCES) $(SOURCES_LEGACY)
+ tox run -e isort
.PHONY: test-mypy
test-mypy:
- mypy $(SOURCES)
+ tox run -e mypy
.PHONY: test-pydocstyle
test-pydocstyle:
- pydocstyle snapcraft
+ tox run -e docstyle
.PHONY: test-pylint
test-pylint:
- pylint snapcraft
- pylint tests/*.py tests/unit --disable=invalid-name,missing-module-docstring,missing-function-docstring,duplicate-code,protected-access,unspecified-encoding,too-many-public-methods,too-many-arguments,too-many-lines
+ tox run -e pylint
.PHONY: test-pyright
test-pyright:
- pyright $(SOURCES)
+ tox run -e pyright
+
+.PHONY: test-ruff
+test-ruff:
+ ruff --config snapcraft_legacy/ruff.toml $(SOURCES_LEGACY)
+ ruff $(SOURCES)
.PHONY: test-shellcheck
test-shellcheck:
-# Skip third-party gradlew script.
- find . \( -name .git -o -name gradlew \) -prune -o -print0 | xargs -0 file -N | grep shell.script | cut -f1 -d: | xargs shellcheck
- ./tools/spread-shellcheck.py spread.yaml tests/spread/
+ tox run -e shellcheck
+ tox run -e spread-shellcheck
.PHONY: test-legacy-units
test-legacy-units:
- pytest --cov-report=xml --cov=snapcraft tests/legacy/unit
+ tox run -e py38-withreq-legacy
.PHONY: test-units
test-units: test-legacy-units
- pytest --cov-report=xml --cov=snapcraft tests/unit
+ tox run -e py38-withreq-unit
.PHONY: tests
tests: tests-static test-units
.PHONY: tests-static
-tests-static: test-black test-codespell test-flake8 test-isort test-mypy test-pydocstyle test-pyright test-pylint test-shellcheck
+tests-static: test-black test-codespell test-ruff test-isort test-mypy test-pydocstyle test-pyright test-pylint test-shellcheck
.PHONY: lint
lint: tests-static
diff --git a/pyproject.toml b/pyproject.toml
index c563dc6ebd..425b0d01f9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -20,6 +20,9 @@ exclude = '''
| ^/prime
)/
'''
+# Targeting future versions as well so we don't have black reformatting code
+# en masse later.
+target_version = ["py38", "py310", "py311"]
[tool.isort]
# black-compatible isort configuration
@@ -29,6 +32,11 @@ force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
line_length = 88
+skip_gitignore = true
+skip = ["tests/spread/tools/snapd-testing-tools"]
+
+[tool.pylint.main]
+ignore-paths = ["tests/legacy"]
[tool.pylint.messages_control]
# duplicate-code can't be disabled locally: https://github.com/PyCQA/pylint/issues/214
@@ -51,3 +59,121 @@ load-plugins = "pylint_fixme_info,pylint_pytest"
[tool.pylint.SIMILARITIES]
min-similarity-lines=10
+
+[tool.mypy]
+python_version = 3.8
+ignore_missing_imports = true
+follow_imports = "silent"
+exclude = [
+ "build",
+ "snapcraft_legacy",
+ "tests/spread",
+ "tests/legacy",
+ "tools",
+ "venv",
+]
+
+[tool.pyright]
+include = ["snapcraft", "tests"]
+exclude = ["tests/legacy", "tests/spread", "build"]
+pythonVersion = "3.8"
+
+[tool.pytest.ini_options]
+minversion = 7.0
+required_plugins = ["pytest-cov>=4.0", "pytest-mock>=3.10", "pytest-subprocess>=1.4"]
+addopts = ["--cov=snapcraft"]
+
+# Most of this ruff configuration comes from craft-parts
+[tool.ruff]
+line-length = 88
+extend-exclude = [
+ "docs",
+ "__pycache__",
+ "legacy",
+ "tests/legacy",
+]
+select = [
+ "E", "F", # The rules built into Flake8
+ "I", # isort checking
+ "PLC", "PLE", "PLR", "PLW", # Pylint
+ # Additional stricter checking than previously enabled:
+ "A", # Shadowing built-ins.
+ "W", # PyCodeStyle warnings.
+ "N", # Pep8 naming
+ "YTT", # flake8-2020: Misuse of `sys.version` and `sys.version_info`
+ "ANN", # Annotations
+ "S", # Checks for common security issues
+ "BLE", # Blind exception
+ "B", # Opinionated bugbear linting
+ "C4", # better comprehensions
+ "T10", # Ensure we don't leave code that starts the debugger in
+ "ICN", # Unconventional import aliases.
+ "Q", # Consistent quotations
+ "RET", # Return values
+]
+ignore = [
+ # These following copy the flake8 configuration
+ #"E203", # Whitespace before ":" -- Commented because ruff doesn't currently check E203
+ "E501", # Line too long
+ # The following copy our pydocstyle configuration
+ "D105", # Missing docstring in magic method (reason: magic methods already have definitions)
+ "D107", # Missing docstring in __init__ (reason: documented in class docstring)
+ "D203", # 1 blank line required before class docstring (reason: pep257 default)
+ "D213", # Multi-line docstring summary should start at the second line (reason: pep257 default)
+ "D215", # Section underline is over-indented (reason: pep257 default)
+
+
+ # Stricter type checking rules that that are disabled.
+ "A003", # Class attribute shadowing a python bulitin (class attributes are seldom if ever referred to bare)
+ "N818", # Exception names ending with suffix `Error`
+ "ANN002", "ANN003", # Missing type annotation for *args and **kwargs
+ "ANN101", "ANN102", # Type annotations for `self` and `cls` in methods
+ "ANN204", # Missing type annotations for magic methods
+ "ANN401", # Disallowing `typing.Any`
+ "B904", # Within an except clause, always explicitly `raise` an exception `from` something.
+ "B905", # Zip without explicit `strict=` parameter - this only exists in 3.10+
+
+
+ # Disabled because the current code breaks these rules without "noqa" comments
+ # Most of these are probably worth enabling eventually.
+
+ # 3 instances of breaking N802, all from overriding do_GET in http.server.BaseHTTPRequestHandler
+ # We can probably noqa these and enable the rule
+ "N802", # Function names should be lowercase.
+ # 3 instances of N805, all PyDantic validators (which are classmethods).
+ # These have pylint disablers, but ruff doesn't understand those (yet).
+ # https://github.com/charliermarsh/ruff/issues/1203
+ "N805", # First argument of a method should be named `self`
+ # Annotation issues appear to be mostly in older code, so could be eventually enabled.
+ # 39 instances of ANN001.
+ "ANN001", # Missing type annotation for function argument
+ # 5 instances of ANN201, 10 instances of ANN202
+ "ANN201", "ANN202", # Missing return type annotation for public/private function
+ # 13 instances of ANN206 - probably mostly :noqa-able
+ "ANN206",
+ # 4 instances - would break if run with optimization
+ "S101", # Use of assert detected
+ # 1 instance, disabled for pylint
+ "BLE001",
+ # Comprehensions - IDK, these ones flagged and they really could go either way.
+ "C405", "C408", "C414",
+ "Q000", # 2 single-quoted strings - probably accidental
+ "RET504", "RET506", # Return value related.
+
+]
+
+[tool.ruff.per-file-ignores]
+"tests/**.py" = [
+ "D", # Ignore docstring rules in tests
+ "ANN", # Ignore type annotations in tests
+ "S101", # Yeah of course we assert in tests
+ "B009", # Allow calling `getattr` in tests since it can be used to make the test clearer.
+ "S103", # Allow `os.chmod` setting a permissive mask `0o555` on file or directory
+ "S105", # Allow Possible hardcoded password.
+ "S106", # Allow Possible hardcoded password.
+ "S108", # Allow Probable insecure usage of temporary file or directory.
+]
+"__init__.py" = ["I001"] # Imports in __init__ filesare allowed to be out of order
+
+[tool.ruff.flake8-annotations]
+suppress-none-returning = true # We don't need to explicitly point out that a function doesn't return anything
diff --git a/requirements-devel.txt b/requirements-devel.txt
index dda1472803..0a2043e91d 100644
--- a/requirements-devel.txt
+++ b/requirements-devel.txt
@@ -1,72 +1,74 @@
-astroid==2.12.11
-attrs==22.1.0
-black==22.10.0
+astroid==2.13.2
+attrs==22.2.0
+black==23.1.0
+cachetools==5.2.0
catkin-pkg==0.5.2
certifi==2022.9.24
cffi==1.15.1
-chardet==5.0.0
+chardet==5.1.0
charset-normalizer==2.1.1
click==8.1.3
-codespell==2.2.1
-coverage==6.5.0
+codespell==2.2.2
+colorama==0.4.6
+coverage==7.0.4
craft-cli==1.2.0
craft-grammar==1.1.1
-craft-parts==1.15.3
-craft-providers==1.6.2
+craft-parts==1.18.1
+craft-providers==1.7.2
craft-store==2.3.0
cryptography==3.4
Deprecated==1.2.13
dill==0.3.5.1
distro==1.8.0
docutils==0.19
+exceptiongroup==1.1.0
extras==1.0.0
fixtures==4.0.1
-flake8==5.0.4
gnupg==2.3.1
-httplib2==0.20.4
-hupper==1.10.3
+httplib2==0.21.0
+hupper==1.11
idna==3.4
-iniconfig==1.1.1
-isort==5.10.1
+importlib-metadata==6.0.0
+iniconfig==2.0.0
+isort==5.11.4
jaraco.classes==3.2.3
jeepney==0.8.0
jsonschema==2.5.1
-keyring==23.9.3
-launchpadlib==1.10.16
-lazr.restfulclient==0.14.4
+keyring==23.13.1
+launchpadlib==1.11.0
+lazr.restfulclient==0.14.5
lazr.uri==1.0.6
-lazy-object-proxy==1.7.1
-lxml==4.9.1
+lazy-object-proxy==1.9.0
+lxml==4.9.2
macaroonbakery==1.3.1
mccabe==0.7.0
more-itertools==8.14.0
mypy==0.982
mypy-extensions==0.4.3
-oauthlib==3.2.1
-overrides==7.0.0
-packaging==21.3
-PasteDeploy==2.1.1
-pathspec==0.10.1
-pbr==5.10.0
+oauthlib==3.2.2
+overrides==7.3.1
+packaging==23.0
+PasteDeploy==3.0.1
+pathspec==0.10.3
+pbr==5.11.0
pexpect==4.8.0
-plaster==1.0
-plaster-pastedeploy==0.7
-platformdirs==2.5.2
+plaster==1.1.2
+plaster-pastedeploy==1.0.1
+platformdirs==2.6.2
pluggy==1.0.0
progressbar==2.5
protobuf==3.20.3
psutil==5.9.2
ptyprocess==0.7.0
-py==1.11.0
-pycodestyle==2.9.1
+pycodestyle==2.10.0
pycparser==2.21
pydantic==1.9.0
-pydantic-yaml==0.8.1
-pydocstyle==6.1.1
+pydantic-yaml==0.9.0
+pydocstyle==6.2.3
pyelftools==0.29
-pyflakes==2.5.0
+pyflakes==3.0.1
pyftpdlib==1.5.7
-pylint==2.15.4
+pylint==2.15.10
pylint-fixme-info==1.0.3
pylint-pytest==1.1.2
pylxd==2.3.1
@@ -79,18 +81,19 @@ pytest-cov==4.0.0
pytest-mock==3.10.0
pytest-subprocess==1.4.2
python-dateutil==2.8.2
-python-debian==0.1.47
-pytz==2022.4
+python-debian==0.1.49
+pytz==2022.7
pyxdg==0.28
PyYAML==6.0
raven==6.10.0
requests==2.28.1
requests-toolbelt==0.10.0
requests-unixsocket==0.3.0
+ruff==0.0.220
SecretStorage==3.3.3
semantic-version==2.10.0
semver==2.13.0
-simplejson==3.17.6
+simplejson==3.18.1
six==1.16.0
snap-helpers==0.2.0
snowballstemmer==2.2.0
@@ -100,12 +103,14 @@ testtools==2.5.0
tinydb==4.7.0
toml==0.10.2
tomli==2.0.1
-tomlkit==0.11.5
+tomlkit==0.11.6
+tox==4.0.11
translationstring==1.4
types-Deprecated==1.2.9
-types-PyYAML==6.0.12
-types-requests==2.28.11.2
-types-setuptools==65.4.0.0
+types-docutils==0.19.1.1
+types-PyYAML==6.0.12.2
+types-requests==2.28.11.7
+types-setuptools==65.6.0.3
types-tabulate==0.9.0.0
types-urllib3==1.26.25
typing_extensions==4.4.0
diff --git a/requirements.txt b/requirements.txt
index 9e3259d2a3..dc9aaec5df 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,51 +1,52 @@
-attrs==22.1.0
+attrs==22.2.0
catkin-pkg==0.5.2
certifi==2022.9.24
cffi==1.15.1
-chardet==5.0.0
+chardet==5.1.0
charset-normalizer==2.1.1
click==8.1.3
craft-cli==1.2.0
craft-grammar==1.1.1
-craft-parts==1.15.3
-craft-providers==1.6.2
+craft-parts==1.18.1
+craft-providers==1.7.2
craft-store==2.3.0
cryptography==3.4
Deprecated==1.2.13
distro==1.8.0
docutils==0.19
gnupg==2.3.1
-httplib2==0.20.4
+httplib2==0.21.0
idna==3.4
+importlib-metadata==6.0.0
jaraco.classes==3.2.3
jeepney==0.8.0
jsonschema==2.5.1
-keyring==23.9.3
-launchpadlib==1.10.16
-lazr.restfulclient==0.14.4
+keyring==23.13.1
+launchpadlib==1.11.0
+lazr.restfulclient==0.14.5
lazr.uri==1.0.6
-lxml==4.9.1
+lxml==4.9.2
macaroonbakery==1.3.1
more-itertools==8.14.0
mypy-extensions==0.4.3
-oauthlib==3.2.1
-overrides==7.0.0
-packaging==21.3
-platformdirs==2.5.2
+oauthlib==3.2.2
+overrides==7.3.1
+packaging==23.0
+platformdirs==2.6.2
progressbar==2.5
protobuf==3.20.3
psutil==5.9.2
pycparser==2.21
pydantic==1.9.0
-pydantic-yaml==0.8.1
+pydantic-yaml==0.9.0
pyelftools==0.29
pylxd==2.3.1
pymacaroons==0.13.0
pyparsing==3.0.9
pyRFC3339==1.1
python-dateutil==2.8.2
-python-debian==0.1.47
-pytz==2022.4
+python-debian==0.1.49
+pytz==2022.7
pyxdg==0.28
PyYAML==6.0
raven==6.10.0
@@ -55,7 +56,7 @@ requests-unixsocket==0.3.0
SecretStorage==3.3.3
semantic-version==2.10.0
semver==2.13.0
-simplejson==3.17.6
+simplejson==3.18.1
six==1.16.0
snap-helpers==0.2.0
tabulate==0.9.0
diff --git a/schema/snapcraft.json b/schema/snapcraft.json
index 806177dc15..c9c61369e4 100644
--- a/schema/snapcraft.json
+++ b/schema/snapcraft.json
@@ -523,7 +523,7 @@
"additionalProperties": false,
"validation-failure": "{!r} is not a valid system-username.",
"patternProperties": {
- "^snap_(daemon|microk8s)$": {
+ "^snap_(daemon|microk8s|aziotedge|aziotdu)$": {
"oneOf": [
{
"$ref": "#/definitions/system-username-scope"
diff --git a/setup.cfg b/setup.cfg
index a80dbe21d5..774e3e672a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,8 @@
+[codespell]
+ignore-words-list = buildd,crate,keyserver,comandos,ro
+skip = waf,*.tar,*.xz,*.zip,*.bz2,*.7z,*.gz,*.deb,*.rpm,*.snap,*.gpg,*.pyc,*.png,*.ico,*.jar,*.so,changelog,.git,.hg,.mypy_cache,.tox,.venv,venv,_build,buck-out,__pycache__,build,dist,.vscode,parts,stage,prime,test_appstream.py,./snapcraft.spec,./.direnv,./.pytest_cache,.ruff_cache
+quiet-level = 4
+
[flake8]
# E501 line too long
# E203 whitespace before ':'
@@ -25,11 +30,6 @@ exclude =
stage,
prime
-[mypy]
-python_version = 3.8
-ignore_missing_imports = True
-follow_imports = silent
-
[pycodestyle]
max-line-length = 88
ignore = E203,E501
@@ -40,3 +40,140 @@ ignore = E203,E501
# D213 Multi-line docstring summary should start at the second line (reason: pep257 default)
ignore = D107, D203, D213
ignore_decorators = overrides
+
+[tox:tox]
+min_version = 4.0
+env_list =
+ # Parametrized environments.
+ # First parameter allows us to choose Python 3.8 or 3.10.
+ # Second parameter chooses how to define the environment:
+ # withreq: using requirements-devel.txt
+ # noreq: Without either requirements file (but including dev requirements)
+ # Third parameter selects the current unit tests (unit) or the legacy unit tests (legacy)
+ py{38,310}-{withreq,noreq}-{unit,legacy}
+skip_missing_interpreters = true
+labels =
+ # Minimal testing environments. run with `tox run-parallel -m test`
+ test = py38-withreq-{unit,legacy}
+ # Test in Python 3.10 from an empty environment.
+ future-test = py310-noreq-{unit,legacy}
+ # Environments to run in CI
+ ci = py{38,310}-withreq-{unit,legacy},py{38,310}-noreq-unit
+ # Just run the regular unit tests, not the legacy ones
+ unit = py{38,310}-{withreq,noreq}-unit
+ # Legacy unit tests only
+ legacy = py{38,310}-{withreq,noreq}-legacy
+
+[testenv]
+deps =
+ withreq,pylint,mypy,pyright: -r{tox_root}/requirements-devel.txt
+ noreq: PyNaCl>=1.5.0
+ noreq,codespell: python-apt@git+https://salsa.debian.org/apt-team/python-apt.git@2.0.0
+extras =
+ noreq: dev
+package = wheel
+set_env =
+ SNAPCRAFT_IGNORE_YAML_BINDINGS = 1
+ COVERAGE_FILE = .coverage_{env_name}
+
+[testenv:py{38,310}-{withreq,noreq}-unit]
+description = Run the unit tests
+commands =
+ pytest {tty:--color=yes} --cov-report=xml:results/coverage-{env_name}.xml --junit-xml=results/test-results-{env_name}.xml tests/unit {posargs}
+
+[testenv:py{38,310}-{withreq,noreq}-legacy]
+description = Run the legacy unit tests
+commands =
+ pytest {tty:--color=yes} --cov-report=xml:results/coverage-{env_name}.xml --junit-xml=results/test-results-{env_name}.xml tests/legacy/unit/ {posargs}
+
+[testenv:format-black]
+description = Autoformat with black
+labels = fix
+deps = black
+skip_install = true
+# Note: this does not include `snapcraft_legacy` as it contains several files that need reformatting.
+commands = black setup.py snapcraft tests
+
+[testenv:format-ruff]
+base = ruff
+description = Autoformat with ruff
+labels = fix
+commands =
+ ruff --fix setup.py snapcraft tests tools
+ ruff --fix --config snapcraft_legacy/ruff.toml snapcraft_legacy tests/legacy
+
+[testenv:pylint]
+description = Lint with pylint
+labels = lint
+# This runs all commands even if the first fails.
+# Not to be confused with ignore_outcome, which turns errors into warnings.
+ignore_errors = true
+commands =
+ pylint -j 0 snapcraft
+ pylint -j 0 tests --disable=invalid-name,missing-module-docstring,missing-function-docstring,duplicate-code,protected-access,unspecified-encoding,too-many-public-methods,too-many-arguments,too-many-lines,redefined-outer-name
+
+[testenv:shellcheck]
+description = Check spelling with shellcheck
+labels = lint
+skip_install = true
+allowlist_externals = bash, git
+commands = bash -c "git ls-files | file --mime-type -Nnf- | grep shellscript | cut -f1 -d: | xargs shellcheck"
+
+[testenv:spread-shellcheck]
+description = Run shellcheck on spread's test.yaml files using spread-shellcheck.py
+labels = lint
+deps = pyyaml
+skip_install = true
+commands = python3 tools/spread-shellcheck.py spread.yaml tests/spread/
+
+[testenv:mypy]
+description = Run mypy
+labels = type, lint
+skip_install = true
+commands = mypy --install-types --non-interactive .
+
+[testenv:pyright]
+description = run PyRight
+labels = type, lint
+allowlist_externals = pyright
+commands = pyright snapcraft tests
+
+[testenv:black]
+description = run black in checking mode
+skip_install = true
+labels = lint
+deps = black
+commands = black --check --diff setup.py snapcraft tests
+
+[testenv:codespell]
+description = Check spelling with codespell
+skip_install = true
+labels = lint
+deps = codespell
+commands = codespell
+
+[testenv:ruff]
+description = Lint with ruff
+skip_install = true
+labels = lint
+deps = ruff==0.0.220
+# This runs all commands even if the first fails.
+# Not to be confused with ignore_outcome, which turns errors into warnings.
+ignore_errors = true
+commands =
+ ruff --config snapcraft_legacy/ruff.toml snapcraft_legacy tests/legacy
+ ruff setup.py snapcraft tests tools
+
+[testenv:isort]
+description = Check import order with isort
+skip_install = true
+labels = lint
+deps = isort
+commands = isort --check .
+
+[testenv:docstyle]
+description = Check documentation style with pydocstyle
+skip_install = true
+labels = lint
+deps = pydocstyle
+commands = pydocstyle snapcraft
diff --git a/setup.py b/setup.py
index f9908deaac..787a08ab0c 100755
--- a/setup.py
+++ b/setup.py
@@ -16,6 +16,7 @@
# along with this program. If not, see .
import os
+import re
import sys
from setuptools import find_namespace_packages, setup
@@ -25,7 +26,7 @@
def recursive_data_files(directory, install_directory):
data_files = []
- for root, directories, file_names in os.walk(directory):
+ for root, _directories, file_names in os.walk(directory):
file_paths = [os.path.join(root, file_name) for file_name in file_names]
data_files.append((os.path.join(install_directory, root), file_paths))
return data_files
@@ -36,7 +37,7 @@ def recursive_data_files(directory, install_directory):
description = "Publish your app for Linux users for desktop, cloud, and IoT."
author_email = "snapcraft@lists.snapcraft.io"
url = "https://github.com/snapcore/snapcraft"
-license = "GPL v3"
+license_ = "GPL v3"
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Console",
@@ -61,8 +62,7 @@ def recursive_data_files(directory, install_directory):
dev_requires = [
"black",
"codespell",
- "coverage",
- "flake8",
+ "coverage[toml]",
"pyflakes",
"fixtures",
"isort",
@@ -82,6 +82,8 @@ def recursive_data_files(directory, install_directory):
"pytest-cov",
"pytest-mock",
"pytest-subprocess",
+ "ruff==0.0.220",
+ "tox>=4.0",
"types-PyYAML",
"types-requests",
"types-setuptools",
@@ -117,6 +119,8 @@ def recursive_data_files(directory, install_directory):
"requests-toolbelt",
"requests-unixsocket",
"requests",
+ # pin setuptools<66 (CRAFT-1598)
+ "setuptools<66",
"simplejson",
"snap-helpers",
"tabulate",
@@ -126,8 +130,13 @@ def recursive_data_files(directory, install_directory):
]
try:
- os_release = open("/etc/os-release").read()
- ubuntu = "ID=ubuntu" in os_release
+ ubuntu = bool(
+ re.search(
+ r"^ID(?:_LIKE)?=.*\bubuntu\b.*$",
+ open("/etc/os-release").read(),
+ re.MULTILINE,
+ )
+ )
except FileNotFoundError:
ubuntu = False
@@ -154,7 +163,7 @@ def recursive_data_files(directory, install_directory):
author_email=author_email,
url=url,
packages=find_namespace_packages(),
- license=license,
+ license=license_,
classifiers=classifiers,
scripts=scripts,
entry_points=dict(
diff --git a/snap/hooks/remove b/snap/hooks/remove
new file mode 100644
index 0000000000..d8c8ac48d0
--- /dev/null
+++ b/snap/hooks/remove
@@ -0,0 +1,51 @@
+#! /usr/bin/env sh
+
+# Remove hook for snapcraft
+
+# separate lxc output by newlines
+# \n can not be last, or it will be stripped by $() - see shellcheck SC3003
+IFS=$(printf '\n\t')
+
+# check for lxc
+if ! command -v lxc > /dev/null 2>&1; then
+ >&2 echo "lxc not installed"
+ exit 0
+fi
+
+# check for snapcraft project
+if ! lxc project info snapcraft > /dev/null 2>&1; then
+ >&2 echo "lxc project 'snapcraft' does not exist"
+ exit 0
+fi
+
+# get instances
+instances="$(lxc list --project=snapcraft --format=csv --columns="n")"
+
+# delete base instances
+if [ -n "$instances" ]; then
+ for instance in $instances; do
+ >&2 echo "checking instance $instance"
+ if [ "$(expr "$instance" : "^base-instance-snapcraft-.*")" -ne 0 ]; then
+ >&2 echo "deleting base instance $instance"
+ lxc delete --project=snapcraft --force "$instance"
+ fi
+ done
+else
+ >&2 echo "no base instances to remove"
+fi
+
+# get images
+images="$(lxc image list --project=snapcraft --format=csv --columns=l)"
+
+# delete base images created by craft-providers < 1.7.0
+if [ -n "$images" ]; then
+ for image in $images; do
+ >&2 echo "image: $image"
+ if expr "$image" : "^snapshot-.*"; then
+ >&2 echo "deleting base image $instance"
+ lxc image delete --project=snapcraft "$image"
+ fi
+ done
+else
+ >&2 echo "no base images to remove"
+fi
diff --git a/snapcraft/cli.py b/snapcraft/cli.py
index d31a071891..48fb588447 100644
--- a/snapcraft/cli.py
+++ b/snapcraft/cli.py
@@ -49,6 +49,7 @@
commands.SnapCommand, # hidden (legacy compatibility)
commands.PluginsCommand,
commands.ListPluginsCommand,
+ commands.TryCommand,
],
),
craft_cli.CommandGroup(
@@ -148,6 +149,20 @@ def get_verbosity() -> EmitterMode:
if utils.strtobool(os.getenv("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG", "n").strip()):
verbosity = EmitterMode.DEBUG
+ # if defined, use environmental variable SNAPCRAFT_VERBOSITY_LEVEL
+ verbosity_env = os.getenv("SNAPCRAFT_VERBOSITY_LEVEL")
+ if verbosity_env:
+ try:
+ verbosity = EmitterMode[verbosity_env.strip().upper()]
+ except KeyError:
+ values = utils.humanize_list(
+ [e.name.lower() for e in EmitterMode], "and", sort=False
+ )
+ raise ArgumentParsingError(
+ f"cannot parse verbosity level {verbosity_env!r} from environment "
+ f"variable SNAPCRAFT_VERBOSITY_LEVEL (valid values are {values})"
+ ) from KeyError
+
return verbosity
diff --git a/snapcraft/commands/__init__.py b/snapcraft/commands/__init__.py
index 0e358338fe..6f86f96a7d 100644
--- a/snapcraft/commands/__init__.py
+++ b/snapcraft/commands/__init__.py
@@ -49,6 +49,7 @@
PullCommand,
SnapCommand,
StageCommand,
+ TryCommand,
)
from .manage import StoreCloseCommand, StoreReleaseCommand
from .names import (
@@ -112,5 +113,6 @@
"StoreTracksCommand",
"StoreUploadCommand",
"StoreWhoAmICommand",
+ "TryCommand",
"VersionCommand",
]
diff --git a/snapcraft/commands/account.py b/snapcraft/commands/account.py
index 9507bc7b6a..61586a0a85 100644
--- a/snapcraft/commands/account.py
+++ b/snapcraft/commands/account.py
@@ -204,7 +204,7 @@ def run(self, parsed_args):
with contextlib.suppress(ValueError):
expiry_date = datetime.strptime(parsed_args.expires, date_format)
break
- else:
+ else: # noqa: PLW0120 Else clause on loop without a break statement
valid_formats = utils.humanize_list(_VALID_DATE_FORMATS, "or")
raise ArgumentParsingError(
f"The expiry follow an ISO 8601 format ({valid_formats})"
diff --git a/snapcraft/commands/discovery.py b/snapcraft/commands/discovery.py
index b5ae84d172..dce63fe315 100644
--- a/snapcraft/commands/discovery.py
+++ b/snapcraft/commands/discovery.py
@@ -25,8 +25,14 @@
from overrides import overrides
from snapcraft import errors
-from snapcraft.parts.lifecycle import get_snap_project, process_yaml
+from snapcraft.parts.lifecycle import (
+ apply_yaml,
+ extract_parse_info,
+ get_snap_project,
+ process_yaml,
+)
from snapcraft.projects import Project
+from snapcraft.utils import get_host_architecture
if TYPE_CHECKING:
import argparse
@@ -66,7 +72,15 @@ def run(self, parsed_args):
snap_project = get_snap_project()
# Run this to trigger legacy behavior
yaml_data = process_yaml(snap_project.project_file)
- project = Project.unmarshal(yaml_data)
+
+ # process yaml before unmarshalling the data
+ arch = get_host_architecture()
+ yaml_data_for_arch = apply_yaml(yaml_data, arch, arch)
+ # discard parse-info as it is not part of Project which we use to
+ # determine the base
+ extract_parse_info(yaml_data_for_arch)
+
+ project = Project.unmarshal(yaml_data_for_arch)
base = project.get_effective_base()
message = (
f"Displaying plugins available to the current base {base!r} project"
diff --git a/snapcraft/commands/legacy.py b/snapcraft/commands/legacy.py
index 8a7f13609d..3123541517 100644
--- a/snapcraft/commands/legacy.py
+++ b/snapcraft/commands/legacy.py
@@ -70,6 +70,12 @@ class StoreLegacyUploadMetadataCommand(LegacyBaseCommand):
@overrides
def fill_parser(self, parser: "argparse.ArgumentParser") -> None:
+ parser.add_argument(
+ "snap_file",
+ metavar="snap-file",
+ type=str,
+ help="Snap to upload metadata from",
+ )
parser.add_argument(
"--force",
action="store_true",
@@ -222,7 +228,7 @@ class StoreLegacyRegisterKeyCommand(LegacyBaseCommand):
help_msg = "Register a key to sign assertions with the Snap Store."
overview = textwrap.dedent(
"""
- Register a a key with the Snap Store. Prior to registration, use register-key
+ Register a key with the Snap Store. Prior to registration, use create-key
to create one."""
)
diff --git a/snapcraft/commands/lifecycle.py b/snapcraft/commands/lifecycle.py
index 05706ccce0..ae3860f3d7 100644
--- a/snapcraft/commands/lifecycle.py
+++ b/snapcraft/commands/lifecycle.py
@@ -296,3 +296,23 @@ class CleanCommand(_LifecycleStepCommand):
remove the managed snap packing environment (VM or container).
"""
)
+
+
+class TryCommand(_LifecycleCommand):
+ """Command to prepare the parts for ``snap try``."""
+
+ name = "try"
+ help_msg = 'Prepare a snap for "snap try".'
+ overview = textwrap.dedent(
+ """
+ Process parts and expose the ``prime`` directory containing the
+ final payload, ready for ``snap try prime``.
+ """
+ )
+
+ @overrides
+ def run(self, parsed_args):
+ """Overridden to give a helpful message when the lifecycle finishes."""
+ super().run(parsed_args)
+ if not utils.is_managed_mode():
+ emit.message("You can now run `snap try prime`")
diff --git a/snapcraft/elf/_elf_file.py b/snapcraft/elf/_elf_file.py
index 85c91f4a09..572259ce49 100644
--- a/snapcraft/elf/_elf_file.py
+++ b/snapcraft/elf/_elf_file.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright 2016-2022 Canonical Ltd.
+# Copyright 2016-2023 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
@@ -125,7 +125,6 @@ def __init__(
arch_tuple: _ElfArchitectureTuple,
soname_cache: SonameCache,
) -> None:
-
self.soname = soname
self.soname_path = soname_path
self.search_paths = search_paths
@@ -325,6 +324,7 @@ def _extract_attributes(self) -> None: # noqa: C901
def is_linker_compatible(self, *, linker_version: str) -> bool:
"""Determine if the linker will work given the required glibc version."""
version_required = self.get_required_glibc()
+ # TODO: pkg_resources is deprecated in setuptools>66 (CRAFT-1598)
is_compatible = parse_version(version_required) <= parse_version(linker_version)
emit.debug(
f"Check if linker {linker_version!r} works with GLIBC_{version_required} "
@@ -343,6 +343,7 @@ def get_required_glibc(self) -> str:
if not version.startswith("GLIBC_"):
continue
version = version[6:]
+ # TODO: pkg_resources is deprecated in setuptools>66 (CRAFT-1598)
if parse_version(version) > parse_version(version_required):
version_required = version
@@ -356,7 +357,7 @@ def load_dependencies(
content_dirs: List[Path],
arch_triplet: str,
soname_cache: Optional[SonameCache] = None,
- ) -> Set[str]:
+ ) -> Set[Path]:
"""Load the set of libraries that are needed to satisfy elf's runtime.
This may include libraries contained within the project.
@@ -364,9 +365,11 @@ def load_dependencies(
:param root_path: the root path to search for missing dependencies.
:param base_path: the core base path to search for missing dependencies.
+ :param content_dirs: list of paths sourced from content snaps.
+ :param arch_triplet: architecture triplet of the platform.
:param soname_cache: a cache of previously search dependencies.
- :returns: a set of string with paths to the library dependencies of elf.
+ :returns: a set of paths to the library dependencies of elf.
"""
if soname_cache is None:
soname_cache = SonameCache()
@@ -402,10 +405,10 @@ def load_dependencies(
)
# Return the set of dependency paths, minus those found in the base.
- dependencies: Set[str] = set()
+ dependencies: Set[Path] = set()
for library in self.dependencies:
if not library.in_base_snap:
- dependencies.add(str(library.path))
+ dependencies.add(library.path)
return dependencies
diff --git a/snapcraft/elf/_patcher.py b/snapcraft/elf/_patcher.py
index 0c607c9e60..9c625aa9f8 100644
--- a/snapcraft/elf/_patcher.py
+++ b/snapcraft/elf/_patcher.py
@@ -41,7 +41,7 @@ def __init__(
"""Create a Patcher instance.
:param dynamic_linker: The path to the dynamic linker to set the ELF file to.
- :param snap_path: The base path for the snap being processed.
+ :param root_path: The base path for the snap being processed.
:param preferred_patchelf: patch the necessary elf_files with this patchelf.
"""
self._dynamic_linker = dynamic_linker
@@ -62,11 +62,15 @@ def patch(self, *, elf_file: ElfFile) -> None:
patchelf_args = []
if elf_file.interp and elf_file.interp != self._dynamic_linker:
patchelf_args.extend(["--set-interpreter", self._dynamic_linker])
+ emit.progress(f" Interpreter={self._dynamic_linker!r}")
if elf_file.dependencies:
current_rpath = self.get_current_rpath(elf_file)
proposed_rpath = self.get_proposed_rpath(elf_file)
+ emit.progress(f" Current rpath={current_rpath}")
+ emit.progress(f" Proposed rpath={proposed_rpath}")
+
# Removing the current rpath should not be necessary after patchelf 0.11,
# see https://github.com/NixOS/patchelf/issues/94
@@ -111,7 +115,7 @@ def _run_patchelf(self, *, patchelf_args: List[str], elf_file_path: Path) -> Non
os.unlink(elf_file_path)
shutil.copy2(temp_file.name, elf_file_path)
- @functools.lru_cache(maxsize=1024)
+ @functools.lru_cache(maxsize=1024) # noqa: B019 Possible memory leaks in lru_cache
def get_current_rpath(self, elf_file: ElfFile) -> List[str]:
"""Obtain the current rpath from the ELF file dynamic section."""
output = subprocess.check_output(
@@ -119,7 +123,7 @@ def get_current_rpath(self, elf_file: ElfFile) -> List[str]:
)
return [x for x in output.decode().strip().split(":") if x]
- @functools.lru_cache(maxsize=1024)
+ @functools.lru_cache(maxsize=1024) # noqa: B019 Possible memory leaks in lru_cache
def get_proposed_rpath(self, elf_file: ElfFile) -> List[str]:
"""Obtain the proposed rpath pointing to the base or application snaps."""
origin_rpaths: List[str] = []
diff --git a/snapcraft/elf/elf_utils.py b/snapcraft/elf/elf_utils.py
index b3ebb75c20..4519b86faa 100644
--- a/snapcraft/elf/elf_utils.py
+++ b/snapcraft/elf/elf_utils.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright 2016-2022 Canonical Ltd.
+# Copyright 2016-2023 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
@@ -136,3 +136,8 @@ def get_arch_triplet() -> str:
raise RuntimeError(f"Arch triplet not defined for arch {arch!r}")
return arch_config.arch_triplet
+
+
+def get_all_arch_triplets() -> List[str]:
+ """Get a list of all architecture triplets."""
+ return [architecture.arch_triplet for architecture in _ARCH_CONFIG.values()]
diff --git a/snapcraft/elf/errors.py b/snapcraft/elf/errors.py
index b348bcae6e..2ff3734099 100644
--- a/snapcraft/elf/errors.py
+++ b/snapcraft/elf/errors.py
@@ -30,10 +30,7 @@ def __init__(self, path: Path, *, cmd: List[str], code: int) -> None:
self.cmd = cmd
self.code = code
- super().__init__(
- f"{str(path)!r} cannot be patched to function properly in a classic"
- f"confined snap: {cmd} failed with exit code {code}"
- )
+ super().__init__(f"Error patching ELF file: {cmd} failed with exit code {code}")
class CorruptedElfFile(errors.SnapcraftError):
diff --git a/snapcraft/extensions/_utils.py b/snapcraft/extensions/_utils.py
index e0b6ab66f3..630e0bc433 100644
--- a/snapcraft/extensions/_utils.py
+++ b/snapcraft/extensions/_utils.py
@@ -90,7 +90,7 @@ def _apply_extension(
# Next, apply the part-specific components
part_extension = extension.get_part_snippet()
parts = yaml_data["parts"]
- for part_name, part_definition in parts.items():
+ for _part_name, part_definition in parts.items():
for property_name, property_value in part_extension.items():
part_definition[property_name] = _apply_extension_property(
part_definition.get(property_name), property_value
diff --git a/snapcraft/extensions/kde_neon.py b/snapcraft/extensions/kde_neon.py
new file mode 100644
index 0000000000..dfd61366bf
--- /dev/null
+++ b/snapcraft/extensions/kde_neon.py
@@ -0,0 +1,245 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2022 Canonical Ltd.
+# 2023 Scarlett Moore
+#
+# 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 .
+
+"""Generic KDE NEON extension to support core22 and onwards."""
+import dataclasses
+import functools
+import re
+from typing import Any, Dict, List, Optional, Tuple
+
+from overrides import overrides
+
+from .extension import Extension, get_extensions_data_dir, prepend_to_env
+
+_SDK_SNAP = {"core22": "kde-frameworks-5-102-qt-5-15-8-core22-sd"}
+
+
+@dataclasses.dataclass
+class ExtensionInfo:
+ """Content/SDK build information."""
+
+ cmake_args: str
+
+
+@dataclasses.dataclass
+class KDESnaps:
+ """A structure of KDE related snaps."""
+
+ sdk: str
+ content: str
+ builtin: bool = True
+
+
+class KDENeon(Extension):
+ r"""The KDE Neon extension.
+
+ This extension makes it easy to assemble KDE based applications
+ using the Neon stack.
+
+ It configures each application with the following plugs:
+
+ \b
+ - Common Icon Themes.
+ - Common Sound Themes.
+ - The Qt5 and KDE Frameworks runtime libraries and utilities.
+
+ For easier desktop integration, it also configures each application
+ entry with these additional plugs:
+
+ \b
+ - desktop (https://snapcraft.io/docs/desktop-interface)
+ - desktop-legacy (https://snapcraft.io/docs/desktop-legacy-interface)
+ - opengl (https://snapcraft.io/docs/opengl-interface)
+ - wayland (https://snapcraft.io/docs/wayland-interface)
+ - x11 (https://snapcraft.io/docs/x11-interface)
+ """
+
+ @staticmethod
+ @overrides
+ def get_supported_bases() -> Tuple[str, ...]:
+ return ("core22",)
+
+ @staticmethod
+ @overrides
+ def get_supported_confinement() -> Tuple[str, ...]:
+ return "strict", "devmode"
+
+ @staticmethod
+ @overrides
+ def is_experimental(base: Optional[str]) -> bool:
+ return False
+
+ @overrides
+ def get_app_snippet(self) -> Dict[str, Any]:
+ return {
+ "command-chain": ["snap/command-chain/desktop-launch"],
+ "plugs": ["desktop", "desktop-legacy", "opengl", "wayland", "x11"],
+ }
+
+ @functools.cached_property
+ def kde_snaps(self) -> KDESnaps:
+ """Return the KDE related snaps to use to construct the environment."""
+ base = self.yaml_data["base"]
+ sdk_snap = _SDK_SNAP[base]
+
+ build_snaps: List[str] = []
+ for part in self.yaml_data["parts"].values():
+ build_snaps.extend(part.get("build-snaps", []))
+
+ matcher = re.compile(r"kde-frameworks-\d+-qt-\d+" + base + r"sd.*")
+ sdk_snap_candidates = [s for s in build_snaps if matcher.match(s)]
+ if sdk_snap_candidates:
+ sdk_snap = sdk_snap_candidates[0].split("/")[0]
+ builtin = False
+ else:
+ builtin = True
+ # The same except the trailing -sdk
+ content = sdk_snap[:-3]
+
+ return KDESnaps(sdk=sdk_snap, content=content, builtin=builtin)
+
+ @functools.cached_property
+ def ext_info(self) -> ExtensionInfo:
+ """Return the extension info cmake_args, provider, content, build_snaps."""
+ cmake_args = "-DCMAKE_FIND_ROOT_PATH=/snap/" + self.kde_snaps.sdk + "/current"
+
+ return ExtensionInfo(cmake_args=cmake_args)
+
+ @overrides
+ def get_root_snippet(self) -> Dict[str, Any]:
+ platform_snap = self.kde_snaps.content
+ content_snap = self.kde_snaps.content + "-all"
+
+ return {
+ "assumes": ["snapd2.43"], # for 'snapctl is-connected'
+ "compression": "lzo",
+ "plugs": {
+ "desktop": {"mount-host-font-cache": False},
+ "icon-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/icons",
+ "default-provider": "gtk-common-themes",
+ },
+ "sound-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/sounds",
+ "default-provider": "gtk-common-themes",
+ },
+ platform_snap: {
+ "content": content_snap,
+ "interface": "content",
+ "default-provider": platform_snap,
+ "target": "$SNAP/kf5",
+ },
+ },
+ "environment": {"SNAP_DESKTOP_RUNTIME": "$SNAP/kf5"},
+ "hooks": {
+ "configure": {
+ "plugs": ["desktop"],
+ "command-chain": ["snap/command-chain/hooks-configure-desktop"],
+ }
+ },
+ "layout": {"/usr/share/X11": {"symlink": "$SNAP/kf5/usr/share/X11"}},
+ }
+
+ @overrides
+ def get_part_snippet(self) -> Dict[str, Any]:
+ sdk_snap = self.kde_snaps.sdk
+ cmake_args = self.ext_info.cmake_args
+
+ return {
+ "build-environment": [
+ {
+ "PATH": prepend_to_env(
+ "PATH", [f"/snap/{sdk_snap}/current/usr/bin"]
+ ),
+ },
+ {
+ "XDG_DATA_DIRS": prepend_to_env(
+ "XDG_DATA_DIRS",
+ [
+ f"$SNAPCRAFT_STAGE/usr/share:/snap/{sdk_snap}/current/usr/share",
+ "/usr/share",
+ ],
+ ),
+ },
+ {
+ "LD_LIBRARY_PATH": prepend_to_env(
+ "LD_LIBRARY_PATH",
+ [
+ f"/snap/{sdk_snap}/current/lib/$CRAFT_ARCH_TRIPLET",
+ f"/snap/{sdk_snap}/current/usr/lib/$CRAFT_ARCH_TRIPLET",
+ f"/snap/{sdk_snap}/current/usr/lib",
+ f"/snap/{sdk_snap}/current/usr/lib/vala-current",
+ f"/snap/{sdk_snap}/current/usr/lib/$CRAFT_ARCH_TRIPLET/pulseaudio",
+ ],
+ ),
+ },
+ {
+ "PKG_CONFIG_PATH": prepend_to_env(
+ "PKG_CONFIG_PATH",
+ [
+ f"/snap/{sdk_snap}/current/usr/lib/$CRAFT_ARCH_TRIPLET/pkgconfig",
+ f"/snap/{sdk_snap}/current/usr/lib/pkgconfig",
+ f"/snap/{sdk_snap}/current/usr/share/pkgconfig",
+ ],
+ ),
+ },
+ {
+ "ACLOCAL_PATH": prepend_to_env(
+ "ACLOCAL_PATH",
+ [
+ f"/snap/{sdk_snap}/current/usr/share/aclocal",
+ ],
+ ),
+ },
+ {
+ "SNAPCRAFT_CMAKE_ARGS": prepend_to_env(
+ "SNAPCRAFT_CMAKE_ARGS",
+ [
+ cmake_args,
+ ],
+ ),
+ },
+ ],
+ }
+
+ @overrides
+ def get_parts_snippet(self) -> Dict[str, Any]:
+ source = get_extensions_data_dir() / "desktop" / "command-chain"
+
+ if self.kde_snaps.builtin:
+ base = self.yaml_data["base"]
+ sdk_snap = _SDK_SNAP[base]
+ provider = self.kde_snaps.content
+ return {
+ "kde-neon-extension/sdk": {
+ "source": str(source),
+ "source-subdir": "kde-neon",
+ "plugin": "make",
+ "make-parameters": [f"PLATFORM_PLUG={provider}"],
+ "build-packages": ["g++"],
+ "build-snaps": [f"{sdk_snap}/current/stable"],
+ }
+ }
+
+ return {
+ "kde-neon-extension/sdk": {
+ "source": str(source),
+ "plugin": "make",
+ }
+ }
diff --git a/snapcraft/extensions/registry.py b/snapcraft/extensions/registry.py
index 425fc17154..77ddb6ebe4 100644
--- a/snapcraft/extensions/registry.py
+++ b/snapcraft/extensions/registry.py
@@ -21,6 +21,7 @@
from snapcraft import errors
from .gnome import GNOME
+from .kde_neon import KDENeon
from .ros2_humble import ROS2HumbleExtension
if TYPE_CHECKING:
@@ -31,6 +32,7 @@
_EXTENSIONS: Dict[str, "ExtensionType"] = {
"gnome": GNOME,
"ros2-humble": ROS2HumbleExtension,
+ "kde-neon": KDENeon,
}
diff --git a/snapcraft/extensions/ros2_humble.py b/snapcraft/extensions/ros2_humble.py
index 70982d92e8..67c3de6d4e 100644
--- a/snapcraft/extensions/ros2_humble.py
+++ b/snapcraft/extensions/ros2_humble.py
@@ -45,7 +45,7 @@ def get_supported_confinement() -> Tuple[str, ...]:
@staticmethod
@overrides
def is_experimental(base: Optional[str]) -> bool:
- return True
+ return False
@overrides
def get_root_snippet(self) -> Dict[str, Any]:
diff --git a/snapcraft/linters/classic_linter.py b/snapcraft/linters/classic_linter.py
index 218aa80676..a2a6c3136e 100644
--- a/snapcraft/linters/classic_linter.py
+++ b/snapcraft/linters/classic_linter.py
@@ -25,6 +25,8 @@
from .base import Linter, LinterIssue, LinterResult
+_HELP_URL = "https://snapcraft.io/docs/linters-classic"
+
class ClassicLinter(Linter):
"""Linter for classic snaps."""
@@ -74,14 +76,13 @@ def run(self) -> List[LinterIssue]:
elf_files = elf_utils.get_elf_files(current_path)
patcher = Patcher(dynamic_linker=linker, root_path=current_path.absolute())
soname_cache = SonameCache()
+ arch_triplet = elf_utils.get_arch_triplet()
for elf_file in elf_files:
# Skip linting files listed in the ignore list.
if self._is_file_ignored(elf_file):
continue
- arch_triplet = elf_utils.get_arch_triplet()
-
elf_file.load_dependencies(
root_path=current_path.absolute(),
base_path=installed_base_path,
@@ -105,6 +106,7 @@ def _check_elf_interpreter(
result=LinterResult.WARNING,
filename=str(elf_file.path),
text=f"ELF interpreter should be set to {linker!r}.",
+ url=_HELP_URL,
)
issues.append(issue)
@@ -126,5 +128,6 @@ def _check_elf_rpath(
result=LinterResult.WARNING,
filename=str(elf_file.path),
text=f"ELF rpath should be set to {formatted_rpath!r}.",
+ url=_HELP_URL,
)
issues.append(issue)
diff --git a/snapcraft/linters/library_linter.py b/snapcraft/linters/library_linter.py
index bcf49471f8..922cccb1e1 100644
--- a/snapcraft/linters/library_linter.py
+++ b/snapcraft/linters/library_linter.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright 2022 Canonical Ltd.
+# Copyright 2022-2023 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
@@ -16,8 +16,8 @@
"""Library linter implementation."""
-from pathlib import Path, PurePath
-from typing import List
+from pathlib import Path
+from typing import List, Set
from craft_cli import emit
from overrides import overrides
@@ -45,6 +45,8 @@ def run(self) -> List[LinterIssue]:
issues: List[LinterIssue] = []
elf_files = elf_utils.get_elf_files(current_path)
soname_cache = SonameCache()
+ all_libraries: Set[Path] = set()
+ used_libraries: Set[Path] = set()
for elf_file in elf_files:
# Skip linting files listed in the ignore list.
@@ -62,6 +64,20 @@ def run(self) -> List[LinterIssue]:
soname_cache=soname_cache,
)
+ # collect paths to local libraries used by the elf file
+ for dependency in dependencies:
+ if (
+ self._is_library_path(path=dependency)
+ and current_path.resolve() in dependency.resolve().parents
+ ):
+ # resolve symlinks to libraries
+ used_libraries.add(dependency.resolve())
+
+ # if the elf file is a library, add it to the list of all libraries
+ if elf_file.soname and self._is_library_path(elf_file.path):
+ # resolve symlinks to libraries
+ all_libraries.add(elf_file.path.resolve())
+
search_paths = [current_path.absolute(), *content_dirs]
if installed_base_path:
search_paths.append(installed_base_path)
@@ -73,6 +89,8 @@ def run(self) -> List[LinterIssue]:
issues=issues,
)
+ issues.extend(self._get_unused_library_issues(all_libraries, used_libraries))
+
return issues
def _check_dependencies_satisfied(
@@ -80,7 +98,7 @@ def _check_dependencies_satisfied(
elf_file: ElfFile,
*,
search_paths: List[Path],
- dependencies: List[str],
+ dependencies: List[Path],
issues: List[LinterIssue],
) -> None:
"""Check if ELF executable dependencies are satisfied by snap files.
@@ -98,20 +116,92 @@ def _check_dependencies_satisfied(
emit.debug(f"dynamic linker name is: {linker_name!r}")
for dependency in dependencies:
- dependency_path = PurePath(dependency)
-
# the dynamic linker is not a regular library
- if linker_name == dependency_path.name:
+ if linker_name == dependency.name:
continue
for path in search_paths:
- if path in dependency_path.parents:
+ if path in dependency.parents:
break
else:
issue = LinterIssue(
name=self._name,
result=LinterResult.WARNING,
filename=str(elf_file.path),
- text=f"missing dependency {dependency_path.name!r}.",
+ text=f"missing dependency {dependency.name!r}.",
+ url="https://snapcraft.io/docs/linters-library",
)
issues.append(issue)
+
+ def _get_unused_library_issues(
+ self, all_libraries: Set[Path], used_libraries: Set[Path]
+ ) -> List[LinterIssue]:
+ """Get a list of unused library issues.
+
+ :param all_libraries: a set of paths to all libraries
+ :param used_libraries: a set of libraries used by elf files in the snap
+
+ :returns: list of LinterIssues for unused libraries
+ """
+ issues: List[LinterIssue] = []
+ unused_libraries = all_libraries - used_libraries
+
+ # sort libraries so the results are ordered in a determistic way
+ for library_path in sorted(unused_libraries):
+ try:
+ # Resolving symlinks to a library will change the path from relative
+ # to absolute. To make it relative again, remove the current directory
+ # prefix from the path.
+ resolved_library_path = library_path.resolve().relative_to(Path.cwd())
+ except ValueError:
+ # A ValueError is not expected because these libraries should be within
+ # the current directory, but check anyways
+ emit.debug(f"could not resolve path for library {library_path!r}")
+ continue
+
+ library = ElfFile(path=resolved_library_path)
+
+ # skip linting files listed in the ignore list
+ if self._is_file_ignored(library):
+ continue
+
+ issue = LinterIssue(
+ name=self._name,
+ result=LinterResult.WARNING,
+ filename=library.soname,
+ text=f"unused library {str(library.path)!r}.",
+ url="https://snapcraft.io/docs/linters-library",
+ )
+ issues.append(issue)
+
+ return issues
+
+ def _is_library_path(self, path: Path) -> bool:
+ """Check if a file is in a library directory.
+
+ An elf file is considered a library if it has an soname and it is within a
+ library directory.
+ A library directory is a directory whose pathname ends in `lib/` or
+ `lib//` (i.e. lib/x86_64-linux-gnu/`)
+
+ :param path: filepath to check
+
+ :returns: True if the file is in a library directory. False if the path is not
+ a file or if it is not in a library directory.
+ """
+ # follow symlinks
+ path = path.resolve()
+
+ if not path.is_file():
+ return False
+
+ # TODO: get library directories from LD_LIBRARY_PATH and `ld --verbose`
+ # instead of matching against patterns
+ if path.match("lib/*") or path.match("lib32/*") or path.match("lib64/*"):
+ return True
+
+ for arch_triplet in elf_utils.get_all_arch_triplets():
+ if path.match(f"lib/{arch_triplet}/*"):
+ return True
+
+ return False
diff --git a/snapcraft/linters/linters.py b/snapcraft/linters/linters.py
index 1671b2f172..89be9193ee 100644
--- a/snapcraft/linters/linters.py
+++ b/snapcraft/linters/linters.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright 2022 Canonical Ltd.
+# Copyright 2022-2023 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
@@ -104,7 +104,7 @@ def report(
def _update_status(status: LinterStatus, result: LinterResult) -> LinterStatus:
- """Compute the consolitated status based on individual linter results."""
+ """Compute the consolidated status based on individual linter results."""
if result == LinterResult.FATAL:
status = LinterStatus.FATAL
elif result == LinterResult.ERROR and status != LinterStatus.FATAL:
@@ -161,7 +161,6 @@ def _ignore_matching_filenames(
return
for issue in issues:
-
files = lint.ignored_files(issue.name)
for pattern in files:
if (
diff --git a/snapcraft/meta/snap_yaml.py b/snapcraft/meta/snap_yaml.py
index a69e013580..40358f60df 100644
--- a/snapcraft/meta/snap_yaml.py
+++ b/snapcraft/meta/snap_yaml.py
@@ -201,6 +201,7 @@ class Config:
hooks: Optional[Dict[str, Any]]
layout: Optional[Dict[str, Dict[str, str]]]
system_usernames: Optional[Dict[str, Any]]
+ provenance: Optional[str]
@classmethod
def unmarshal(cls, data: Dict[str, Any]) -> "SnapMetadata":
@@ -388,6 +389,7 @@ def write(project: Project, prime_dir: Path, *, arch: str, arch_triplet: str):
hooks=project.hooks,
layout=project.layout,
system_usernames=project.system_usernames,
+ provenance=project.provenance,
)
if project.passthrough:
for name, value in project.passthrough.items():
diff --git a/snapcraft/os_release.py b/snapcraft/os_release.py
index cbf3bee9f9..692b02cb95 100644
--- a/snapcraft/os_release.py
+++ b/snapcraft/os_release.py
@@ -33,13 +33,19 @@
class OsRelease:
"""A class to intelligently determine the OS on which we're running."""
- def __init__(self, *, os_release_file: Path = Path("/etc/os-release")) -> None:
+ def __init__(
+ self,
+ *,
+ os_release_file: Path = Path( # noqa: B008 Function call in arg defaults
+ "/etc/os-release"
+ ),
+ ) -> None:
"""Create a new OsRelease instance.
:param str os_release_file: Path to os-release file to be parsed.
"""
with contextlib.suppress(FileNotFoundError):
- self._os_release = {} # type: Dict[str, str]
+ self._os_release: Dict[str, str] = {}
with os_release_file.open(encoding="utf-8") as release_file:
for line in release_file:
entry = line.rstrip().split("=")
diff --git a/snapcraft/parts/lifecycle.py b/snapcraft/parts/lifecycle.py
index d076dc1fbb..b175df4c35 100644
--- a/snapcraft/parts/lifecycle.py
+++ b/snapcraft/parts/lifecycle.py
@@ -27,9 +27,12 @@
import craft_parts
from craft_cli import emit
-from craft_parts import ProjectInfo, StepInfo, callbacks
+from craft_parts import ProjectInfo, Step, StepInfo, callbacks
+from craft_providers import Executor
from snapcraft import errors, extensions, linters, pack, providers, ua_manager, utils
+from snapcraft.elf import Patcher, SonameCache, elf_utils
+from snapcraft.elf import errors as elf_errors
from snapcraft.linters import LinterStatus
from snapcraft.meta import manifest, snap_yaml
from snapcraft.projects import (
@@ -95,7 +98,10 @@ def apply_yaml(
The architectures data is reduced to architectures in the current build plan.
:param yaml_data: The project YAML data.
+ :param build_on: Architecture the snap project will be built on.
:param build_for: Target architecture the snap project will be built to.
+
+ :return: A dictionary of yaml data with snapcraft logic applied.
"""
# validate project grammar
GrammarAwareProject.validate_grammar(yaml_data)
@@ -142,7 +148,7 @@ def process_yaml(project_file: Path) -> Dict[str, Any]:
return yaml_data
-def _extract_parse_info(yaml_data: Dict[str, Any]) -> Dict[str, List[str]]:
+def extract_parse_info(yaml_data: Dict[str, Any]) -> Dict[str, List[str]]:
"""Remove parse-info data from parts.
:param yaml_data: The project YAML data.
@@ -191,13 +197,14 @@ def run(command_name: str, parsed_args: "argparse.Namespace") -> None:
# Register our own callbacks
callbacks.register_prologue(_set_global_environment)
callbacks.register_pre_step(_set_step_environment)
+ callbacks.register_post_step(_patch_elf, step_list=[Step.PRIME])
build_count = utils.get_parallel_build_count()
for build_on, build_for in build_plan:
emit.verbose(f"Running on {build_on} for {build_for}")
yaml_data_for_arch = apply_yaml(yaml_data, build_on, build_for)
- parse_info = _extract_parse_info(yaml_data_for_arch)
+ parse_info = extract_parse_info(yaml_data_for_arch)
_expand_environment(
yaml_data_for_arch,
parallel_build_count=build_count,
@@ -255,7 +262,9 @@ def _run_command(
else:
work_dir = project_dir = Path.cwd()
- step_name = "prime" if command_name in ("pack", "snap") else command_name
+ step_name = "prime" if command_name in ("pack", "snap", "try") else command_name
+
+ track_stage_packages = getattr(parsed_args, "enable_manifest", False)
lifecycle = PartsLifecycle(
project.parts,
@@ -274,6 +283,7 @@ def _run_command(
},
extra_build_snaps=project.get_extra_build_snaps(),
target_arch=project.get_build_for(),
+ track_stage_packages=track_stage_packages,
)
if command_name == "clean":
@@ -534,20 +544,15 @@ def _run_in_provider(
build_base=build_base.value,
instance_name=instance_name,
) as instance:
- # mount project
- instance.mount(
- host_source=project_path,
- target=utils.get_managed_environment_project_path(),
- )
-
- # mount ssh directory
- if parsed_args.bind_ssh:
- instance.mount(
- host_source=Path.home() / ".ssh",
- target=utils.get_managed_environment_home_path() / ".ssh",
- )
try:
+ providers.prepare_instance(
+ instance=instance,
+ host_project_path=project_path,
+ bind_ssh=parsed_args.bind_ssh,
+ )
with emit.pause():
+ if command_name == "try":
+ _expose_prime(project_path, instance)
# run snapcraft inside the instance
instance.execute_run(cmd, check=True, cwd=output_dir)
except subprocess.CalledProcessError as err:
@@ -562,6 +567,17 @@ def _run_in_provider(
providers.capture_logs_from_instance(instance)
+def _expose_prime(project_path: Path, instance: Executor):
+ """Expose the instance's prime directory in ``project_path`` on the host."""
+ host_prime = project_path / "prime"
+ host_prime.mkdir(exist_ok=True)
+
+ managed_root = utils.get_managed_environment_home_path()
+ dirs = craft_parts.ProjectDirs(work_dir=managed_root)
+
+ instance.mount(host_source=project_path / "prime", target=dirs.prime_dir)
+
+
def _set_global_environment(info: ProjectInfo) -> None:
"""Set global environment variables."""
info.global_environment.update(
@@ -593,6 +609,53 @@ def _set_step_environment(step_info: StepInfo) -> bool:
return True
+def _patch_elf(step_info: StepInfo) -> bool:
+ """Patch rpath and interpreter in ELF files for classic mode."""
+ if "enable-patchelf" not in step_info.build_attributes:
+ emit.debug(f"patch_elf: not enabled for part {step_info.part_name!r}")
+ return True
+
+ if not step_info.state:
+ emit.debug("patch_elf: no state information")
+ return True
+
+ try:
+ # If libc is staged we'll find a dynamic linker in the payload. At
+ # runtime the linker will be in the installed snap path.
+ linker = elf_utils.get_dynamic_linker(
+ root_path=step_info.prime_dir,
+ snap_path=Path(f"/snap/{step_info.project_name}/current"),
+ )
+ except elf_errors.DynamicLinkerNotFound:
+ # Otherwise look for the host linker, which should match the base
+ # system linker. At runtime use the linker from the installed base
+ # snap.
+ linker = elf_utils.get_dynamic_linker(
+ root_path=Path("/"), snap_path=Path(f"/snap/{step_info.base}/current")
+ )
+
+ migrated_files = step_info.state.files
+ patcher = Patcher(dynamic_linker=linker, root_path=step_info.prime_dir)
+ elf_files = elf_utils.get_elf_files_from_list(step_info.prime_dir, migrated_files)
+ soname_cache = SonameCache()
+ arch_triplet = elf_utils.get_arch_triplet()
+
+ for elf_file in elf_files:
+ elf_file.load_dependencies(
+ root_path=step_info.prime_dir,
+ base_path=Path(f"/snap/{step_info.base}/current"),
+ content_dirs=[], # classic snaps don't use content providers
+ arch_triplet=arch_triplet,
+ soname_cache=soname_cache,
+ )
+
+ relative_path = elf_file.path.relative_to(step_info.prime_dir)
+ emit.progress(f"Patch ELF file: {str(relative_path)!r}")
+ patcher.patch(elf_file=elf_file)
+
+ return True
+
+
def _expand_environment(
snapcraft_yaml: Dict[str, Any], *, parallel_build_count: int, target_arch: str
) -> None:
diff --git a/snapcraft/parts/parts.py b/snapcraft/parts/parts.py
index 5247426185..e4fac59d50 100644
--- a/snapcraft/parts/parts.py
+++ b/snapcraft/parts/parts.py
@@ -67,6 +67,7 @@ def __init__(
project_vars: Dict[str, str],
extra_build_snaps: Optional[List[str]] = None,
target_arch: str,
+ track_stage_packages: bool,
):
self._work_dir = work_dir
self._assets_dir = assets_dir
@@ -96,6 +97,7 @@ def __init__(
base=base,
ignore_local_sources=["*.snap"],
extra_build_snaps=extra_build_snaps,
+ track_stage_packages=track_stage_packages,
parallel_build_count=parallel_build_count,
project_name=project_name,
project_vars_part_name=adopt_info,
diff --git a/snapcraft/parts/plugins/__init__.py b/snapcraft/parts/plugins/__init__.py
index f8f4e88123..179c40aa70 100644
--- a/snapcraft/parts/plugins/__init__.py
+++ b/snapcraft/parts/plugins/__init__.py
@@ -19,6 +19,7 @@
from .colcon import ColconPlugin
from .conda_plugin import CondaPlugin
+from .flutter_plugin import FlutterPlugin
from .register import register
-__all__ = ["ColconPlugin", "CondaPlugin", "register"]
+__all__ = ["ColconPlugin", "CondaPlugin", "FlutterPlugin", "register"]
diff --git a/snapcraft/parts/plugins/colcon.py b/snapcraft/parts/plugins/colcon.py
index 1adb04073e..45cac24029 100644
--- a/snapcraft/parts/plugins/colcon.py
+++ b/snapcraft/parts/plugins/colcon.py
@@ -135,8 +135,13 @@ def _get_workspace_activation_commands(self) -> List[str]:
'state="$(set +o); set -$-"',
"set +u",
# If it exists, source the stage-snap underlay
- 'if [ -f "${CRAFT_PART_INSTALL}/opt/ros/snap/setup.sh" ]; then',
- 'COLCON_CURRENT_PREFIX="{path}" . "{path}/setup.sh"'.format(
+ 'if [ -f "${CRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}/local_setup.sh" ]; then',
+ 'COLCON_CURRENT_PREFIX="{path}" . "{path}/local_setup.sh"'.format(
+ path="${CRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}"
+ ),
+ "fi",
+ 'if [ -f "${CRAFT_PART_INSTALL}/opt/ros/snap/local_setup.sh" ]; then',
+ 'COLCON_CURRENT_PREFIX="{path}" . "{path}/local_setup.sh"'.format(
path="${CRAFT_PART_INSTALL}/opt/ros/snap"
),
"fi",
diff --git a/snapcraft/parts/plugins/flutter_plugin.py b/snapcraft/parts/plugins/flutter_plugin.py
new file mode 100644
index 0000000000..894ec25c18
--- /dev/null
+++ b/snapcraft/parts/plugins/flutter_plugin.py
@@ -0,0 +1,120 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2023 Canonical Ltd.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 3 as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""The flutter plugin."""
+from typing import Any, Dict, List, Literal, Set, cast
+
+from craft_parts import infos, plugins
+from overrides import overrides
+
+FLUTTER_REPO = "https://github.com/flutter/flutter.git"
+"""The repository where the flutter SDK resides."""
+
+
+class FlutterPluginProperties(plugins.PluginProperties, plugins.PluginModel):
+ """The part properties used by the flutter plugin."""
+
+ source: str
+ flutter_channel: Literal["stable", "master", "beta"] = "stable"
+ flutter_target: str = "lib/main.dart"
+
+ @classmethod
+ @overrides
+ def unmarshal(cls, data: Dict[str, Any]) -> "FlutterPluginProperties":
+ """Populate class attributes from the part specification.
+
+ :param data: A dictionary containing part properties.
+
+ :return: The populated plugin properties data object.
+
+ :raise pydantic.ValidationError: If validation fails.
+ """
+ plugin_data = plugins.extract_plugin_properties(
+ data,
+ plugin_name="flutter",
+ required=["source"],
+ )
+ return cls(**plugin_data)
+
+
+class FlutterPlugin(plugins.Plugin):
+ """A plugin for flutter projects.
+
+ This plugin uses the common plugin keywords as well as those for "sources".
+ For more information check the 'plugins' topic for the former and the
+ 'sources' topic for the latter.
+
+ Additionally, this plugin uses the following plugin-specific keywords:
+ - flutter-channel
+ (enum: [stable, master, beta], default: stable)
+ The default flutter channel to use for the build.
+ - flutter-target
+ (str, default: lib/main.dart)
+ The flutter target to build.
+ """
+
+ properties_class = FlutterPluginProperties
+
+ def __init__(
+ self, *, properties: plugins.PluginProperties, part_info: infos.PartInfo
+ ) -> None:
+ super().__init__(properties=properties, part_info=part_info)
+
+ self.flutter_dir = part_info.part_build_dir / "flutter-distro"
+
+ @overrides
+ def get_build_snaps(self) -> Set[str]:
+ return set()
+
+ @overrides
+ def get_build_packages(self) -> Set[str]:
+ return {
+ "clang",
+ "git",
+ "cmake",
+ "ninja-build",
+ "unzip",
+ }
+
+ @overrides
+ def get_build_environment(self) -> Dict[str, str]:
+ return {
+ "PATH": f"{self.flutter_dir / 'bin'}:${{PATH}}",
+ }
+
+ def _get_setup_flutter(self, options) -> List[str]:
+ # TODO move to pull
+ return [
+ # TODO detect changes to plugin properties
+ f"git clone --depth 1 -b {options.flutter_channel} {FLUTTER_REPO} {self.flutter_dir}",
+ "flutter precache --linux",
+ "flutter pub get",
+ ]
+
+ @overrides
+ def get_build_commands(self) -> List[str]:
+ options = cast(FlutterPluginProperties, self._options)
+
+ flutter_install_cmd: List[str] = []
+
+ if not self.flutter_dir.exists():
+ flutter_install_cmd = self._get_setup_flutter(options)
+
+ flutter_build_cmd = [
+ f"flutter build linux --release --verbose --target {options.flutter_target}",
+ "cp -r build/linux/*/release/bundle/* $CRAFT_PART_INSTALL/",
+ ]
+ return flutter_install_cmd + flutter_build_cmd
diff --git a/snapcraft/parts/plugins/register.py b/snapcraft/parts/plugins/register.py
index 3c213bd135..1e875f8e31 100644
--- a/snapcraft/parts/plugins/register.py
+++ b/snapcraft/parts/plugins/register.py
@@ -20,9 +20,11 @@
from .colcon import ColconPlugin
from .conda_plugin import CondaPlugin
+from .flutter_plugin import FlutterPlugin
def register() -> None:
"""Register Snapcraft plugins."""
craft_parts.plugins.register({"colcon": ColconPlugin})
craft_parts.plugins.register({"conda": CondaPlugin})
+ craft_parts.plugins.register({"flutter": FlutterPlugin})
diff --git a/snapcraft/parts/yaml_utils.py b/snapcraft/parts/yaml_utils.py
index 95599416bd..89104f95d9 100644
--- a/snapcraft/parts/yaml_utils.py
+++ b/snapcraft/parts/yaml_utils.py
@@ -96,6 +96,9 @@ def load(filestream: TextIO) -> Dict[str, Any]:
filestream.seek(0)
try:
- return yaml.load(filestream, Loader=_SafeLoader)
+ return yaml.load(
+ filestream,
+ Loader=_SafeLoader, # noqa: S506 Probable unsafe use of yaml.load()
+ )
except yaml.error.YAMLError as err:
raise errors.SnapcraftError(f"snapcraft.yaml parsing error: {err!s}") from err
diff --git a/snapcraft/projects.py b/snapcraft/projects.py
index 23c6638612..d47de55ff0 100644
--- a/snapcraft/projects.py
+++ b/snapcraft/projects.py
@@ -423,6 +423,7 @@ class Project(ProjectModel):
build_packages: Optional[GrammarStrList]
build_snaps: Optional[GrammarStrList]
ua_services: Optional[UniqueStrList]
+ provenance: Optional[str]
@pydantic.validator("plugs")
@classmethod
@@ -547,6 +548,16 @@ def _validate_architecture_data(cls, architectures):
"""Validate architecture data."""
return _validate_architectures(architectures)
+ @pydantic.validator("provenance")
+ @classmethod
+ def _validate_provenance(cls, provenance):
+ if provenance and not re.match(r"^[a-zA-Z0-9-]+$", provenance):
+ raise ValueError(
+ "provenance must consist of alphanumeric characters and/or hyphens."
+ )
+
+ return provenance
+
@classmethod
def unmarshal(cls, data: Dict[str, Any]) -> "Project":
"""Create and populate a new ``Project`` object from dictionary data.
diff --git a/snapcraft/providers.py b/snapcraft/providers.py
index 415477815e..dcd7b36118 100644
--- a/snapcraft/providers.py
+++ b/snapcraft/providers.py
@@ -15,10 +15,11 @@
# along with this program. If not, see .
"""Snapcraft-specific code to interface with craft-providers."""
-
+import io
import os
import sys
from pathlib import Path
+from textwrap import dedent
from typing import Dict, Optional
from craft_cli import emit
@@ -29,7 +30,9 @@
from snapcraft.snap_config import get_snap_config
from snapcraft.utils import (
confirm_with_user,
+ get_managed_environment_home_path,
get_managed_environment_log_path,
+ get_managed_environment_project_path,
get_managed_environment_snap_channel,
)
@@ -39,6 +42,66 @@
"core22": bases.BuilddBaseAlias.JAMMY,
}
+# TODO: move to a package data file for shellcheck and syntax highlighting
+# pylint: disable=line-too-long
+BASHRC = dedent(
+ """\
+ #!/bin/bash
+
+ env_file="$HOME/environment.sh"
+
+ # save default environment on first login
+ if [[ ! -e $env_file ]]; then
+ env > "$env_file"
+ sed -i 's/^/export /' "$env_file" # prefix 'export' to variables
+ sed -i 's/=/="/; s/$/"/' "$env_file" # surround values with quotes
+ sed -i '1i#! /bin/bash\\n' "$env_file" # add shebang
+ fi
+ previous_pwd=$PWD
+
+ function set_environment {
+ # only update the environment when the directory changes
+ if [[ ! $PWD = "$previous_pwd" ]]; then
+ # set part's environment when inside a part's build directory
+ if [[ "$PWD" =~ $HOME/parts/.*/build ]] && [[ -e "${PWD/build*/run/environment.sh}" ]] ; then
+ part_name=$(echo "${PWD#$"HOME"}" | cut -d "/" -f 3)
+ echo "build environment set for part '$part_name'"
+ # shellcheck disable=SC1090
+ source "${PWD/build*/run/environment.sh}"
+
+ # else clear and set the default environment
+ else
+ # shellcheck disable=SC2046
+ unset $(/usr/bin/env | /usr/bin/cut -d= -f1)
+ # shellcheck disable=SC1090
+ source "$env_file"
+ PWD="$(pwd)"
+ export PWD
+ fi
+ fi
+ previous_pwd=$PWD
+ }
+
+ function set_prompt {
+ # do not show path in HOME directory
+ if [[ "$PWD" = "$HOME" ]]; then
+ export PS1="\\h # "
+
+ # show relative path inside a subdirectory of HOME
+ elif [[ "$PWD" =~ ^$HOME/* ]]; then
+ export PS1="\\h ..${PWD/$HOME/}# "
+
+ # show full path outside the home directory
+ else
+ export PS1="\\h $PWD# "
+ fi
+ }
+
+ PROMPT_COMMAND="set_environment; set_prompt"
+ """
+)
+# pylint: enable=line-too-long
+
def capture_logs_from_instance(instance: executor.Executor) -> None:
"""Capture and emit snapcraft logs from an instance.
@@ -153,6 +216,7 @@ def get_command_environment(
"SNAPCRAFT_BUILD_FOR",
"SNAPCRAFT_BUILD_INFO",
"SNAPCRAFT_IMAGE_INFO",
+ "SNAPCRAFT_MAX_PARALLEL_BUILD_COUNT",
]:
if env_key in os.environ:
env[env_key] = os.environ[env_key]
@@ -246,3 +310,33 @@ def get_provider(provider: Optional[str] = None) -> Provider:
return MultipassProvider()
raise ValueError(f"unsupported provider specified: {chosen_provider!r}")
+
+
+def prepare_instance(
+ instance: executor.Executor, host_project_path: Path, bind_ssh: bool
+) -> None:
+ """Prepare an instance to run snapcraft.
+
+ The preparation includes:
+ - mounting the project directory
+ - mounting the `.ssh` directory, if specified
+ - setting up `.bashrc` and the command line prompt
+ """
+ # mount project
+ instance.mount(
+ host_source=host_project_path,
+ target=get_managed_environment_project_path(),
+ )
+
+ # mount ssh directory
+ if bind_ssh:
+ instance.mount(
+ host_source=Path.home() / ".ssh",
+ target=get_managed_environment_home_path() / ".ssh",
+ )
+
+ instance.push_file_io(
+ destination=Path("/root/.bashrc"),
+ content=io.BytesIO(BASHRC.encode()),
+ file_mode="644",
+ )
diff --git a/snapcraft/repo/apt_key_manager.py b/snapcraft/repo/apt_key_manager.py
index 75ac90a350..f95965f16a 100644
--- a/snapcraft/repo/apt_key_manager.py
+++ b/snapcraft/repo/apt_key_manager.py
@@ -33,7 +33,7 @@ class AptKeyManager:
def __init__(
self,
*,
- gpg_keyring: pathlib.Path = pathlib.Path(
+ gpg_keyring: pathlib.Path = pathlib.Path( # noqa: B008 Function call in arg defaults
"/etc/apt/trusted.gpg.d/snapcraft.gpg"
),
key_assets: pathlib.Path,
diff --git a/snapcraft/repo/apt_sources_manager.py b/snapcraft/repo/apt_sources_manager.py
index a0a60f07e9..b5556a9a7b 100644
--- a/snapcraft/repo/apt_sources_manager.py
+++ b/snapcraft/repo/apt_sources_manager.py
@@ -75,7 +75,9 @@ class AptSourcesManager:
def __init__(
self,
*,
- sources_list_d: pathlib.Path = pathlib.Path("/etc/apt/sources.list.d"),
+ sources_list_d: pathlib.Path = pathlib.Path( # noqa: B008 Function call in arg defaults
+ "/etc/apt/sources.list.d"
+ ),
) -> None:
self._sources_list_d = sources_list_d
diff --git a/snapcraft/store/client.py b/snapcraft/store/client.py
index 411764eca0..a005cd1d53 100644
--- a/snapcraft/store/client.py
+++ b/snapcraft/store/client.py
@@ -47,7 +47,8 @@
def build_user_agent(
- version=__version__, os_platform: utils.OSPlatform = utils.get_os_platform()
+ version=__version__,
+ os_platform: utils.OSPlatform = utils.get_os_platform(), # noqa: B008
):
"""Build Snapcraft's user agent."""
if any(
@@ -99,7 +100,11 @@ def _prompt_login() -> Tuple[str, str]:
return (email, password)
-def _get_hostname(hostname: Optional[str] = platform.node()) -> str:
+def _get_hostname(
+ hostname: Optional[
+ str
+ ] = platform.node(), # noqa: B008 Function call in arg defaults
+) -> str:
"""Return the computer's network name or UNNKOWN if it cannot be determined."""
if not hostname:
hostname = "UNKNOWN"
@@ -177,7 +182,7 @@ def __init__(self, ephemeral=False):
def login(
self,
*,
- ttl: int = int(timedelta(days=365).total_seconds()),
+ ttl: int = int(timedelta(days=365).total_seconds()), # noqa: B008
acls: Optional[Sequence[str]] = None,
packages: Optional[Sequence[str]] = None,
channels: Optional[Sequence[str]] = None,
diff --git a/snapcraft/store/onprem_client.py b/snapcraft/store/onprem_client.py
index 5b5fc55bd1..fed601f3ba 100644
--- a/snapcraft/store/onprem_client.py
+++ b/snapcraft/store/onprem_client.py
@@ -30,8 +30,8 @@
ON_PREM_ENDPOINTS: Final = endpoints.Endpoints(
namespace="snap",
whoami="/v1/tokens/whoami",
- tokens="",
- tokens_exchange="/v1/tokens/offline/exchange",
+ tokens="", # noqa: S106 Possible hardcoded password
+ tokens_exchange="/v1/tokens/offline/exchange", # noqa: S106
valid_package_types=["snap"],
list_releases_model=models.charm_list_releases_model.ListReleasesModel,
)
diff --git a/snapcraft/utils.py b/snapcraft/utils.py
index 57fd66140b..42b975bd0d 100644
--- a/snapcraft/utils.py
+++ b/snapcraft/utils.py
@@ -81,7 +81,11 @@ def __str__(self) -> str:
}
-def get_os_platform(filepath=pathlib.Path("/etc/os-release")):
+def get_os_platform(
+ filepath=pathlib.Path( # noqa: B008 Function call in arg defaults
+ "/etc/os-release"
+ ),
+):
"""Determine a system/release combo for an OS using /etc/os-release if available."""
system = platform.system()
release = platform.release()
@@ -172,7 +176,9 @@ def get_managed_environment_project_path():
def get_managed_environment_log_path():
"""Path for log when running in managed environment."""
- return pathlib.Path("/tmp/snapcraft.log")
+ return pathlib.Path(
+ "/tmp/snapcraft.log" # noqa: S108 Probable insecure use of temp file
+ )
def get_managed_environment_snap_channel() -> Optional[str]:
@@ -227,12 +233,13 @@ def get_parallel_build_count() -> int:
)
build_count = 1
+ max_count_env = os.environ.get("SNAPCRAFT_MAX_PARALLEL_BUILD_COUNT", "")
try:
- max_count = int(os.environ.get("SNAPCRAFT_MAX_PARALLEL_BUILD_COUNT", ""))
+ max_count = int(max_count_env)
if max_count > 0:
build_count = min(build_count, max_count)
except ValueError:
- emit.debug("Invalid SNAPCRAFT_MAX_PARALLEL_BUILD_COUNT value")
+ emit.debug(f"Invalid SNAPCRAFT_MAX_PARALLEL_BUILD_COUNT {max_count_env!r}")
return build_count
@@ -290,7 +297,10 @@ def prompt(prompt_text: str, *, hide: bool = False) -> str:
def humanize_list(
- items: Iterable[str], conjunction: str, item_format: str = "{!r}"
+ items: Iterable[str],
+ conjunction: str,
+ item_format: str = "{!r}",
+ sort: bool = True,
) -> str:
"""Format a list into a human-readable string.
@@ -298,11 +308,16 @@ def humanize_list(
:param conjunction: the conjunction used to join the final element to
the rest of the list (e.g. 'and').
:param item_format: format string to use per item.
+ :param sort: if true, sort the list.
"""
if not items:
return ""
- quoted_items = [item_format.format(item) for item in sorted(items)]
+ quoted_items = [item_format.format(item) for item in items]
+
+ if sort:
+ quoted_items = sorted(quoted_items)
+
if len(quoted_items) == 1:
return quoted_items[0]
diff --git a/snapcraft_legacy/cli/_options.py b/snapcraft_legacy/cli/_options.py
index adafc0df5d..a0e0ab653b 100644
--- a/snapcraft_legacy/cli/_options.py
+++ b/snapcraft_legacy/cli/_options.py
@@ -72,50 +72,50 @@ def __repr__(self):
_ALL_PROVIDERS = _SUPPORTED_PROVIDERS + _HIDDEN_PROVIDERS
_PROVIDER_OPTIONS: List[Dict[str, Any]] = [
dict(
- param_decls="--target-arch",
+ param_decls=["--target-arch"],
metavar="",
help="Target architecture to cross compile to",
supported_providers=["host", "lxd", "multipass"],
),
dict(
- param_decls="--debug",
+ param_decls=["--debug"],
is_flag=True,
help="Shells into the environment if the build fails.",
supported_providers=["host", "lxd", "managed-host", "multipass"],
),
dict(
- param_decls="--shell",
+ param_decls=["--shell"],
is_flag=True,
help="Shells into the environment in lieu of the step to run.",
supported_providers=["host", "lxd", "managed-host", "multipass"],
),
dict(
- param_decls="--offline",
+ param_decls=["--offline"],
is_flag=True,
help="Operate in offline mode.",
envvar="SNAPCRAFT_OFFLINE",
supported_providers=["host", "lxd", "managed-host", "multipass"],
),
dict(
- param_decls="--shell-after",
+ param_decls=["--shell-after"],
is_flag=True,
help="Shells into the environment after the step has run.",
supported_providers=["host", "lxd", "managed-host", "multipass"],
),
dict(
- param_decls="--destructive-mode",
+ param_decls=["--destructive-mode"],
is_flag=True,
help="Forces snapcraft to try and use the current host to build (implies `--provider=host`).",
supported_providers=["host", "managed-host"],
),
dict(
- param_decls="--use-lxd",
+ param_decls=["--use-lxd"],
is_flag=True,
help="Forces snapcraft to use LXD to build (implies `--provider=lxd`).",
supported_providers=["lxd"],
),
dict(
- param_decls="--provider",
+ param_decls=["--provider"],
envvar="SNAPCRAFT_BUILD_ENVIRONMENT",
show_envvar=False,
help="Build provider to use.",
@@ -124,21 +124,21 @@ def __repr__(self):
supported_providers=_ALL_PROVIDERS,
),
dict(
- param_decls="--http-proxy",
+ param_decls=["--http-proxy"],
metavar="",
help="HTTP proxy for host build environments.",
envvar="http_proxy",
supported_providers=["host", "lxd", "managed-host", "multipass"],
),
dict(
- param_decls="--https-proxy",
+ param_decls=["--https-proxy"],
metavar="",
help="HTTPS proxy for host build environments.",
envvar="https_proxy",
supported_providers=["host", "lxd", "managed-host", "multipass"],
),
dict(
- param_decls="--add-ca-certificates",
+ param_decls=["--add-ca-certificates"],
metavar="",
help="File or directory containing CA certificates to install into build environments.",
envvar="SNAPCRAFT_ADD_CA_CERTIFICATES",
@@ -148,14 +148,14 @@ def __repr__(self):
),
),
dict(
- param_decls="--bind-ssh",
+ param_decls=["--bind-ssh"],
is_flag=True,
help="Bind ~/.ssh directory to locally-run build environments.",
envvar="SNAPCRAFT_BIND_SSH",
supported_providers=["lxd", "multipass"],
),
dict(
- param_decls="--enable-developer-debug",
+ param_decls=["--enable-developer-debug"],
is_flag=True,
help="Enable developer debug logging.",
envvar="SNAPCRAFT_ENABLE_DEVELOPER_DEBUG",
@@ -163,7 +163,7 @@ def __repr__(self):
hidden=True,
),
dict(
- param_decls="--enable-manifest",
+ param_decls=["--enable-manifest"],
is_flag=True,
type=BoolParamType(),
help="Generate snap manifest.",
@@ -172,7 +172,7 @@ def __repr__(self):
hidden=True,
),
dict(
- param_decls="--manifest-image-information",
+ param_decls=["--manifest-image-information"],
metavar="",
help="Set snap manifest image-info",
envvar="SNAPCRAFT_IMAGE_INFO",
@@ -180,33 +180,39 @@ def __repr__(self):
hidden=True,
),
dict(
- param_decls="--enable-experimental-extensions",
+ param_decls=["--enable-experimental-extensions"],
is_flag=True,
help="Enable extensions that are experimental and not considered stable.",
envvar="SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS",
supported_providers=["host", "lxd", "managed-host", "multipass"],
),
dict(
- param_decls="--enable-experimental-target-arch",
+ param_decls=["--enable-experimental-target-arch"],
is_flag=True,
help="Enable experimental `--target-arch` support for core20.",
envvar="SNAPCRAFT_ENABLE_EXPERIMENTAL_TARGET_ARCH",
supported_providers=["host", "lxd", "managed-host", "multipass"],
),
dict(
- param_decls="--ua-token",
+ param_decls=["--ua-token"],
metavar="",
help="Configure build environment with ESM using specified UA token.",
envvar="SNAPCRAFT_UA_TOKEN",
supported_providers=["host", "lxd", "managed-host", "multipass"],
),
dict(
- param_decls="--enable-experimental-ua-services",
+ param_decls=["--enable-experimental-ua-services"],
is_flag=True,
help="Allow selection of UA services to enable.",
envvar="SNAPCRAFT_ENABLE_EXPERIMENTAL_UA_SERVICES",
supported_providers=["host", "lxd", "managed-host", "multipass"],
),
+ dict(
+ param_decls=["--verbose", "-v"],
+ is_flag=True,
+ help="Show debug information and be more verbose.",
+ supported_providers=["host", "lxd", "managed-host", "multipass"],
+ ),
]
@@ -222,7 +228,7 @@ def _add_options(options, func, hidden):
option.pop("supported_providers")
hidden_override = option.pop("hidden", hidden)
- click_option = click.option(param_decls, **option, hidden=hidden_override)
+ click_option = click.option(*param_decls, **option, hidden=hidden_override)
func = click_option(func)
return func
@@ -264,12 +270,13 @@ def _sanity_check_build_provider_flags(build_provider: str, **kwargs) -> None:
# change defaults, so they are safe to ignore due to filtering
# in get_build_provider_flags().
for option in _PROVIDER_OPTIONS:
- key: str = option["param_decls"] # type: ignore
+ keys: List[str] = option["param_decls"] # type: ignore
supported_providers: List[str] = option["supported_providers"] # type: ignore
- if key in sys.argv and build_provider not in supported_providers:
- raise click.BadArgumentUsage(
- f"{key} cannot be used with build provider {build_provider!r}"
- )
+ for key in keys:
+ if key in sys.argv and build_provider not in supported_providers:
+ raise click.BadArgumentUsage(
+ f"{key} cannot be used with build provider {build_provider!r}"
+ )
# Check if running as sudo but only if the host is not managed-host where Snapcraft
# runs as root already. This effectively avoids the warning when using the default
@@ -336,45 +343,61 @@ def get_build_provider(**kwargs) -> str:
def _param_decls_to_kwarg(key: str) -> str:
"""Format a param_decls to keyword argument name."""
-
- # Drop leading "--".
- key = key.replace("--", "", 1)
+ # Drop leading dashes ("-" or "--")
+ if key.startswith("--"):
+ key = key.replace("--", "", 1)
+ elif key.startswith("-"):
+ key = key.replace("-", "", 1)
# Convert dashes to underscores.
return key.replace("-", "_")
def get_build_provider_flags(build_provider: str, **kwargs) -> Dict[str, str]:
- """Get configured options applicable to build_provider."""
+ """Get provider options from kwargs for a build provider.
+
+ Boolean options that are false are not collected from kwargs.
+ Options without an environment variable are not collected from kwargs.
- build_provider_flags: Dict[str, str] = dict()
+ :param build_provider: Build provider to collect options for. Valid providers are
+ 'host', 'lxd', 'multipass', and 'managed-host'.
+ :param kwargs: Dictionary containing provider options.
+
+ :return: Dictionary of provider options with their environment variable as the key.
+
+ :raises RuntimeError: If build provider is invalid.
+ """
+ build_provider_flags: Dict[str, str] = {}
# Should not happen - developer safety check.
if build_provider not in _ALL_PROVIDERS:
raise RuntimeError(f"Invalid build provider: {build_provider}")
for option in _PROVIDER_OPTIONS:
- key: str = option["param_decls"] # type: ignore
+ keys: List[str] = option["param_decls"] # type: ignore
is_flag: bool = option.get("is_flag", False) # type: ignore
envvar: Optional[str] = option.get("envvar") # type: ignore
supported_providers: List[str] = option["supported_providers"] # type: ignore
- # Skip --provider option.
- if key == "--provider":
- continue
+ for key in keys:
+ # TODO: skip single character flags (e.g. keep `verbose` but discard `v`)
- # Skip options that do not apply to configured provider.
- if build_provider not in supported_providers:
- continue
+ # Skip --provider option.
+ if key == "--provider":
+ continue
- # Skip boolean flags that have not been set.
- key_formatted = _param_decls_to_kwarg(key)
- if is_flag and not kwargs.get(key_formatted):
- continue
+ # Skip options that do not apply to configured provider.
+ if build_provider not in supported_providers:
+ continue
+
+ # Skip boolean flags that have not been set.
+ key_formatted = _param_decls_to_kwarg(key)
+ if is_flag and not kwargs.get(key_formatted):
+ continue
- # Add build provider flag using envvar as key.
- if envvar is not None and key_formatted in kwargs:
- build_provider_flags[envvar] = kwargs[key_formatted]
+ # Add build provider flag using envvar as key.
+ if envvar is not None and key_formatted in kwargs:
+ build_provider_flags[envvar] = kwargs[key_formatted]
return build_provider_flags
diff --git a/snapcraft_legacy/cli/_runner.py b/snapcraft_legacy/cli/_runner.py
index 326cb43119..d01c4bb5cf 100644
--- a/snapcraft_legacy/cli/_runner.py
+++ b/snapcraft_legacy/cli/_runner.py
@@ -91,17 +91,26 @@ def configure_requests_ca() -> None:
@click.pass_context
@add_provider_options(hidden=True)
@click.option("--debug", "-d", is_flag=True)
+@click.option("--verbose", "-v", is_flag=True)
def run(ctx, debug, catch_exceptions=False, **kwargs):
"""Snapcraft is a delightful packaging tool."""
is_snapcraft_developer_debug = kwargs["enable_developer_debug"]
+ verbose = kwargs["verbose"]
+
if is_snapcraft_developer_debug:
+ if verbose:
+ raise click.BadArgumentUsage(
+ "The 'enable-developer-debug' and 'verbose' options are "
+ "mutually exclusive."
+ )
log_level = logging.DEBUG
click.echo(
"Starting snapcraft {} from {}.".format(
snapcraft_legacy.__version__, os.path.dirname(__file__)
)
)
+ # verbose is the default log level so no need to check if `--verbose` was passed
else:
log_level = logging.INFO
diff --git a/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py b/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py
index dc72c125fb..70af188f97 100644
--- a/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py
+++ b/snapcraft_legacy/internal/project_loader/_extensions/kde_neon.py
@@ -31,10 +31,10 @@
build_snaps=["kde-frameworks-5-core18-sdk/latest/stable"],
),
core20=_ExtensionInfo(
- cmake_args="-DCMAKE_FIND_ROOT_PATH=/snap/kde-frameworks-5-96-qt-5-15-5-core20-sdk/current",
- content="kde-frameworks-5-96-qt-5-15-5-core20-all",
- provider="kde-frameworks-5-96-qt-5-15-5-core20",
- build_snaps=["kde-frameworks-5-96-qt-5-15-5-core20-sdk/latest/stable"],
+ cmake_args="-DCMAKE_FIND_ROOT_PATH=/snap/kde-frameworks-5-99-qt-5-15-7-core20-sdk/current",
+ content="kde-frameworks-5-99-qt-5-15-7-core20-all",
+ provider="kde-frameworks-5-99-qt-5-15-7-core20",
+ build_snaps=["kde-frameworks-5-99-qt-5-15-7-core20-sdk/latest/stable"],
),
)
diff --git a/snapcraft_legacy/internal/project_loader/_extensions/ros1_noetic.py b/snapcraft_legacy/internal/project_loader/_extensions/ros1_noetic.py
index 3951a5d56d..1872c4cacb 100644
--- a/snapcraft_legacy/internal/project_loader/_extensions/ros1_noetic.py
+++ b/snapcraft_legacy/internal/project_loader/_extensions/ros1_noetic.py
@@ -39,7 +39,7 @@ def get_supported_confinement() -> Tuple[str, ...]:
@staticmethod
def is_experimental(base: Optional[str]) -> bool:
- return True
+ return False
def __init__(self, *, extension_name: str, yaml_data: Dict[str, Any]) -> None:
super().__init__(extension_name=extension_name, yaml_data=yaml_data)
diff --git a/snapcraft_legacy/internal/project_loader/_extensions/ros2_foxy.py b/snapcraft_legacy/internal/project_loader/_extensions/ros2_foxy.py
index 4d9a1a4fa1..0f8ed7f5ea 100644
--- a/snapcraft_legacy/internal/project_loader/_extensions/ros2_foxy.py
+++ b/snapcraft_legacy/internal/project_loader/_extensions/ros2_foxy.py
@@ -41,7 +41,7 @@ def get_supported_confinement() -> Tuple[str, ...]:
@staticmethod
def is_experimental(base: Optional[str]) -> bool:
- return True
+ return False
def __init__(self, *, extension_name: str, yaml_data: Dict[str, Any]) -> None:
super().__init__(extension_name=extension_name, yaml_data=yaml_data)
@@ -87,6 +87,11 @@ def __init__(self, *, extension_name: str, yaml_data: Dict[str, Any]) -> None:
"source": "$SNAPCRAFT_EXTENSIONS_DIR/ros2",
"plugin": "nil",
"override-build": "install -D -m 0755 launch ${SNAPCRAFT_PART_INSTALL}/snap/command-chain/ros2-launch",
- "build-packages": [f"ros-{self.ROS_DISTRO}-ros-core"],
+ "build-packages": [
+ f"ros-{self.ROS_DISTRO}-ros-environment",
+ f"ros-{self.ROS_DISTRO}-ros-workspace",
+ f"ros-{self.ROS_DISTRO}-ament-index-cpp",
+ f"ros-{self.ROS_DISTRO}-ament-index-python",
+ ],
}
}
diff --git a/snapcraft_legacy/internal/repo/apt_cache.py b/snapcraft_legacy/internal/repo/apt_cache.py
index 11be8a58f5..f562034352 100644
--- a/snapcraft_legacy/internal/repo/apt_cache.py
+++ b/snapcraft_legacy/internal/repo/apt_cache.py
@@ -110,6 +110,7 @@ def _populate_stage_cache_dir(self) -> None:
return
# Copy apt configuration from host.
+ etc_apt_path = Path("/etc/apt")
cache_etc_apt_path = Path(self.stage_cache, "etc", "apt")
# Delete potentially outdated cache configuration.
@@ -120,7 +121,21 @@ def _populate_stage_cache_dir(self) -> None:
# Copy current cache configuration.
cache_etc_apt_path.parent.mkdir(parents=True, exist_ok=True)
- shutil.copytree("/etc/apt", cache_etc_apt_path)
+
+ # systems with ubuntu pro have an auth file inside the /etc/apt directory.
+ # this auth file is readable only by root, so the copytree call below may
+ # fail when attempting to copy this file into the cache directory
+ try:
+ shutil.copytree(etc_apt_path, cache_etc_apt_path)
+ except shutil.Error as error:
+ # copytree is a multi-file operation, so it generates a list of exceptions
+ # each exception in the list is a 3-element tuple: (source, dest, reason)
+ raise errors.PopulateCacheDirError(error.args[0]) from error
+ except PermissionError as error:
+ # catch the PermissionError raised when `/etc/apt` is unreadable
+ raise errors.PopulateCacheDirError(
+ [(etc_apt_path, cache_etc_apt_path, error)]
+ ) from error
# Specify default arch (if specified).
if self.stage_cache_arch is not None:
diff --git a/snapcraft_legacy/internal/repo/errors.py b/snapcraft_legacy/internal/repo/errors.py
index b06d3943ce..19985097d0 100644
--- a/snapcraft_legacy/internal/repo/errors.py
+++ b/snapcraft_legacy/internal/repo/errors.py
@@ -15,7 +15,7 @@
# along with this program. If not, see .
from pathlib import Path
-from typing import List, Optional, Sequence
+from typing import List, Optional, Sequence, Tuple
from snapcraft_legacy import formatting_utils
from snapcraft_legacy.internal import errors
@@ -57,6 +57,27 @@ def __init__(self, errors: str) -> None:
super().__init__(errors=errors)
+class PopulateCacheDirError(SnapcraftException):
+ def __init__(self, copy_errors: List[Tuple]) -> None:
+ """:param copy_errors: A list of tuples containing the copy errors, where each
+ tuple is ordered as (source, destination, reason)."""
+ self.copy_errors = copy_errors
+
+ def get_brief(self) -> str:
+ return "Could not populate apt cache directory."
+
+ def get_details(self) -> str:
+ """Build a readable list of errors."""
+ details = ""
+ for error in self.copy_errors:
+ source, dest, reason = error
+ details += f"Unable to copy {source} to {dest}: {reason}\n"
+ return details
+
+ def get_resolution(self) -> str:
+ return "Verify user has read access to contents of /etc/apt."
+
+
class FileProviderNotFound(RepoError):
fmt = "{file_path} is not provided by any package."
diff --git a/snapcraft_legacy/plugins/v2/colcon.py b/snapcraft_legacy/plugins/v2/colcon.py
index b1c82ae178..89516cdd15 100644
--- a/snapcraft_legacy/plugins/v2/colcon.py
+++ b/snapcraft_legacy/plugins/v2/colcon.py
@@ -137,8 +137,13 @@ def _get_workspace_activation_commands(self) -> List[str]:
'state="$(set +o); set -$-"',
"set +u",
# If it exists, source the stage-snap underlay
- 'if [ -f "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/setup.sh ]; then',
- "COLCON_CURRENT_PREFIX={path} . {path}/setup.sh".format(
+ 'if [ -f "${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}/local_setup.sh" ]; then',
+ 'AMENT_CURRENT_PREFIX="{path}" . "{path}/local_setup.sh"'.format(
+ path='${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}'
+ ),
+ "fi",
+ 'if [ -f "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/local_setup.sh ]; then',
+ "COLCON_CURRENT_PREFIX={path} . {path}/local_setup.sh".format(
path='"${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap'
),
"fi",
diff --git a/snapcraft_legacy/ruff.toml b/snapcraft_legacy/ruff.toml
new file mode 100644
index 0000000000..e26460ba3b
--- /dev/null
+++ b/snapcraft_legacy/ruff.toml
@@ -0,0 +1,20 @@
+# ruff configuration file to run on legacy code and tests.
+# generated with flake8-to-ruff
+extend-exclude = [
+ ".vscode",
+ "__pycache__",
+ "parts",
+ "stage",
+ "prime",
+]
+ignore = ["E501"]
+line-length = 88
+select = [
+ "C9",
+ "E",
+ "F",
+ "W",
+]
+
+[mccabe]
+max-complexity = 10
diff --git a/spread.yaml b/spread.yaml
index 5013dba2b3..5107957dc9 100644
--- a/spread.yaml
+++ b/spread.yaml
@@ -27,6 +27,7 @@ environment:
DEBIAN_PRIORITY: critical
TOOLS_DIR: /snapcraft/tests/spread/tools
+ PATH: $PATH:$TOOLS_DIR/snapd-testing-tools/tools/
# Git environment for commits
GIT_AUTHOR_NAME: "Test User"
@@ -208,8 +209,9 @@ prepare: |
# nicely handle the snap and deb being installed at the same time.
apt-get remove --purge --yes lxd lxd-client
fi
- # Install and setup the lxd snap
- snap install lxd
+ # install and setup the lxd snap - use 'retry' to workaround aa-exec issue
+ # see https://bugs.launchpad.net/snapd/+bug/1870201
+ retry -n 5 --wait 5 sh -c 'snap install lxd'
# Add the ubuntu user to the lxd group.
adduser ubuntu lxd
lxd init --auto
@@ -228,19 +230,9 @@ prepare: |
/snap/bin/lxd init --auto
fi
- # If $SNAPCRAFT_CHANNEL is defined, install snapcraft from that channel.
- # Otherwise, look for it in /snapcraft/.
- if [ -z "$SNAPCRAFT_CHANNEL" ]; then
- if stat /snapcraft/tests/*.snap 2>/dev/null; then
- snap install --classic --dangerous /snapcraft/tests/*.snap
- else
- echo "Expected a snap to exist in /snapcraft/tests/. If your intention"\
- "was to install from the store, set \$SNAPCRAFT_CHANNEL."
- exit 1
- fi
- else
- snap install --classic snapcraft --channel="$SNAPCRAFT_CHANNEL"
- fi
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/prepare.sh"
+ install_snapcraft
pushd /snapcraft
git init
@@ -293,6 +285,11 @@ suites:
systems:
- ubuntu-22.04*
+ tests/spread/core22/patchelf/:
+ summary: core22 patchelf tests
+ systems:
+ - ubuntu-22.04*
+
# General, core suite
tests/spread/general/:
summary: tests of snapcraft core functionality
diff --git a/tests/legacy/fake_servers/__init__.py b/tests/legacy/fake_servers/__init__.py
index 30debdbc4b..060a4196dc 100644
--- a/tests/legacy/fake_servers/__init__.py
+++ b/tests/legacy/fake_servers/__init__.py
@@ -64,7 +64,6 @@ def __init__(self, server_address):
class FakePartsRequestHandler(BaseHTTPRequestHandler):
-
_date_format = "%a, %d %b %Y %H:%M:%S GMT"
_parts_date = datetime(2016, 7, 7, 10, 0, 20)
@@ -244,7 +243,6 @@ def __init__(self, fake_store, server_address):
class FakeSSORequestHandler(BaseHTTPRequestHandler):
-
_API_PATH = "/api/v2/"
def do_POST(self):
diff --git a/tests/legacy/fake_servers/api.py b/tests/legacy/fake_servers/api.py
index fff327d0c3..79067326fa 100644
--- a/tests/legacy/fake_servers/api.py
+++ b/tests/legacy/fake_servers/api.py
@@ -32,7 +32,6 @@
class FakeStoreAPIServer(base.BaseFakeServer):
-
_DEV_API_PATH = "/dev/api/"
_V2_DEV_API_PATH = "/api/v2/snaps/"
diff --git a/tests/legacy/fake_servers/search.py b/tests/legacy/fake_servers/search.py
index 199010ce46..5e64ab0b7a 100644
--- a/tests/legacy/fake_servers/search.py
+++ b/tests/legacy/fake_servers/search.py
@@ -28,7 +28,6 @@
class FakeStoreSearchServer(base.BaseFakeServer):
-
# XXX This fake server as reused as download server, to avoid passing a
# port as an argument. --elopio - 2016-05-01
diff --git a/tests/legacy/fake_servers/snapd.py b/tests/legacy/fake_servers/snapd.py
index bd0429ca05..4616b28a32 100644
--- a/tests/legacy/fake_servers/snapd.py
+++ b/tests/legacy/fake_servers/snapd.py
@@ -21,7 +21,6 @@
class FakeSnapdRequestHandler(fake_servers.BaseHTTPRequestHandler):
-
snaps_result = [] # type: List[Dict[str, Any]]
snap_details_func = None
find_result = [] # type: List[Dict[str, Any]]
diff --git a/tests/legacy/fixture_setup/_fixtures.py b/tests/legacy/fixture_setup/_fixtures.py
index 50ede1576e..66b1bedc0d 100644
--- a/tests/legacy/fixture_setup/_fixtures.py
+++ b/tests/legacy/fixture_setup/_fixtures.py
@@ -292,22 +292,18 @@ def _stop_fake_server(self, thread):
class FakePartsWikiOriginRunning(FakeServerRunning):
-
fake_server = fake_servers.FakePartsWikiOriginServer
class FakePartsWikiRunning(FakeServerRunning):
-
fake_server = fake_servers.FakePartsWikiServer
class FakePartsWikiWithSlashesRunning(FakeServerRunning):
-
fake_server = fake_servers.FakePartsWikiWithSlashesServer
class FakePartsServerRunning(FakeServerRunning):
-
fake_server = fake_servers.FakePartsServer
@@ -318,7 +314,6 @@ def __init__(self, fake_store):
class FakeStoreUploadServerRunning(FakeServerRunning):
-
fake_server = upload.FakeStoreUploadServer
@@ -329,7 +324,6 @@ def __init__(self, fake_store):
class FakeStoreSearchServerRunning(FakeServerRunning):
-
fake_server = search.FakeStoreSearchServer
@@ -658,7 +652,6 @@ def setUp(self) -> None:
class FakeBaseEnvironment(fixtures.Fixture):
-
_LINKER_FOR_ARCH = dict(
armv7l="lib/ld-linux-armhf.so.3",
aarch64="lib/ld-linux-aarch64.so.1",
diff --git a/tests/legacy/unit/build_providers/multipass/test_multipass.py b/tests/legacy/unit/build_providers/multipass/test_multipass.py
index ce45206617..0305b131f5 100644
--- a/tests/legacy/unit/build_providers/multipass/test_multipass.py
+++ b/tests/legacy/unit/build_providers/multipass/test_multipass.py
@@ -440,7 +440,6 @@ def test_destroy_instance_with_stop_delay_invalid(self):
class TestMultipassWithBases:
-
scenarios = (
("linux", dict(base="core20", expected_image="snapcraft:core20")),
("linux", dict(base="core18", expected_image="snapcraft:core18")),
@@ -525,7 +524,6 @@ def test_lifecycle(
def test_mount_prime_directory(
self, xdg_dirs, in_snap, snap_injector, multipass_cmd, base, expected_image
):
-
with MultipassTestImpl(
project=get_project(base), echoer=mock.Mock(), is_ephemeral=False
) as instance:
diff --git a/tests/legacy/unit/build_providers/test_errors.py b/tests/legacy/unit/build_providers/test_errors.py
index 28bf314394..cefcfd4d3a 100644
--- a/tests/legacy/unit/build_providers/test_errors.py
+++ b/tests/legacy/unit/build_providers/test_errors.py
@@ -18,7 +18,6 @@
class TestErrorFormatting:
-
scenarios = [
(
"ProviderNotSupportedError",
diff --git a/tests/legacy/unit/cache/test_file.py b/tests/legacy/unit/cache/test_file.py
index 75df63d771..f93dca6a76 100644
--- a/tests/legacy/unit/cache/test_file.py
+++ b/tests/legacy/unit/cache/test_file.py
@@ -21,7 +21,6 @@
class TestFileCache:
-
scenarios = [
("sha384", dict(algo="sha384")),
("md5", dict(algo="md5")),
diff --git a/tests/legacy/unit/cli/test_errors.py b/tests/legacy/unit/cli/test_errors.py
index 782715789d..72c93a391c 100644
--- a/tests/legacy/unit/cli/test_errors.py
+++ b/tests/legacy/unit/cli/test_errors.py
@@ -38,7 +38,6 @@
class SnapcraftTError(snapcraft_legacy.internal.errors.SnapcraftError):
-
fmt = "{message}"
def __init__(self, message):
diff --git a/tests/legacy/unit/commands/test_build_providers.py b/tests/legacy/unit/commands/test_build_providers.py
index 15dbb90b0b..ae2ee3081c 100644
--- a/tests/legacy/unit/commands/test_build_providers.py
+++ b/tests/legacy/unit/commands/test_build_providers.py
@@ -31,7 +31,6 @@
class LifecycleCommandsBaseTestCase(CommandBaseTestCase):
-
yaml_template = """name: {step}-test
version: "1.0"
summary: test {step}
diff --git a/tests/legacy/unit/commands/test_list_keys.py b/tests/legacy/unit/commands/test_list_keys.py
index cb808b5641..9e0e23b835 100644
--- a/tests/legacy/unit/commands/test_list_keys.py
+++ b/tests/legacy/unit/commands/test_list_keys.py
@@ -23,7 +23,6 @@
class ListKeysCommandTestCase(FakeStoreCommandsBaseTestCase):
-
command_name = "list-keys"
def test_command_without_login_must_ask(self):
diff --git a/tests/legacy/unit/commands/test_list_plugins.py b/tests/legacy/unit/commands/test_list_plugins.py
index 6d0ebfad82..0bab588967 100644
--- a/tests/legacy/unit/commands/test_list_plugins.py
+++ b/tests/legacy/unit/commands/test_list_plugins.py
@@ -24,7 +24,6 @@
class ListPluginsCommandTestCase(CommandBaseTestCase):
-
command_name = "list-plugins"
default_plugin_output = (
diff --git a/tests/legacy/unit/commands/test_refresh.py b/tests/legacy/unit/commands/test_refresh.py
index 6d8dee82a7..8d12558bca 100644
--- a/tests/legacy/unit/commands/test_refresh.py
+++ b/tests/legacy/unit/commands/test_refresh.py
@@ -25,7 +25,6 @@
class RefreshCommandBaseTestCase(CommandBaseTestCase, TestWithFakeRemoteParts):
-
yaml_template = dedent(
"""\
name: snap-test
diff --git a/tests/legacy/unit/commands/test_verbosity.py b/tests/legacy/unit/commands/test_verbosity.py
new file mode 100644
index 0000000000..2d8e3bad0a
--- /dev/null
+++ b/tests/legacy/unit/commands/test_verbosity.py
@@ -0,0 +1,123 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright (C) 2023 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 sys
+
+import pytest
+
+from snapcraft_legacy.cli._runner import run
+
+
+@pytest.fixture
+def mock_configure(mocker):
+ yield mocker.patch("snapcraft_legacy.internal.log.configure")
+
+
+@pytest.fixture
+def mock_runner(mocker):
+ """Mock commands run by the snapcraft_legacy."""
+ mocker.patch("snapcraft_legacy.cli.lifecycle._execute")
+ mocker.patch("snapcraft_legacy.cli._runner.configure_requests_ca")
+
+
+@pytest.mark.parametrize("command", ["pull", "build", "stage", "prime", "snap"])
+def test_verbosity_debug(command, mock_configure, mock_runner, mocker):
+ """`--enable-developer-debug` should set the verbosity."""
+ mocker.patch.object(sys, "argv", [command, "--enable-developer-debug"])
+ with pytest.raises(SystemExit) as raised:
+ run()
+
+ assert raised.value.code == 0
+ mock_configure.assert_called_once_with(log_level=logging.DEBUG)
+
+
+@pytest.mark.parametrize("command", ["pull", "build", "stage", "prime", "snap"])
+def test_verbosity_verbose_long(command, mock_configure, mock_runner, mocker):
+ """`--verbose` should set the verbosity."""
+ mocker.patch.object(sys, "argv", ["pull", "--verbose"])
+ with pytest.raises(SystemExit) as raised:
+ run()
+
+ assert raised.value.code == 0
+ mock_configure.assert_called_once_with(log_level=logging.INFO)
+
+
+@pytest.mark.parametrize("command", ["pull", "build", "stage", "prime", "snap"])
+def test_verbosity_verbose_long_before_command(
+ command, mock_configure, mock_runner, mocker
+):
+ """`--verbose` can precede the command."""
+ mocker.patch.object(sys, "argv", [command, "--verbose"])
+ with pytest.raises(SystemExit) as raised:
+ run()
+
+ assert raised.value.code == 0
+ mock_configure.assert_called_once_with(log_level=logging.INFO)
+
+
+@pytest.mark.parametrize("command", ["pull", "build", "stage", "prime", "snap"])
+def test_verbosity_verbose_short(command, mock_configure, mock_runner, mocker):
+ """`-v` should set the verbosity."""
+ mocker.patch.object(sys, "argv", [command, "-v"])
+ with pytest.raises(SystemExit) as raised:
+ run()
+
+ assert raised.value.code == 0
+ mock_configure.assert_called_once_with(log_level=logging.INFO)
+
+
+@pytest.mark.parametrize("command", ["pull", "build", "stage", "prime", "snap"])
+def test_verbosity_verbose_short_before_command(
+ command, mock_configure, mock_runner, mocker
+):
+ """`-v` can precede the command."""
+ mocker.patch.object(sys, "argv", [command, "-v"])
+ with pytest.raises(SystemExit) as raised:
+ run()
+
+ assert raised.value.code == 0
+ mock_configure.assert_called_once_with(log_level=logging.INFO)
+
+
+@pytest.mark.parametrize("command", ["pull", "build", "stage", "prime", "snap"])
+def test_verbosity_verbose_long_and_debug_error(
+ capsys, command, mock_configure, mock_runner, mocker
+):
+ mocker.patch.object(sys, "argv", [command, "--verbose", "--enable-developer-debug"])
+ with pytest.raises(SystemExit) as raised:
+ run()
+
+ assert raised.value.code == 2
+ assert (
+ "Error: The 'enable-developer-debug' and 'verbose' options are mutually exclusive."
+ in capsys.readouterr().err
+ )
+
+
+@pytest.mark.parametrize("command", ["pull", "build", "stage", "prime", "snap"])
+def test_verbosity_verbose_short_and_debug_error(
+ capsys, command, mock_configure, mock_runner, mocker
+):
+ mocker.patch.object(sys, "argv", [command, "-v", "--enable-developer-debug"])
+ with pytest.raises(SystemExit) as raised:
+ run()
+
+ assert raised.value.code == 2
+ assert (
+ "Error: The 'enable-developer-debug' and 'verbose' options are mutually exclusive."
+ in capsys.readouterr().err
+ )
diff --git a/tests/legacy/unit/extractors/test_appstream.py b/tests/legacy/unit/extractors/test_appstream.py
index e45f178b07..b8007c16fe 100644
--- a/tests/legacy/unit/extractors/test_appstream.py
+++ b/tests/legacy/unit/extractors/test_appstream.py
@@ -36,7 +36,6 @@ def _create_desktop_file(desktop_file_path, icon: str = None) -> None:
class TestAppstream:
-
scenarios = testscenarios.multiply_scenarios(
[
(
@@ -726,7 +725,6 @@ def test_unhandled_file_test_case(self):
class TestAppstreamLaunchable:
-
scenarios = (
(
"usr/share",
@@ -765,7 +763,6 @@ def test(self, tmp_work_path, desktop_file_path):
class TestAppstreamLegacyDesktop:
-
scenarios = (
(
"usr/share",
diff --git a/tests/legacy/unit/extractors/test_metadata.py b/tests/legacy/unit/extractors/test_metadata.py
index d2bbd65170..670765b311 100644
--- a/tests/legacy/unit/extractors/test_metadata.py
+++ b/tests/legacy/unit/extractors/test_metadata.py
@@ -90,7 +90,6 @@ def test_to_dict_is_a_copy(self):
class TestExtractedMetadataGetters:
-
scenarios = [
("common_id", {"prop": "common_id", "value": "test-value"}),
("summary", {"prop": "summary", "value": "test-value"}),
diff --git a/tests/legacy/unit/extractors/test_setuppy.py b/tests/legacy/unit/extractors/test_setuppy.py
index ab362d5fd0..b8e3a82e5f 100644
--- a/tests/legacy/unit/extractors/test_setuppy.py
+++ b/tests/legacy/unit/extractors/test_setuppy.py
@@ -24,7 +24,6 @@
class TestSetupPy:
-
metadata = [
(
"description",
diff --git a/tests/legacy/unit/lifecycle/test_errors.py b/tests/legacy/unit/lifecycle/test_errors.py
index 318ffa47d4..855f70faff 100644
--- a/tests/legacy/unit/lifecycle/test_errors.py
+++ b/tests/legacy/unit/lifecycle/test_errors.py
@@ -18,7 +18,6 @@
class TestErrorFormatting:
-
scenarios = (
(
"PackVerificationError",
diff --git a/tests/legacy/unit/lifecycle/test_order.py b/tests/legacy/unit/lifecycle/test_order.py
index 71e720d0be..92a62b13a4 100644
--- a/tests/legacy/unit/lifecycle/test_order.py
+++ b/tests/legacy/unit/lifecycle/test_order.py
@@ -1022,7 +1022,6 @@ def test_prime_rebuild(self):
self.run_test()
def test_all_build_rebuild(self):
-
self.set_attributes(
{
"initial_step": steps.BUILD,
diff --git a/tests/legacy/unit/meta/test_errors.py b/tests/legacy/unit/meta/test_errors.py
index ecbcffa7ad..415431d447 100644
--- a/tests/legacy/unit/meta/test_errors.py
+++ b/tests/legacy/unit/meta/test_errors.py
@@ -18,7 +18,6 @@
class TestErrorFormatting:
-
scenarios = (
(
"MissingSnapcraftYamlKeysError",
@@ -153,7 +152,6 @@ def test_error_formatting(self, exception_class, kwargs, expected_message):
class TestSnapcraftException:
-
scenarios = (
(
"GradeDevelRequiredError",
diff --git a/tests/legacy/unit/meta/test_meta.py b/tests/legacy/unit/meta/test_meta.py
index df8fa4d4cd..b195ec026b 100644
--- a/tests/legacy/unit/meta/test_meta.py
+++ b/tests/legacy/unit/meta/test_meta.py
@@ -379,7 +379,6 @@ def test_adapter_none(self):
class StopModeTestCase(CreateBaseTestCase):
-
stop_modes = [
"sigterm",
"sigterm-all",
@@ -405,7 +404,6 @@ def test_valid(self):
class RefreshModeTestCase(CreateBaseTestCase):
-
refresh_modes = ["endure", "restart"]
def test_valid(self):
@@ -523,7 +521,6 @@ def test_warn_once_only(self):
class PassthroughPropagateTestCase(PassthroughBaseTestCase):
-
cases = [
(
"new",
diff --git a/tests/legacy/unit/pluginhandler/test_clean.py b/tests/legacy/unit/pluginhandler/test_clean.py
index f701cf2761..d1c1a8f926 100644
--- a/tests/legacy/unit/pluginhandler/test_clean.py
+++ b/tests/legacy/unit/pluginhandler/test_clean.py
@@ -300,7 +300,6 @@ def test_clean_stage_old_stage_state(self):
class TestCleanPrime:
-
scenarios = [
("all", {"fileset": ["*"]}),
("no1", {"fileset": ["-1"]}),
@@ -343,7 +342,6 @@ def test_clean_prime(self, monkeypatch, tmp_work_path, fileset):
class TestCleanStage:
-
scenarios = [
("all", {"fileset": ["*"]}),
("no1", {"fileset": ["-1"]}),
diff --git a/tests/legacy/unit/pluginhandler/test_dirty_report.py b/tests/legacy/unit/pluginhandler/test_dirty_report.py
index 4d17923a85..78d9dc117d 100644
--- a/tests/legacy/unit/pluginhandler/test_dirty_report.py
+++ b/tests/legacy/unit/pluginhandler/test_dirty_report.py
@@ -24,7 +24,6 @@
class TestDirtyReportGetReport:
-
property_scenarios = [
("no properties", dict(dirty_properties=None, properties_report="")),
(
@@ -122,7 +121,6 @@ def test_get_report(
class TestDirtyReportGetSummary:
-
scenarios = [
(
"single property",
diff --git a/tests/legacy/unit/pluginhandler/test_patcher.py b/tests/legacy/unit/pluginhandler/test_patcher.py
index e22d3b0b64..ac3637fc02 100644
--- a/tests/legacy/unit/pluginhandler/test_patcher.py
+++ b/tests/legacy/unit/pluginhandler/test_patcher.py
@@ -150,7 +150,6 @@ def test_conflicting_patchelf_build_attributes(
class TestPrimeTypeExcludesPatching:
-
scenarios = (
(
"kernel",
@@ -237,7 +236,6 @@ def test_no_patcher_called(
class TestPrimeTypeIncludesPatching:
-
scenarios = (
(
"classic",
diff --git a/tests/legacy/unit/pluginhandler/test_pluginhandler.py b/tests/legacy/unit/pluginhandler/test_pluginhandler.py
index 78ed9f02b6..8ba8412688 100644
--- a/tests/legacy/unit/pluginhandler/test_pluginhandler.py
+++ b/tests/legacy/unit/pluginhandler/test_pluginhandler.py
@@ -474,7 +474,6 @@ def __init__(self, *args, **kwargs):
class TestMigratePartFiles:
-
scenarios = [
("nothing", {"fileset": ["-*"], "result": []}),
(
@@ -616,7 +615,6 @@ def test_migratable_filesets_single_really_really_nested_file(self):
class TestOrganize:
-
scenarios = [
(
"simple_file",
diff --git a/tests/legacy/unit/pluginhandler/test_scriptlets.py b/tests/legacy/unit/pluginhandler/test_scriptlets.py
index d5ac61606e..ef5a857a82 100644
--- a/tests/legacy/unit/pluginhandler/test_scriptlets.py
+++ b/tests/legacy/unit/pluginhandler/test_scriptlets.py
@@ -95,7 +95,6 @@ def test_scriptlet_after_repull(self):
class TestScriptletSetter:
-
scenarios = [
(
"set-version",
@@ -180,7 +179,6 @@ def test_set_in_prime(self, tmp_work_path, setter, getter, value):
class TestScriptletMultipleSettersError:
-
scriptlet_scenarios = [
(
"override-pull/build",
diff --git a/tests/legacy/unit/plugins/v1/python/test_errors.py b/tests/legacy/unit/plugins/v1/python/test_errors.py
index 194e107ed2..a05c43fd49 100644
--- a/tests/legacy/unit/plugins/v1/python/test_errors.py
+++ b/tests/legacy/unit/plugins/v1/python/test_errors.py
@@ -19,7 +19,6 @@
class TestErrorFormatting:
-
scenarios = (
(
"PipListInvalidLegacyFormatError",
diff --git a/tests/legacy/unit/plugins/v1/python/test_pip.py b/tests/legacy/unit/plugins/v1/python/test_pip.py
index fbb57ca5e3..a18c571f08 100644
--- a/tests/legacy/unit/plugins/v1/python/test_pip.py
+++ b/tests/legacy/unit/plugins/v1/python/test_pip.py
@@ -391,7 +391,6 @@ def pip_instance(tmp_work_path):
class TestPipDownload:
-
scenarios = [
(
"packages",
@@ -507,7 +506,6 @@ def test_download_without_packages_or_setup_py_or_requirements_should_noop(
class TestPipInstall:
-
scenarios = [
(
"packages",
@@ -662,7 +660,6 @@ def test_install_without_packages_or_setup_py_or_requirements_should_noop(
class TestPipInstallFixupShebang:
-
scenarios = [
(
"bad shebang",
@@ -726,7 +723,6 @@ def test_install_fixes_shebangs(
class TestPipInstallFixupPermissions:
-
scenarios = [
("755", {"file_path": "example.py", "mode": 0o755, "expected_mode": "755"}),
(
@@ -774,7 +770,6 @@ def test_symlink(mock_chmod, tmp_work_path):
class TestPipWheel:
-
scenarios = [
(
"packages",
diff --git a/tests/legacy/unit/plugins/v1/test_ant.py b/tests/legacy/unit/plugins/v1/test_ant.py
index 2bec995655..01e197c673 100644
--- a/tests/legacy/unit/plugins/v1/test_ant.py
+++ b/tests/legacy/unit/plugins/v1/test_ant.py
@@ -317,7 +317,6 @@ def test_unsupported_base_raises(self):
class TestUnsupportedJDKVersionError:
-
scenarios = (
(
"core18",
diff --git a/tests/legacy/unit/plugins/v1/test_catkin.py b/tests/legacy/unit/plugins/v1/test_catkin.py
index 824e5d137e..92f2c66ccd 100644
--- a/tests/legacy/unit/plugins/v1/test_catkin.py
+++ b/tests/legacy/unit/plugins/v1/test_catkin.py
@@ -686,7 +686,6 @@ def test_run_environment_with_underlay(self, run_mock):
def test_run_environment_with_catkin_ros_master_uri(
self, run_mock, source_setup_sh_mock
):
-
self.properties.catkin_ros_master_uri = "http://rosmaster:11311"
plugin = catkin.CatkinPlugin("test-part", self.properties, self.project)
@@ -912,7 +911,6 @@ def test_prepare_build(self, use_python_mock):
class PullNoUnderlayTestCase(CatkinPluginBaseTest):
-
underlay = None
expected_underlay_path = None
@@ -1170,7 +1168,6 @@ def test_pull_pip_dependencies(self, generate_setup_mock):
class PullUnderlayTestCase(CatkinPluginBaseTest):
-
underlay = {"build-path": "test-build-path", "run-path": "test-run-path"}
expected_underlay_path = "test-build-path"
@@ -1455,7 +1452,6 @@ class Options:
class TestBuildArgs:
-
scenarios = [
(
"release without catkin-cmake-args",
@@ -1511,7 +1507,6 @@ def test_build(
catkin_cmake_args,
disable_parallel,
):
-
options.build_attributes += build_attributes
options.catkin_cmake_args += catkin_cmake_args
options.disable_parallel = disable_parallel
@@ -1603,7 +1598,6 @@ def test_build_all_packages(
class FinishBuildNoUnderlayTestCase(CatkinPluginBaseTest):
-
underlay = None
expected_underlay_path = None
@@ -1759,7 +1753,6 @@ def test_finish_build_python_sitecustomize(
class FinishBuildUnderlayTestCase(CatkinPluginBaseTest):
-
underlay = {"build-path": "test-build-path", "run-path": "test-run-path"}
expected_underlay_path = "test-run-path"
diff --git a/tests/legacy/unit/plugins/v1/test_catkin_tools.py b/tests/legacy/unit/plugins/v1/test_catkin_tools.py
index b57a0e200e..2e5a3d0813 100644
--- a/tests/legacy/unit/plugins/v1/test_catkin_tools.py
+++ b/tests/legacy/unit/plugins/v1/test_catkin_tools.py
@@ -139,7 +139,6 @@ class Options:
class TestPrepareBuild:
-
scenarios = [
(
"release without catkin-cmake-args",
diff --git a/tests/legacy/unit/plugins/v1/test_cmake.py b/tests/legacy/unit/plugins/v1/test_cmake.py
index b026a07e3c..160af69a10 100644
--- a/tests/legacy/unit/plugins/v1/test_cmake.py
+++ b/tests/legacy/unit/plugins/v1/test_cmake.py
@@ -177,7 +177,6 @@ def test_unsupported_base(self):
class TestSnapCMakeBuild:
-
scenarios = [
("no snaps", dict(build_snaps=[], expected_config_flags=[])),
(
diff --git a/tests/legacy/unit/plugins/v1/test_colcon.py b/tests/legacy/unit/plugins/v1/test_colcon.py
index 3eacba69a0..3a1d5786b4 100644
--- a/tests/legacy/unit/plugins/v1/test_colcon.py
+++ b/tests/legacy/unit/plugins/v1/test_colcon.py
@@ -1032,7 +1032,6 @@ class Options:
class TestBuildArgs:
-
package_scenarios = [
("one package", {"colcon_packages": ["my_package"]}),
("no packages", {"colcon_packages": []}),
diff --git a/tests/legacy/unit/plugins/v1/test_conda.py b/tests/legacy/unit/plugins/v1/test_conda.py
index 870ecb2c36..43c36839ce 100644
--- a/tests/legacy/unit/plugins/v1/test_conda.py
+++ b/tests/legacy/unit/plugins/v1/test_conda.py
@@ -29,7 +29,6 @@
class CondaPluginBaseTest(PluginsV1BaseTestCase):
-
deb_arch = None
def setUp(self):
diff --git a/tests/legacy/unit/plugins/v1/test_dotnet.py b/tests/legacy/unit/plugins/v1/test_dotnet.py
index f60407c775..094f549d7c 100644
--- a/tests/legacy/unit/plugins/v1/test_dotnet.py
+++ b/tests/legacy/unit/plugins/v1/test_dotnet.py
@@ -132,7 +132,6 @@ def side_effect(cmd, *args, **kwargs):
class TestDotNetErrors:
-
scenarios = (
(
"DotNetBadArchitectureError",
diff --git a/tests/legacy/unit/plugins/v1/test_gradle.py b/tests/legacy/unit/plugins/v1/test_gradle.py
index 69c8786f70..9b96fdbeb7 100644
--- a/tests/legacy/unit/plugins/v1/test_gradle.py
+++ b/tests/legacy/unit/plugins/v1/test_gradle.py
@@ -204,7 +204,6 @@ def fake_run(cmd, **kwargs):
class TestGradleProxies:
-
scenarios = [
(
"http proxy url",
diff --git a/tests/legacy/unit/plugins/v1/test_maven.py b/tests/legacy/unit/plugins/v1/test_maven.py
index 39e215693e..384d215bb7 100644
--- a/tests/legacy/unit/plugins/v1/test_maven.py
+++ b/tests/legacy/unit/plugins/v1/test_maven.py
@@ -616,7 +616,6 @@ def test_unsupported_base_raises(self):
class TestUnsupportedJDKVersionError:
-
scenarios = (
(
"core18",
diff --git a/tests/legacy/unit/plugins/v1/test_rust.py b/tests/legacy/unit/plugins/v1/test_rust.py
index 5ecab1b939..7d2fcb3a5f 100644
--- a/tests/legacy/unit/plugins/v1/test_rust.py
+++ b/tests/legacy/unit/plugins/v1/test_rust.py
@@ -140,7 +140,6 @@ class Options:
class TestRustPluginCrossCompile:
-
scenarios = [
("armv7l", dict(deb_arch="armhf", target="armv7-unknown-linux-gnueabihf")),
("aarch64", dict(deb_arch="arm64", target="aarch64-unknown-linux-gnu")),
diff --git a/tests/legacy/unit/plugins/v2/test_colcon.py b/tests/legacy/unit/plugins/v2/test_colcon.py
index fbeb9f62a4..51873ef213 100644
--- a/tests/legacy/unit/plugins/v2/test_colcon.py
+++ b/tests/legacy/unit/plugins/v2/test_colcon.py
@@ -107,8 +107,11 @@ class Options:
assert plugin.get_build_commands() == [
'state="$(set +o); set -$-"',
"set +u",
- 'if [ -f "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/setup.sh ]; then',
- 'COLCON_CURRENT_PREFIX="${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap . "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/setup.sh',
+ 'if [ -f "${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}/local_setup.sh" ]; then',
+ 'AMENT_CURRENT_PREFIX="${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}" . "${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}/local_setup.sh"',
+ "fi",
+ 'if [ -f "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/local_setup.sh ]; then',
+ 'COLCON_CURRENT_PREFIX="${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap . "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/local_setup.sh',
"fi",
'. /opt/ros/"${ROS_DISTRO}"/local_setup.sh',
'eval "${state}"',
@@ -161,8 +164,11 @@ class Options:
assert plugin.get_build_commands() == [
'state="$(set +o); set -$-"',
"set +u",
- 'if [ -f "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/setup.sh ]; then',
- 'COLCON_CURRENT_PREFIX="${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap . "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/setup.sh',
+ 'if [ -f "${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}/local_setup.sh" ]; then',
+ 'AMENT_CURRENT_PREFIX="${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}" . "${SNAPCRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}/local_setup.sh"',
+ "fi",
+ 'if [ -f "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/local_setup.sh ]; then',
+ 'COLCON_CURRENT_PREFIX="${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap . "${SNAPCRAFT_PART_INSTALL}"/opt/ros/snap/local_setup.sh',
"fi",
'. /opt/ros/"${ROS_DISTRO}"/local_setup.sh',
'eval "${state}"',
diff --git a/tests/legacy/unit/plugins/v2/test_python.py b/tests/legacy/unit/plugins/v2/test_python.py
index c56eae0987..438c324c91 100644
--- a/tests/legacy/unit/plugins/v2/test_python.py
+++ b/tests/legacy/unit/plugins/v2/test_python.py
@@ -44,7 +44,6 @@ def test_schema():
"uniqueItems": True,
},
},
- "type": "object",
}
diff --git a/tests/legacy/unit/project/test_errors.py b/tests/legacy/unit/project/test_errors.py
index 0b664765b2..30cb804cd4 100644
--- a/tests/legacy/unit/project/test_errors.py
+++ b/tests/legacy/unit/project/test_errors.py
@@ -17,7 +17,6 @@
class TestErrorFormatting:
-
scenarios = [
(
"MissingSnapcraftYamlError",
diff --git a/tests/legacy/unit/project/test_schema.py b/tests/legacy/unit/project/test_schema.py
index 3b5593b791..f22ef421e4 100644
--- a/tests/legacy/unit/project/test_schema.py
+++ b/tests/legacy/unit/project/test_schema.py
@@ -613,7 +613,6 @@ def test_invalid_part_names(data, name):
class TestInvalidArchitectures:
-
scenarios = [
(
"single string",
@@ -1225,13 +1224,29 @@ def test_invalid_command_chain(data, command_chain):
assert expected_message in str(error.value)
-@pytest.mark.parametrize("username", ["snap_daemon", "snap_microk8s"])
+@pytest.mark.parametrize(
+ "username",
+ [
+ "snap_daemon",
+ "snap_microk8s",
+ "snap_aziotedge",
+ "snap_aziotdu",
+ ],
+)
def test_yaml_valid_system_usernames_long(data, username):
data["system-usernames"] = {username: {"scope": "shared"}}
Validator(data).validate()
-@pytest.mark.parametrize("username", ["snap_daemon", "snap_microk8s"])
+@pytest.mark.parametrize(
+ "username",
+ [
+ "snap_daemon",
+ "snap_microk8s",
+ "snap_aziotedge",
+ "snap_aziotdu",
+ ],
+)
def test_yaml_valid_system_usernames_short(data, username):
data["system-usernames"] = {username: "shared"}
Validator(data).validate()
diff --git a/tests/legacy/unit/project_loader/extensions/test_kde_neon.py b/tests/legacy/unit/project_loader/extensions/test_kde_neon.py
index 8b4c4f2458..00f242030b 100644
--- a/tests/legacy/unit/project_loader/extensions/test_kde_neon.py
+++ b/tests/legacy/unit/project_loader/extensions/test_kde_neon.py
@@ -93,9 +93,9 @@ def test_extension_core20():
"interface": "content",
"target": "$SNAP/data-dir/icons",
},
- "kde-frameworks-5-96-qt-5-15-5-core20": {
- "content": "kde-frameworks-5-96-qt-5-15-5-core20-all",
- "default-provider": "kde-frameworks-5-96-qt-5-15-5-core20",
+ "kde-frameworks-5-99-qt-5-15-7-core20": {
+ "content": "kde-frameworks-5-99-qt-5-15-7-core20-all",
+ "default-provider": "kde-frameworks-5-99-qt-5-15-7-core20",
"interface": "content",
"target": "$SNAP/kf5",
},
@@ -113,15 +113,15 @@ def test_extension_core20():
assert kde_neon_extension.part_snippet == {
"build-environment": [
{
- "SNAPCRAFT_CMAKE_ARGS": "-DCMAKE_FIND_ROOT_PATH=/snap/kde-frameworks-5-96-qt-5-15-5-core20-sdk/current"
+ "SNAPCRAFT_CMAKE_ARGS": "-DCMAKE_FIND_ROOT_PATH=/snap/kde-frameworks-5-99-qt-5-15-7-core20-sdk/current"
}
]
}
assert kde_neon_extension.parts == {
"kde-neon-extension": {
"build-packages": ["g++"],
- "build-snaps": ["kde-frameworks-5-96-qt-5-15-5-core20-sdk/latest/stable"],
- "make-parameters": ["PLATFORM_PLUG=kde-frameworks-5-96-qt-5-15-5-core20"],
+ "build-snaps": ["kde-frameworks-5-99-qt-5-15-7-core20-sdk/latest/stable"],
+ "make-parameters": ["PLATFORM_PLUG=kde-frameworks-5-99-qt-5-15-7-core20"],
"plugin": "make",
"source": "$SNAPCRAFT_EXTENSIONS_DIR/desktop",
"source-subdir": "kde-neon",
diff --git a/tests/legacy/unit/project_loader/extensions/test_ros2_foxy.py b/tests/legacy/unit/project_loader/extensions/test_ros2_foxy.py
index 91a7dc40d6..39cc9ac073 100644
--- a/tests/legacy/unit/project_loader/extensions/test_ros2_foxy.py
+++ b/tests/legacy/unit/project_loader/extensions/test_ros2_foxy.py
@@ -60,7 +60,12 @@ def test_extension(extension_class):
assert ros2_extension.parts == {
"ros2-foxy-extension": {
- "build-packages": ["ros-foxy-ros-core"],
+ "build-packages": [
+ "ros-foxy-ros-environment",
+ "ros-foxy-ros-workspace",
+ "ros-foxy-ament-index-cpp",
+ "ros-foxy-ament-index-python",
+ ],
"override-build": "install -D -m 0755 launch "
"${SNAPCRAFT_PART_INSTALL}/snap/command-chain/ros2-launch",
"plugin": "nil",
diff --git a/tests/legacy/unit/project_loader/grammar_processing/test_part_grammar_processor.py b/tests/legacy/unit/project_loader/grammar_processing/test_part_grammar_processor.py
index 77fadc7482..63532afd62 100644
--- a/tests/legacy/unit/project_loader/grammar_processing/test_part_grammar_processor.py
+++ b/tests/legacy/unit/project_loader/grammar_processing/test_part_grammar_processor.py
@@ -36,7 +36,6 @@ def load_tests(loader, tests, ignore):
class TestPartGrammarSource:
-
source_scenarios = [
(
"empty",
@@ -238,7 +237,6 @@ def test(
class TestPartGrammarBuildAndStageSnaps:
-
source_scenarios = [
(
"empty",
@@ -395,7 +393,6 @@ class Plugin:
class TestPartGrammarStagePackages:
-
source_scenarios = [
(
"empty",
@@ -528,7 +525,6 @@ class Plugin:
class TestPartGrammarBuildPackages:
-
source_scenarios = [
(
"empty",
diff --git a/tests/legacy/unit/project_loader/test_parts.py b/tests/legacy/unit/project_loader/test_parts.py
index e2e9aaf06f..b2baf800e8 100644
--- a/tests/legacy/unit/project_loader/test_parts.py
+++ b/tests/legacy/unit/project_loader/test_parts.py
@@ -64,7 +64,6 @@ def test_after_inexistent_part(self):
class TestPartOrder:
-
scenarios = [
(
"part1 then part2",
diff --git a/tests/legacy/unit/project_loader/test_schema.py b/tests/legacy/unit/project_loader/test_schema.py
index f16a952c63..80deaf727a 100644
--- a/tests/legacy/unit/project_loader/test_schema.py
+++ b/tests/legacy/unit/project_loader/test_schema.py
@@ -45,7 +45,6 @@ def get_project_config(snapcraft_yaml_content, target_deb_arch=None):
class TestValidArchitectures:
-
yaml_scenarios = [
(
"none",
diff --git a/tests/legacy/unit/remote_build/test_errors.py b/tests/legacy/unit/remote_build/test_errors.py
index 05865a6339..994ed235ee 100644
--- a/tests/legacy/unit/remote_build/test_errors.py
+++ b/tests/legacy/unit/remote_build/test_errors.py
@@ -18,7 +18,6 @@
class TestSnapcraftException:
-
scenarios = (
(
"LaunchpadGitPushError",
diff --git a/tests/legacy/unit/repo/test_apt_cache.py b/tests/legacy/unit/repo/test_apt_cache.py
index 676154fa02..b2230c2bd1 100644
--- a/tests/legacy/unit/repo/test_apt_cache.py
+++ b/tests/legacy/unit/repo/test_apt_cache.py
@@ -15,14 +15,17 @@
# along with this program. If not, see .
import os
+import shutil
import unittest
from pathlib import Path
from unittest.mock import call
import fixtures
+import pytest
from testtools.matchers import Equals
from snapcraft_legacy.internal.repo.apt_cache import AptCache
+from snapcraft_legacy.internal.repo.errors import PopulateCacheDirError
from tests.legacy import unit
@@ -191,3 +194,62 @@ def test_host_get_installed_version(self):
self.assertThat(
apt_cache.get_installed_version("fake-news-bears"), Equals(None)
)
+
+
+def test_populate_stage_cache_dir_shutil_error(mocker, tmp_path):
+ """Raise an error when the apt cache directory cannot be populated."""
+ mock_copytree = mocker.patch(
+ "snapcraft_legacy.internal.repo.apt_cache.shutil.copytree",
+ side_effect=shutil.Error(
+ [
+ (
+ "/etc/apt/source-file-1",
+ "/root/.cache/dest-file-1",
+ "[Errno 13] Permission denied: '/etc/apt/source-file-1'",
+ ),
+ (
+ "/etc/apt/source-file-2",
+ "/root/.cache/dest-file-2",
+ "[Errno 13] Permission denied: '/etc/apt/source-file-2'",
+ ),
+ ]
+ ),
+ )
+
+ with pytest.raises(PopulateCacheDirError) as raised:
+ with AptCache() as apt_cache:
+ # set stage_cache directory so method does not return early
+ apt_cache.stage_cache = tmp_path
+ apt_cache._populate_stage_cache_dir()
+
+ assert mock_copytree.mock_calls == [call(Path("/etc/apt"), tmp_path / "etc/apt")]
+
+ # verify the data inside the shutil error was passed to PopulateCacheDirError
+ assert raised.value.get_details() == (
+ "Unable to copy /etc/apt/source-file-1 to /root/.cache/dest-file-1: "
+ "[Errno 13] Permission denied: '/etc/apt/source-file-1'\n"
+ "Unable to copy /etc/apt/source-file-2 to /root/.cache/dest-file-2: "
+ "[Errno 13] Permission denied: '/etc/apt/source-file-2'\n"
+ )
+
+
+def test_populate_stage_cache_dir_permission_error(mocker, tmp_path):
+ """Raise an error when the apt cache directory cannot be populated."""
+ mock_copytree = mocker.patch(
+ "snapcraft_legacy.internal.repo.apt_cache.shutil.copytree",
+ side_effect=PermissionError("[Errno 13] Permission denied: '/etc/apt"),
+ )
+
+ with pytest.raises(PopulateCacheDirError) as raised:
+ with AptCache() as apt_cache:
+ # set stage_cache directory so method does not return early
+ apt_cache.stage_cache = tmp_path
+ apt_cache._populate_stage_cache_dir()
+
+ assert mock_copytree.mock_calls == [call(Path("/etc/apt"), tmp_path / "etc/apt")]
+
+ # verify the data inside the permission error was passed to PopulateCacheDirError
+ assert raised.value.get_details() == (
+ f"Unable to copy {Path('/etc/apt')} to {tmp_path / 'etc/apt'}: "
+ "[Errno 13] Permission denied: '/etc/apt\n"
+ )
diff --git a/tests/legacy/unit/repo/test_base.py b/tests/legacy/unit/repo/test_base.py
index d6d880dd0b..d4607c12d7 100644
--- a/tests/legacy/unit/repo/test_base.py
+++ b/tests/legacy/unit/repo/test_base.py
@@ -183,7 +183,6 @@ def test_no_fix_xml2_xslt_config(self):
class FixShebangTestCase(RepoBaseTestCase):
-
scenarios = [
(
"python bin dir",
diff --git a/tests/legacy/unit/repo/test_errors.py b/tests/legacy/unit/repo/test_errors.py
index dc336d66eb..deeebfa49d 100644
--- a/tests/legacy/unit/repo/test_errors.py
+++ b/tests/legacy/unit/repo/test_errors.py
@@ -18,7 +18,6 @@
class TestErrorFormatting:
-
scenarios = (
(
"SnapdConnectionError",
@@ -38,7 +37,6 @@ def test_error_formatting(self, exception_class, kwargs, expected_message):
class TestAptGPGKeyInstallError:
-
scenarios = [
(
"AptGPGKeyInstallError basic",
@@ -181,3 +179,33 @@ def test_multiple_packages_not_found_error():
assert exception.get_details() is None
assert exception.get_docs_url() is None
assert exception.get_reportable() is False
+
+
+def test_snapcraft_exception_handling():
+ exception = errors.PopulateCacheDirError(
+ [
+ (
+ "/etc/apt/source-file-1",
+ "/root/.cache/dest-file-1",
+ "[Errno 13] Permission denied: '/etc/apt/source-file-1'",
+ ),
+ (
+ "/etc/apt/source-file-2",
+ "/root/.cache/dest-file-2",
+ "[Errno 13] Permission denied: '/etc/apt/source-file-2'",
+ ),
+ ]
+ )
+
+ assert exception.get_brief() == "Could not populate apt cache directory."
+ assert exception.get_resolution() == (
+ "Verify user has read access to contents of /etc/apt."
+ )
+ assert exception.get_details() == (
+ "Unable to copy /etc/apt/source-file-1 to /root/.cache/dest-file-1: "
+ "[Errno 13] Permission denied: '/etc/apt/source-file-1'\n"
+ "Unable to copy /etc/apt/source-file-2 to /root/.cache/dest-file-2: "
+ "[Errno 13] Permission denied: '/etc/apt/source-file-2'\n"
+ )
+ assert exception.get_docs_url() is None
+ assert exception.get_reportable() is False
diff --git a/tests/legacy/unit/repo/test_snaps.py b/tests/legacy/unit/repo/test_snaps.py
index 46b2ab1c80..bf7f8ffcb6 100644
--- a/tests/legacy/unit/repo/test_snaps.py
+++ b/tests/legacy/unit/repo/test_snaps.py
@@ -68,7 +68,6 @@ def test_default(self):
)
def test_track_risk(self):
-
self.assert_installed(
snap="fake-snap-stable/latest/stable",
installed_snaps=[{"name": "fake-snap-stable", "channel": "stable"}],
diff --git a/tests/legacy/unit/review_tools/test_errors.py b/tests/legacy/unit/review_tools/test_errors.py
index c59ffb5305..9fcfe3e865 100644
--- a/tests/legacy/unit/review_tools/test_errors.py
+++ b/tests/legacy/unit/review_tools/test_errors.py
@@ -20,7 +20,6 @@
class TestSnapcraftException:
-
scenarios = (
(
"ReviewError (linting error with link)",
diff --git a/tests/legacy/unit/sources/test_errors.py b/tests/legacy/unit/sources/test_errors.py
index 691de2a5f4..59e0fd1ed8 100644
--- a/tests/legacy/unit/sources/test_errors.py
+++ b/tests/legacy/unit/sources/test_errors.py
@@ -18,7 +18,6 @@
class TestErrorFormatting:
-
scenarios = (
(
"SnapcraftSourceNotFoundError",
@@ -88,7 +87,6 @@ def test_error_formatting(self, exception_class, kwargs, expected_message):
class TestSnapcraftException:
-
scenarios = (
(
"GitCommandError",
diff --git a/tests/legacy/unit/sources/test_git.py b/tests/legacy/unit/sources/test_git.py
index 8ca9baeb7e..dc1753f1ca 100644
--- a/tests/legacy/unit/sources/test_git.py
+++ b/tests/legacy/unit/sources/test_git.py
@@ -35,7 +35,6 @@ def fake_git_command_error(*args, **kwargs):
# LP: #1733584
class TestGit(unit.sources.SourceTestCase): # type: ignore
def setUp(self):
-
super().setUp()
patcher = mock.patch("snapcraft_legacy.sources.Git._get_source_details")
self.mock_get_source_details = patcher.start()
@@ -657,7 +656,6 @@ class TestGitConflicts(GitBaseTestCase):
"""Test that git pull errors don't kill the parser"""
def test_git_conflicts(self):
-
repo = "/tmp/conflict-test.git"
working_tree = "/tmp/git-conflict-test"
conflicting_tree = "{}-conflict".format(working_tree)
@@ -863,7 +861,6 @@ def setUp(self):
class TestGitGenerateVersion:
-
scenarios = (
("only_tag", dict(return_value="2.28", expected="2.28")),
(
diff --git a/tests/legacy/unit/sources/test_local.py b/tests/legacy/unit/sources/test_local.py
index 43c444bf90..7720153540 100644
--- a/tests/legacy/unit/sources/test_local.py
+++ b/tests/legacy/unit/sources/test_local.py
@@ -342,7 +342,6 @@ def test_directory_modified(self):
class TestLocalUpdateSnapcraftYaml:
-
scenarios = [
("snapcraft.yaml", dict(snapcraft_file="snapcraft.yaml")),
(".snapcraft.yaml", dict(snapcraft_file=".snapcraft.yaml")),
diff --git a/tests/legacy/unit/sources/test_sources.py b/tests/legacy/unit/sources/test_sources.py
index a895b7e8b3..97fa31eaff 100644
--- a/tests/legacy/unit/sources/test_sources.py
+++ b/tests/legacy/unit/sources/test_sources.py
@@ -20,7 +20,6 @@
class TestUri:
-
scenarios = [
("tar.gz", dict(result="tar", source="https://golang.tar.gz")),
("tar.gz", dict(result="tar", source="https://golang.tar.xz")),
@@ -43,7 +42,6 @@ def test(self, source, result):
class TestSourceWithBranchErrors:
-
scenarios = [
(
"bzr with source branch",
@@ -136,7 +134,6 @@ def test(self, source_type, source_branch, source_tag, source_commit, error):
class TestSourceWithBranchAndTagErrors:
-
scenarios = [
(
"git with source branch and tag",
diff --git a/tests/legacy/unit/states/test_global_state.py b/tests/legacy/unit/states/test_global_state.py
index d3541d490c..4d804db22d 100644
--- a/tests/legacy/unit/states/test_global_state.py
+++ b/tests/legacy/unit/states/test_global_state.py
@@ -43,7 +43,6 @@
class TestGlobalState:
-
scenarios = _scenarios
def test_save(self, tmp_work_path, build_packages, build_snaps, required_grade):
diff --git a/tests/legacy/unit/store/test_errors.py b/tests/legacy/unit/store/test_errors.py
index ea0a1982f3..8de21e59ba 100644
--- a/tests/legacy/unit/store/test_errors.py
+++ b/tests/legacy/unit/store/test_errors.py
@@ -118,7 +118,6 @@ def test_snapcraft_exception_handling(
class TestErrorFormatting:
-
scenarios = [
(
"NoSnapIdError",
diff --git a/tests/legacy/unit/store/test_store_client.py b/tests/legacy/unit/store/test_store_client.py
index f56fbb9b45..bb14afc614 100644
--- a/tests/legacy/unit/store/test_store_client.py
+++ b/tests/legacy/unit/store/test_store_client.py
@@ -49,12 +49,10 @@ def setUp(self):
class DownloadTestCase(StoreTestCase):
-
# sha3-384 of tests/data/test-snap.snap
EXPECTED_SHA3_384 = ""
def test_download_nonexistent_snap_raises_exception(self):
-
raised = self.assertRaises(
errors.SnapNotFoundError,
self.client.download,
@@ -792,7 +790,6 @@ def test_get_snap_status_filter_by_arch(self):
)
def test_get_snap_status_filter_by_unknown_arch(self):
-
raised = self.assertRaises(
storeapi.errors.SnapNotFoundError,
self.client.get_snap_status,
diff --git a/tests/legacy/unit/test_common.py b/tests/legacy/unit/test_common.py
index 64a91c9786..b06e5dcef0 100644
--- a/tests/legacy/unit/test_common.py
+++ b/tests/legacy/unit/test_common.py
@@ -79,7 +79,6 @@ def test_arch_triplet_migration_message(self):
class FormatInColumnsTestCase(unit.TestCase):
-
elements_list = [
"ant",
"autotools",
@@ -151,7 +150,6 @@ def test_format_output_in_columns_one_space(self):
class TestFormatSnapFileName:
-
scenarios = [
(
"all info",
diff --git a/tests/legacy/unit/test_elf.py b/tests/legacy/unit/test_elf.py
index 3fd88871d9..82454e3b33 100644
--- a/tests/legacy/unit/test_elf.py
+++ b/tests/legacy/unit/test_elf.py
@@ -476,7 +476,6 @@ def test_reset_except_root(self):
class TestSonameCacheErrors:
-
scenarios = (
("invalid string key", dict(key="soname.so", partial_message="The key for")),
(
diff --git a/tests/legacy/unit/test_errors.py b/tests/legacy/unit/test_errors.py
index 56da142dba..33aed13dca 100644
--- a/tests/legacy/unit/test_errors.py
+++ b/tests/legacy/unit/test_errors.py
@@ -637,7 +637,6 @@ def get_docs_url(self):
class TestSnapcraftExceptionTests:
-
scenarios = (
(
"StrangeExceptionSimple",
diff --git a/tests/legacy/unit/test_file_utils.py b/tests/legacy/unit/test_file_utils.py
index f5c1c479e7..15ca2a6925 100644
--- a/tests/legacy/unit/test_file_utils.py
+++ b/tests/legacy/unit/test_file_utils.py
@@ -31,7 +31,6 @@
class TestReplaceInFile:
-
scenarios = [
(
"2to3",
@@ -298,7 +297,6 @@ def test_bad_file_formatlinker_raises_exception(self):
class TestGetToolPath:
-
scenarios = [
(i, dict(tool_path=pathlib.Path(i) / "tool-command")) for i in _BIN_PATHS
]
diff --git a/tests/legacy/unit/test_indicators.py b/tests/legacy/unit/test_indicators.py
index 7243b12e03..2cfe83c4ab 100644
--- a/tests/legacy/unit/test_indicators.py
+++ b/tests/legacy/unit/test_indicators.py
@@ -49,7 +49,6 @@ def test_vt100_terminal_environmment(self):
class TestProgressBarInitialization:
-
scenarios = [("Terminal", {"is_dumb": True}), ("Dumb Terminal", {"is_dumb": False})]
def test_init_progress_bar_with_length(self, monkeypatch, is_dumb):
diff --git a/tests/legacy/unit/test_options.py b/tests/legacy/unit/test_options.py
index c8f558ada6..f705f530a8 100644
--- a/tests/legacy/unit/test_options.py
+++ b/tests/legacy/unit/test_options.py
@@ -32,7 +32,6 @@
class TestNativeOptions:
-
scenarios = [
(
"amd64",
@@ -241,7 +240,6 @@ def test_cross_compiler_prefix_empty(
class TestHostIsCompatibleWithTargetBase:
-
scenarios = (
("trusty core", dict(codename="trusty", base="core", is_compatible=False)),
("xenial core", dict(codename="xenial", base="core", is_compatible=False)),
diff --git a/tests/legacy/unit/test_target_arch.py b/tests/legacy/unit/test_target_arch.py
index 039fff1455..8f78e71143 100644
--- a/tests/legacy/unit/test_target_arch.py
+++ b/tests/legacy/unit/test_target_arch.py
@@ -18,7 +18,6 @@
class TestFindMachine:
-
scenarios = [
("x86_64", dict(machine="x86_64", expected_machine="x86_64")),
("amd64", dict(machine="amd64", expected_machine="x86_64")),
diff --git a/tests/spread/core22/linters/classic-libc/expected_linter_output.txt b/tests/spread/core22/linters/classic-libc/expected_linter_output.txt
index 419ecf5d0d..0eb1a1df30 100644
--- a/tests/spread/core22/linters/classic-libc/expected_linter_output.txt
+++ b/tests/spread/core22/linters/classic-libc/expected_linter_output.txt
@@ -3,26 +3,26 @@ Running linter: classic
Lint OK:
- classic: Snap contains staged libc.
Lint warnings:
-- classic: lib/x86_64-linux-gnu/libBrokenLocale.so.1: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libanl.so.1: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libc.so.6: ELF interpreter should be set to '/snap/classic-linter-test/current/lib64/ld-linux-x86-64.so.2'.
-- classic: lib/x86_64-linux-gnu/libc_malloc_debug.so.0: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libdl.so.2: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libm.so.6: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libmemusage.so: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libmvec.so.1: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libnsl.so.1: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libnss_compat.so.2: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libnss_dns.so.2: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libnss_files.so.2: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libnss_hesiod.so.2: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libpcprofile.so: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libpthread.so.0: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libresolv.so.2: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/librt.so.1: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libthread_db.so.1: ELF rpath should be set to '$ORIGIN'.
-- classic: lib/x86_64-linux-gnu/libutil.so.1: ELF rpath should be set to '$ORIGIN'.
-- classic: usr/bin/hello: ELF interpreter should be set to '/snap/classic-linter-test/current/lib64/ld-linux-x86-64.so.2'.
-- classic: usr/bin/hello: ELF rpath should be set to '$ORIGIN/../../lib/x86_64-linux-gnu'.
-- classic: usr/lib/x86_64-linux-gnu/audit/sotruss-lib.so: ELF rpath should be set to '$ORIGIN/../../../../lib/x86_64-linux-gnu'.
+- classic: lib/x86_64-linux-gnu/libBrokenLocale.so.1: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libanl.so.1: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libc.so.6: ELF interpreter should be set to '/snap/classic-linter-test/current/lib64/ld-linux-x86-64.so.2'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libc_malloc_debug.so.0: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libdl.so.2: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libm.so.6: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libmemusage.so: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libmvec.so.1: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libnsl.so.1: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libnss_compat.so.2: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libnss_dns.so.2: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libnss_files.so.2: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libnss_hesiod.so.2: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libpcprofile.so: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libpthread.so.0: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libresolv.so.2: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/librt.so.1: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libthread_db.so.1: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: lib/x86_64-linux-gnu/libutil.so.1: ELF rpath should be set to '$ORIGIN'. (https://snapcraft.io/docs/linters-classic)
+- classic: usr/bin/hello: ELF interpreter should be set to '/snap/classic-linter-test/current/lib64/ld-linux-x86-64.so.2'. (https://snapcraft.io/docs/linters-classic)
+- classic: usr/bin/hello: ELF rpath should be set to '$ORIGIN/../../lib/x86_64-linux-gnu'. (https://snapcraft.io/docs/linters-classic)
+- classic: usr/lib/x86_64-linux-gnu/audit/sotruss-lib.so: ELF rpath should be set to '$ORIGIN/../../../../lib/x86_64-linux-gnu'. (https://snapcraft.io/docs/linters-classic)
Creating snap package...
diff --git a/tests/spread/core22/linters/classic/expected_linter_output.txt b/tests/spread/core22/linters/classic/expected_linter_output.txt
index 5021c6157b..13e926b86e 100644
--- a/tests/spread/core22/linters/classic/expected_linter_output.txt
+++ b/tests/spread/core22/linters/classic/expected_linter_output.txt
@@ -3,11 +3,11 @@ Running linter: classic
Lint OK:
- classic: Snap confinement is set to classic.
Lint warnings:
-- classic: usr/bin/toilet: ELF interpreter should be set to '/snap/core22/current/lib64/ld-linux-x86-64.so.2'.
-- classic: usr/bin/toilet: ELF rpath should be set to '$ORIGIN/../lib/x86_64-linux-gnu:/snap/core22/current/lib/x86_64-linux-gnu'.
-- classic: usr/lib/x86_64-linux-gnu/caca/libgl_plugin.so.0.0.0: ELF rpath should be set to '$ORIGIN/..:/snap/core22/current/lib/x86_64-linux-gnu'.
-- classic: usr/lib/x86_64-linux-gnu/caca/libx11_plugin.so.0.0.0: ELF rpath should be set to '$ORIGIN/..:/snap/core22/current/lib/x86_64-linux-gnu'.
-- classic: usr/lib/x86_64-linux-gnu/libcaca++.so.0.99.19: ELF rpath should be set to '$ORIGIN:/snap/core22/current/lib/x86_64-linux-gnu'.
-- classic: usr/lib/x86_64-linux-gnu/libcaca.so.0.99.19: ELF rpath should be set to '$ORIGIN:/snap/core22/current/lib/x86_64-linux-gnu'.
-- classic: usr/lib/x86_64-linux-gnu/libslang.so.2.3.2: ELF rpath should be set to '/snap/core22/current/lib/x86_64-linux-gnu'.
+- classic: usr/bin/toilet: ELF interpreter should be set to '/snap/core22/current/lib64/ld-linux-x86-64.so.2'. (https://snapcraft.io/docs/linters-classic)
+- classic: usr/bin/toilet: ELF rpath should be set to '$ORIGIN/../lib/x86_64-linux-gnu:/snap/core22/current/lib/x86_64-linux-gnu'. (https://snapcraft.io/docs/linters-classic)
+- classic: usr/lib/x86_64-linux-gnu/caca/libgl_plugin.so.0.0.0: ELF rpath should be set to '$ORIGIN/..:/snap/core22/current/lib/x86_64-linux-gnu'. (https://snapcraft.io/docs/linters-classic)
+- classic: usr/lib/x86_64-linux-gnu/caca/libx11_plugin.so.0.0.0: ELF rpath should be set to '$ORIGIN/..:/snap/core22/current/lib/x86_64-linux-gnu'. (https://snapcraft.io/docs/linters-classic)
+- classic: usr/lib/x86_64-linux-gnu/libcaca++.so.0.99.19: ELF rpath should be set to '$ORIGIN:/snap/core22/current/lib/x86_64-linux-gnu'. (https://snapcraft.io/docs/linters-classic)
+- classic: usr/lib/x86_64-linux-gnu/libcaca.so.0.99.19: ELF rpath should be set to '$ORIGIN:/snap/core22/current/lib/x86_64-linux-gnu'. (https://snapcraft.io/docs/linters-classic)
+- classic: usr/lib/x86_64-linux-gnu/libslang.so.2.3.2: ELF rpath should be set to '/snap/core22/current/lib/x86_64-linux-gnu'. (https://snapcraft.io/docs/linters-classic)
Creating snap package...
diff --git a/tests/spread/core22/linters/library-ignore-missing/expected_linter_output.txt b/tests/spread/core22/linters/library-ignore-missing/expected_linter_output.txt
new file mode 100644
index 0000000000..e1fabef93a
--- /dev/null
+++ b/tests/spread/core22/linters/library-ignore-missing/expected_linter_output.txt
@@ -0,0 +1,4 @@
+Running linters...
+Running linter: classic
+Running linter: library
+Creating snap package...
diff --git a/tests/spread/core22/linters/library-ignore-missing/snap/snapcraft.yaml b/tests/spread/core22/linters/library-ignore-missing/snap/snapcraft.yaml
new file mode 100644
index 0000000000..6a2c9c7eff
--- /dev/null
+++ b/tests/spread/core22/linters/library-ignore-missing/snap/snapcraft.yaml
@@ -0,0 +1,23 @@
+name: library-ignore-missing
+base: core22
+version: '0.1'
+summary: Ignore missing library linter issues
+description: spread test
+
+grade: devel
+confinement: strict
+
+lint:
+ ignore:
+ - library:
+ - linter-test
+
+parts:
+ my-part:
+ plugin: nil
+ source: src
+ build-packages:
+ - gcc
+ - libcaca-dev
+ override-build:
+ gcc -o $CRAFT_PART_INSTALL/linter-test test.c -lcaca
diff --git a/tests/spread/core22/linters/library-ignore-missing/src/test.c b/tests/spread/core22/linters/library-ignore-missing/src/test.c
new file mode 100644
index 0000000000..1b6f17fc0f
--- /dev/null
+++ b/tests/spread/core22/linters/library-ignore-missing/src/test.c
@@ -0,0 +1,7 @@
+#include "caca.h"
+
+int main()
+{
+ caca_create_canvas(80, 24);
+ return 0;
+}
diff --git a/tests/spread/core22/linters/library-ignore-missing/task.yaml b/tests/spread/core22/linters/library-ignore-missing/task.yaml
new file mode 100644
index 0000000000..9ab39c8f14
--- /dev/null
+++ b/tests/spread/core22/linters/library-ignore-missing/task.yaml
@@ -0,0 +1,13 @@
+summary: Ignore missing library linter issues
+
+restore: |
+ snapcraft clean
+ rm -f ./*.snap ./*.txt
+
+execute: |
+ snapcraft 2> output.txt
+
+ test -f library-ignore-missing_0.1_*.snap
+
+ sed -n '/^Running linters/,/^Creating snap/p' < output.txt > linter_output.txt
+ diff -u linter_output.txt expected_linter_output.txt
diff --git a/tests/spread/core22/linters/library-ignore-unused/expected_linter_output.txt b/tests/spread/core22/linters/library-ignore-unused/expected_linter_output.txt
new file mode 100644
index 0000000000..e1fabef93a
--- /dev/null
+++ b/tests/spread/core22/linters/library-ignore-unused/expected_linter_output.txt
@@ -0,0 +1,4 @@
+Running linters...
+Running linter: classic
+Running linter: library
+Creating snap package...
diff --git a/tests/spread/core22/linters/library-ignore-unused/snap/snapcraft.yaml b/tests/spread/core22/linters/library-ignore-unused/snap/snapcraft.yaml
new file mode 100644
index 0000000000..8013ee01a3
--- /dev/null
+++ b/tests/spread/core22/linters/library-ignore-unused/snap/snapcraft.yaml
@@ -0,0 +1,22 @@
+name: library-ignore-unused
+base: core22
+version: '0.1'
+summary: Ignore unused library linter issues
+description: spread test
+
+grade: devel
+confinement: strict
+
+lint:
+ ignore:
+ - library:
+ - usr/lib/*/libpng16.so*
+
+parts:
+ my-part:
+ plugin: nil
+ source: src
+ stage-packages:
+ - libpng16-16
+ override-build:
+ gcc -o $CRAFT_PART_INSTALL/linter-test test.c
diff --git a/tests/spread/core22/linters/library-ignore-unused/src/test.c b/tests/spread/core22/linters/library-ignore-unused/src/test.c
new file mode 100644
index 0000000000..dcfb86bc74
--- /dev/null
+++ b/tests/spread/core22/linters/library-ignore-unused/src/test.c
@@ -0,0 +1,5 @@
+#include
+int main() {
+ printf("Hello, World!");
+ return 0;
+}
diff --git a/tests/spread/core22/linters/library-ignore-unused/task.yaml b/tests/spread/core22/linters/library-ignore-unused/task.yaml
new file mode 100644
index 0000000000..15984ec3ec
--- /dev/null
+++ b/tests/spread/core22/linters/library-ignore-unused/task.yaml
@@ -0,0 +1,13 @@
+summary: Ignore unused library linter issues
+
+restore: |
+ snapcraft clean
+ rm -f ./*.snap ./*.txt
+
+execute: |
+ snapcraft 2> output.txt
+
+ test -f library-ignore-unused_0.1_*.snap
+
+ sed -n '/^Running linters/,/^Creating snap/p' < output.txt > linter_output.txt
+ diff -u linter_output.txt expected_linter_output.txt
diff --git a/tests/spread/core22/linters/library-missing/expected_linter_output.txt b/tests/spread/core22/linters/library-missing/expected_linter_output.txt
index 6696454537..fc910046c6 100644
--- a/tests/spread/core22/linters/library-missing/expected_linter_output.txt
+++ b/tests/spread/core22/linters/library-missing/expected_linter_output.txt
@@ -2,6 +2,6 @@ Running linters...
Running linter: classic
Running linter: library
Lint warnings:
-- library: linter-test: missing dependency 'libcaca.so.0'.
-- library: linter-test: missing dependency 'libslang.so.2'.
+- library: linter-test: missing dependency 'libcaca.so.0'. (https://snapcraft.io/docs/linters-library)
+- library: linter-test: missing dependency 'libslang.so.2'. (https://snapcraft.io/docs/linters-library)
Creating snap package...
diff --git a/tests/spread/core22/linters/library-missing/snap/snapcraft.yaml b/tests/spread/core22/linters/library-missing/snap/snapcraft.yaml
index 0e903b4b2e..1685636072 100644
--- a/tests/spread/core22/linters/library-missing/snap/snapcraft.yaml
+++ b/tests/spread/core22/linters/library-missing/snap/snapcraft.yaml
@@ -1,14 +1,10 @@
-name: library-linter-test
+name: library-missing
base: core22
version: '0.1'
-summary: Single-line elevator pitch for your amazing snap
-description: |
- This is my-snap's description. You have a paragraph or two to tell the
- most important story about your snap. Keep it under 100 words though,
- we live in tweetspace and your description wants to look good in the snap
- store.
+summary: Raise linter warnings for missing libraries
+description: spread test
-grade: devel # must be 'stable' to release into candidate/stable channels
+grade: devel
confinement: strict
parts:
@@ -20,4 +16,3 @@ parts:
- libcaca-dev
override-build:
gcc -o $CRAFT_PART_INSTALL/linter-test test.c -lcaca
-
diff --git a/tests/spread/core22/linters/library-missing/task.yaml b/tests/spread/core22/linters/library-missing/task.yaml
index ccbf5c95cb..673ae27f6f 100644
--- a/tests/spread/core22/linters/library-missing/task.yaml
+++ b/tests/spread/core22/linters/library-missing/task.yaml
@@ -1,4 +1,4 @@
-summary: Test library linter output
+summary: Raise linter warnings for missing libraries
restore: |
snapcraft clean
@@ -7,7 +7,7 @@ restore: |
execute: |
snapcraft 2> output.txt
- test -f library-linter-test_0.1_*.snap
+ test -f library-missing_0.1_*.snap
sed -n '/^Running linters/,/^Creating snap/p' < output.txt > linter_output.txt
diff -u linter_output.txt expected_linter_output.txt
diff --git a/tests/spread/core22/linters/library-unused/expected_linter_output.txt b/tests/spread/core22/linters/library-unused/expected_linter_output.txt
new file mode 100644
index 0000000000..6e5abaf10d
--- /dev/null
+++ b/tests/spread/core22/linters/library-unused/expected_linter_output.txt
@@ -0,0 +1,6 @@
+Running linters...
+Running linter: classic
+Running linter: library
+Lint warnings:
+- library: libpng16.so.16: unused library 'usr/lib/x86_64-linux-gnu/libpng16.so.16.37.0'. (https://snapcraft.io/docs/linters-library)
+Creating snap package...
diff --git a/tests/spread/core22/linters/library-unused/snap/snapcraft.yaml b/tests/spread/core22/linters/library-unused/snap/snapcraft.yaml
new file mode 100644
index 0000000000..8c5ada3d83
--- /dev/null
+++ b/tests/spread/core22/linters/library-unused/snap/snapcraft.yaml
@@ -0,0 +1,17 @@
+name: library-unused
+base: core22
+version: '0.1'
+summary: Raise linter warnings for unused libraries.
+description: spread test
+
+grade: devel
+confinement: strict
+
+parts:
+ my-part:
+ plugin: nil
+ source: src
+ stage-packages:
+ - libpng16-16
+ override-build:
+ gcc -o $CRAFT_PART_INSTALL/linter-test test.c
diff --git a/tests/spread/core22/linters/library-unused/src/test.c b/tests/spread/core22/linters/library-unused/src/test.c
new file mode 100644
index 0000000000..dcfb86bc74
--- /dev/null
+++ b/tests/spread/core22/linters/library-unused/src/test.c
@@ -0,0 +1,5 @@
+#include
+int main() {
+ printf("Hello, World!");
+ return 0;
+}
diff --git a/tests/spread/core22/linters/library-unused/task.yaml b/tests/spread/core22/linters/library-unused/task.yaml
new file mode 100644
index 0000000000..bf9d9ef90b
--- /dev/null
+++ b/tests/spread/core22/linters/library-unused/task.yaml
@@ -0,0 +1,13 @@
+summary: Raise linter warnings for unused libraries.
+
+restore: |
+ snapcraft clean
+ rm -f ./*.snap ./*.txt
+
+execute: |
+ snapcraft 2> output.txt
+
+ test -f library-unused_0.1_*.snap
+
+ sed -n '/^Running linters/,/^Creating snap/p' < output.txt > linter_output.txt
+ diff -u linter_output.txt expected_linter_output.txt
diff --git a/tests/spread/core22/linters/library/expected_linter_output.txt b/tests/spread/core22/linters/library/expected_linter_output.txt
index e1fabef93a..619730ddee 100644
--- a/tests/spread/core22/linters/library/expected_linter_output.txt
+++ b/tests/spread/core22/linters/library/expected_linter_output.txt
@@ -1,4 +1,7 @@
Running linters...
Running linter: classic
Running linter: library
+Lint warnings:
+- library: libgci-1.so.0.0.0: unused library 'usr/lib/x86_64-linux-gnu/libgci-1.so.0'. (https://snapcraft.io/docs/linters-library)
+- library: libgtksourceview-4.so.0: unused library 'usr/lib/x86_64-linux-gnu/libgtksourceview-4.so.0.0.0'. (https://snapcraft.io/docs/linters-library)
Creating snap package...
diff --git a/tests/spread/core22/patchelf/classic-patchelf/Makefile b/tests/spread/core22/patchelf/classic-patchelf/Makefile
new file mode 100644
index 0000000000..8367026229
--- /dev/null
+++ b/tests/spread/core22/patchelf/classic-patchelf/Makefile
@@ -0,0 +1,11 @@
+# -*- Mode: Makefile; indent-tabs-mode:t; tab-width: 4 -*-
+.PHONY: all
+
+all: hello-classic
+
+install: hello-classic
+ install -d $(DESTDIR)/bin/
+ install -D $^ $(DESTDIR)/bin/
+
+hello-classic: hello-classic.c
+ $(CC) hello-classic.c -o hello-classic -lcurl
diff --git a/tests/spread/core22/patchelf/classic-patchelf/hello-classic.c b/tests/spread/core22/patchelf/classic-patchelf/hello-classic.c
new file mode 100644
index 0000000000..0980d5a387
--- /dev/null
+++ b/tests/spread/core22/patchelf/classic-patchelf/hello-classic.c
@@ -0,0 +1,6 @@
+#include
+#include
+
+int main() {
+ curl_global_init(CURL_GLOBAL_DEFAULT);
+}
diff --git a/tests/spread/core22/patchelf/classic-patchelf/snap/snapcraft.yaml b/tests/spread/core22/patchelf/classic-patchelf/snap/snapcraft.yaml
new file mode 100644
index 0000000000..deb5211e63
--- /dev/null
+++ b/tests/spread/core22/patchelf/classic-patchelf/snap/snapcraft.yaml
@@ -0,0 +1,42 @@
+name: classic-patchelf
+version: "0.1"
+summary: Build a classic confined snap
+description: |
+ Build a classic confined snap, mostly used to test the provisioning
+ of `core` inside a snap.
+base: core22
+
+grade: devel
+confinement: classic
+
+parts:
+ hello:
+ source: .
+ plugin: make
+ build-attributes:
+ - enable-patchelf
+ build-packages:
+ - gcc
+ - libcurl4-openssl-dev
+ hello-existing-rpath:
+ source: .
+ plugin: make
+ build-attributes:
+ - enable-patchelf
+ build-packages:
+ - gcc
+ - libcurl4-openssl-dev
+ - patchelf
+ override-build: |
+ snapcraftctl build
+ mv $CRAFT_PART_INSTALL/bin/hello-classic $CRAFT_PART_INSTALL/bin/hello-classic-existing-rpath
+ patchelf --force-rpath --set-rpath "\$ORIGIN/../fake-lib" $CRAFT_PART_INSTALL/bin/hello-classic-existing-rpath
+ hello-no-patchelf:
+ source: .
+ plugin: make
+ build-packages:
+ - gcc
+ - libcurl4-openssl-dev
+ override-build: |
+ snapcraftctl build
+ mv $CRAFT_PART_INSTALL/bin/hello-classic $CRAFT_PART_INSTALL/bin/hello-classic-no-patchelf
diff --git a/tests/spread/core22/patchelf/classic-patchelf/task.yaml b/tests/spread/core22/patchelf/classic-patchelf/task.yaml
new file mode 100644
index 0000000000..a690cb29cf
--- /dev/null
+++ b/tests/spread/core22/patchelf/classic-patchelf/task.yaml
@@ -0,0 +1,46 @@
+summary: Build a classic snap and validates elf patching
+
+prepare: |
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+ set_base snap/snapcraft.yaml
+
+ apt-get install patchelf dpkg-dev -y
+ apt-mark auto patchelf dpkg-dev
+
+restore: |
+ snapcraft clean
+ rm -f ./*.snap
+
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+ restore_yaml "snap/snapcraft.yaml"
+
+execute: |
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+ base="$(get_base)"
+
+ snapcraft prime
+
+ arch_triplet="$(dpkg-architecture -q DEB_HOST_MULTIARCH)"
+
+ # Account for /usr merge.
+ RPATH_MATCH="^/snap/$base/current/lib/$arch_triplet"
+ RPATH_ORIGIN_MATCH="^\\\$ORIGIN/../fake-lib:/snap/$base/current/lib/$arch_triplet"
+
+ # Verify typical binary.
+ patchelf --print-interpreter prime/bin/hello-classic | MATCH "^/snap/$base/current/lib.*ld.*.so.*"
+ patchelf --print-rpath prime/bin/hello-classic | MATCH "${RPATH_MATCH}"
+
+ # Verify binary w/ existing rpath.
+ patchelf --print-interpreter prime/bin/hello-classic-existing-rpath | MATCH "^/snap/$base/current/lib.*ld.*.so.*"
+ patchelf --print-rpath prime/bin/hello-classic-existing-rpath | MATCH "${RPATH_ORIGIN_MATCH}"
+
+ # Verify untouched no-patchelf.
+ patchelf --print-interpreter prime/bin/hello-classic-no-patchelf | MATCH "^/lib.*ld.*.so.*"
+ rpath="$(patchelf --print-rpath prime/bin/hello-classic-no-patchelf)"
+ if [[ -n "${rpath}" ]]; then
+ echo "found rpath with no-patchelf: ${rpath}"
+ exit 1
+ fi
diff --git a/tests/spread/core22/patchelf/strict-patchelf/Makefile b/tests/spread/core22/patchelf/strict-patchelf/Makefile
new file mode 100644
index 0000000000..9113bff569
--- /dev/null
+++ b/tests/spread/core22/patchelf/strict-patchelf/Makefile
@@ -0,0 +1,11 @@
+# -*- Mode: Makefile; indent-tabs-mode:t; tab-width: 4 -*-
+.PHONY: all
+
+all: hello-strict
+
+install: hello-strict
+ install -d $(DESTDIR)/bin/
+ install -D $^ $(DESTDIR)/bin/
+
+hello-strict: hello-strict.c
+ $(CC) hello-strict.c -o hello-strict -lcurl
diff --git a/tests/spread/core22/patchelf/strict-patchelf/hello-strict.c b/tests/spread/core22/patchelf/strict-patchelf/hello-strict.c
new file mode 100644
index 0000000000..0980d5a387
--- /dev/null
+++ b/tests/spread/core22/patchelf/strict-patchelf/hello-strict.c
@@ -0,0 +1,6 @@
+#include
+#include
+
+int main() {
+ curl_global_init(CURL_GLOBAL_DEFAULT);
+}
diff --git a/tests/spread/core22/patchelf/strict-patchelf/snap/snapcraft.yaml b/tests/spread/core22/patchelf/strict-patchelf/snap/snapcraft.yaml
new file mode 100644
index 0000000000..7549c03004
--- /dev/null
+++ b/tests/spread/core22/patchelf/strict-patchelf/snap/snapcraft.yaml
@@ -0,0 +1,41 @@
+name: strict-patchelf
+version: "0.1"
+summary: Build a strictly confined snap
+description: |
+ Build a strictly confined snap to test ELF patching
+base: core22
+
+grade: devel
+confinement: strict
+
+parts:
+ hello:
+ source: .
+ plugin: make
+ build-packages:
+ - gcc
+ - libcurl4-openssl-dev
+ hello-existing-rpath:
+ source: .
+ plugin: make
+ build-packages:
+ - gcc
+ - libcurl4-openssl-dev
+ - patchelf
+ build-attributes:
+ - enable-patchelf
+ override-build: |
+ snapcraftctl build
+ mv $CRAFT_PART_INSTALL/bin/hello-strict $CRAFT_PART_INSTALL/bin/hello-strict-existing-rpath
+ patchelf --force-rpath --set-rpath "\$ORIGIN/../fake-lib" $CRAFT_PART_INSTALL/bin/hello-strict-existing-rpath
+ hello-enable-patchelf:
+ source: .
+ plugin: make
+ build-packages:
+ - gcc
+ - libcurl4-openssl-dev
+ build-attributes:
+ - enable-patchelf
+ override-build: |
+ snapcraftctl build
+ mv $CRAFT_PART_INSTALL/bin/hello-strict $CRAFT_PART_INSTALL/bin/hello-strict-enable-patchelf
diff --git a/tests/spread/core22/patchelf/strict-patchelf/task.yaml b/tests/spread/core22/patchelf/strict-patchelf/task.yaml
new file mode 100644
index 0000000000..eb6848f97d
--- /dev/null
+++ b/tests/spread/core22/patchelf/strict-patchelf/task.yaml
@@ -0,0 +1,47 @@
+summary: Build a strict snap and validate elf patching
+
+prepare: |
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+ set_base "snap/snapcraft.yaml"
+
+ apt-get install patchelf dpkg-dev -y
+ apt-mark auto patchelf dpkg-dev
+restore: |
+ snapcraft clean
+ rm -f ./*.snap
+
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+ restore_yaml "snap/snapcraft.yaml"
+
+execute: |
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+ base="$(get_base)"
+
+ cat snap/snapcraft.yaml
+ snapcraft prime
+
+ arch_triplet="$(dpkg-architecture -q DEB_HOST_MULTIARCH)"
+
+ # Verify typical strict binary has an untouched rpath
+ patchelf --print-interpreter prime/bin/hello-strict | MATCH "^/lib.*ld.*.so.*"
+ rpath="$(patchelf --print-rpath prime/bin/hello-strict)"
+ if [[ -n "${rpath}" ]]; then
+ echo "found rpath on strict binary: ${rpath}"
+ exit 1
+ fi
+
+ # Account for /usr merge.
+ RPATH_MATCH="^/snap/$base/current/lib/$arch_triplet"
+ RPATH_ORIGIN_MATCH="^\\\$ORIGIN/../fake-lib:/snap/$base/current/lib/$arch_triplet"
+
+ # Verify binary rpath patching with existing rpath
+ patchelf --print-interpreter prime/bin/hello-strict-existing-rpath | MATCH "^/snap/$base/current/lib.*ld.*.so.*"
+ patchelf --print-rpath prime/bin/hello-strict-existing-rpath | MATCH "${RPATH_ORIGIN_MATCH}"
+
+ # Verify binary rpath patching without existing rpath
+ patchelf --print-interpreter prime/bin/hello-strict-enable-patchelf | MATCH "^/snap/$base/current/lib.*ld.*.so.*"
+ patchelf --print-rpath prime/bin/hello-strict-enable-patchelf | MATCH "${RPATH_MATCH}"
+
diff --git a/tests/spread/core22/remove-hook/snap/snapcraft.yaml b/tests/spread/core22/remove-hook/snap/snapcraft.yaml
new file mode 100644
index 0000000000..8273a13dd1
--- /dev/null
+++ b/tests/spread/core22/remove-hook/snap/snapcraft.yaml
@@ -0,0 +1,12 @@
+name: remove-hook
+base: core22
+version: '1.0'
+summary: 'remove-hook'
+description: Verify remove hook works.
+grade: stable
+confinement: strict
+
+parts:
+ hello-world:
+ plugin: nil
+ source: .
diff --git a/tests/spread/core22/remove-hook/task.yaml b/tests/spread/core22/remove-hook/task.yaml
new file mode 100644
index 0000000000..4bd64bc80c
--- /dev/null
+++ b/tests/spread/core22/remove-hook/task.yaml
@@ -0,0 +1,50 @@
+summary: Verify snapcraft remove hook deletes base images and instances
+
+prepare: |
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+ set_base "snap/snapcraft.yaml"
+
+restore: |
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+ restore_yaml "snap/snapcraft.yaml"
+
+ # reinstall snapcraft
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/prepare.sh"
+ install_snapcraft
+
+execute: |
+ unset SNAPCRAFT_BUILD_ENVIRONMENT
+
+ # create a base instance by running snapcraft
+ snapcraft pull --use-lxd --verbosity=trace
+
+ # verify base instance was created
+ instances="$(lxc list --project=snapcraft --format=csv --columns="n")"
+ if [[ ! " ${instances[*]} " =~ base-instance-snapcraft-.* ]]; then
+ echo "base instance was not created"
+ exit 1
+ fi
+
+ # previous version of snapcraft used base images, so manually create a base image
+ # to confirm it also gets deleted
+ lxc image copy --project=snapcraft craft-com.ubuntu.cloud-buildd:core22 local: --alias=snapshot-craft-com.ubuntu.cloud-buildd-core22-snapcraft-buildd-base-v0.0
+
+ # trigger the remove hook
+ snap remove snapcraft
+
+ # confirm base instance was deleted
+ instances="$(lxc list --project=snapcraft --format=csv --columns="n")"
+ if [[ " ${instances[*]} " =~ base-instance-snapcraft-.* ]]; then
+ echo "base instance was not deleted by the remove hook"
+ exit 1
+ fi
+
+ # confirm base image was deleted
+ images="$(lxc image list --project=snapcraft --format=csv --columns=l)"
+ if [[ " ${images[*]} " =~ snapshot-.* ]]; then
+ echo "base image was not deleted by the remove hook"
+ exit 1
+ fi
diff --git a/tests/spread/core22/try/snapcraft.yaml b/tests/spread/core22/try/snapcraft.yaml
new file mode 100644
index 0000000000..6eef9a1c72
--- /dev/null
+++ b/tests/spread/core22/try/snapcraft.yaml
@@ -0,0 +1,17 @@
+name: hello-try
+base: core22
+version: '0.1'
+summary: snapcraft try spread test
+description: snapcraft try spread test
+
+grade: stable
+confinement: strict
+
+apps:
+ hello-try:
+ command: usr/bin/hello
+
+parts:
+ hello-part:
+ plugin: nil
+ stage-packages: ["hello"]
diff --git a/tests/spread/core22/try/task.yaml b/tests/spread/core22/try/task.yaml
new file mode 100644
index 0000000000..c2e9bb2418
--- /dev/null
+++ b/tests/spread/core22/try/task.yaml
@@ -0,0 +1,17 @@
+summary: Test "snapcraft try" in core22
+
+execute: |
+ # TODO if we let `snapcraft try` create the dir, we get a permission error
+ # when trying to write to it from the instance.
+ mkdir prime
+ chmod a+w prime
+
+ unset SNAPCRAFT_BUILD_ENVIRONMENT
+ snapcraft try --use-lxd
+
+ find prime/meta/snap.yaml
+ find prime/usr/bin/hello
+
+ snap try prime
+ hello-try | MATCH "Hello, world"
+ snap remove hello-try
\ No newline at end of file
diff --git a/tests/spread/extensions/kde-neon/task.yaml b/tests/spread/extensions/kde-neon/task.yaml
index bf61ae3abd..dcdf6d3c39 100644
--- a/tests/spread/extensions/kde-neon/task.yaml
+++ b/tests/spread/extensions/kde-neon/task.yaml
@@ -4,9 +4,6 @@ summary: Build and run a basic kde snap using extensions
# available on a subset of all the architectures this testbed
# can run on.
systems:
- - ubuntu-18.04
- - ubuntu-18.04-64
- - ubuntu-18.04-amd64
- ubuntu-20.04
- ubuntu-20.04-64
- ubuntu-20.04-amd64
@@ -47,6 +44,10 @@ execute: |
[ -f "$snap_user_data/.last_revision" ]
[ "$(cat "$snap_user_data/.last_revision")" = "SNAP_DESKTOP_LAST_REVISION=x1" ]
+ # Verify content snap was installed for dependency checks.
+ snap list kde-frameworks-5-99-qt-5-15-7-core20
+ snap list gtk-common-themes
+
# Verify all dependencies were found.
if echo "$output" | grep -q "part is missing libraries"; then
echo "failed to find content snaps' libraries"
diff --git a/tests/spread/general/list-plugins/snapcraft.yaml b/tests/spread/general/list-plugins/snapcraft.yaml
new file mode 100644
index 0000000000..3b53d44f40
--- /dev/null
+++ b/tests/spread/general/list-plugins/snapcraft.yaml
@@ -0,0 +1,26 @@
+name: list-plugins
+base: core22
+version: '0.1'
+summary: "Test `list-plugins` and `plugins` can successfully parse plugins."
+description: |
+ This snapcraft.yaml includes regression tests to verify extensions, parse-info,
+ and advanced grammar are parsed before listing plugins.
+
+grade: stable
+confinement: strict
+
+apps:
+ list-plugins:
+ command: /bin/true
+ extensions: [gnome]
+
+parts:
+ nil:
+ plugin: nil
+ parse-info:
+ - usr/share/metainfo/photos.ansel.app.appdata.xml
+ stage-packages:
+ - mesa-opencl-icd
+ - ocl-icd-libopencl1
+ - on amd64:
+ - intel-opencl-icd
diff --git a/tests/spread/general/list-plugins/task.yaml b/tests/spread/general/list-plugins/task.yaml
new file mode 100644
index 0000000000..0fd620e89e
--- /dev/null
+++ b/tests/spread/general/list-plugins/task.yaml
@@ -0,0 +1,22 @@
+summary: "Test `list-plugins` and `plugins` can successfully parse plugins."
+
+prepare: |
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+ set_base "snapcraft.yaml"
+
+restore: |
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+ restore_yaml "snapcraft.yaml"
+
+execute: |
+ # verify both aliases executes without error
+ snapcraft list-plugins
+ snapcraft plugins
+
+ # verify the `--base` parameter executes without error
+ for base in "core18" "core20" "core22"; do
+ snapcraft list-plugins --base=$base
+ snapcraft plugins --base=$base
+ done
diff --git a/tests/spread/general/verbosity/task.yaml b/tests/spread/general/verbosity/task.yaml
new file mode 100644
index 0000000000..2aa9c5c31c
--- /dev/null
+++ b/tests/spread/general/verbosity/task.yaml
@@ -0,0 +1,26 @@
+summary: Test verbosity arguments
+
+prepare: |
+ snapcraft init
+
+ #shellcheck source=tests/spread/tools/snapcraft-yaml.sh
+ . "$TOOLS_DIR/snapcraft-yaml.sh"
+ set_base snap/snapcraft.yaml
+
+restore: |
+ rm -rf test-snap
+ rm -rf ./*.snap
+
+execute: |
+ unset SNAPCRAFT_ENABLE_DEVELOPER_DEBUG
+
+ # run with the default command
+ snapcraft --verbose
+
+ # run with an argument after a lifecycle command
+ snapcraft pull --verbose
+ snapcraft pull -v
+
+ # run with an argument before a lifecycle command
+ snapcraft --verbose pull
+ snapcraft -v pull
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/README.md b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/README.md
new file mode 100644
index 0000000000..2f699f48ac
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/README.md
@@ -0,0 +1,16 @@
+# flutter_hello
+
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev/), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/analysis_options.yaml b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/analysis_options.yaml
new file mode 100644
index 0000000000..61b6c4de17
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/analysis_options.yaml
@@ -0,0 +1,29 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at
+ # https://dart-lang.github.io/linter/lints/index.html.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/flutter_hello.iml b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/flutter_hello.iml
new file mode 100644
index 0000000000..0a2da097ac
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/flutter_hello.iml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/lib/main.dart b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/lib/main.dart
new file mode 100644
index 0000000000..e2ca568b72
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/lib/main.dart
@@ -0,0 +1,115 @@
+import 'package:flutter/material.dart';
+
+void main() {
+ runApp(const MyApp());
+}
+
+class MyApp extends StatelessWidget {
+ const MyApp({super.key});
+
+ // This widget is the root of your application.
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'Flutter Demo',
+ theme: ThemeData(
+ // This is the theme of your application.
+ //
+ // Try running your application with "flutter run". You'll see the
+ // application has a blue toolbar. Then, without quitting the app, try
+ // changing the primarySwatch below to Colors.green and then invoke
+ // "hot reload" (press "r" in the console where you ran "flutter run",
+ // or simply save your changes to "hot reload" in a Flutter IDE).
+ // Notice that the counter didn't reset back to zero; the application
+ // is not restarted.
+ primarySwatch: Colors.blue,
+ ),
+ home: const MyHomePage(title: 'hello world'),
+ );
+ }
+}
+
+class MyHomePage extends StatefulWidget {
+ const MyHomePage({super.key, required this.title});
+
+ // This widget is the home page of your application. It is stateful, meaning
+ // that it has a State object (defined below) that contains fields that affect
+ // how it looks.
+
+ // This class is the configuration for the state. It holds the values (in this
+ // case the title) provided by the parent (in this case the App widget) and
+ // used by the build method of the State. Fields in a Widget subclass are
+ // always marked "final".
+
+ final String title;
+
+ @override
+ State createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State {
+ int _counter = 0;
+
+ void _incrementCounter() {
+ setState(() {
+ // This call to setState tells the Flutter framework that something has
+ // changed in this State, which causes it to rerun the build method below
+ // so that the display can reflect the updated values. If we changed
+ // _counter without calling setState(), then the build method would not be
+ // called again, and so nothing would appear to happen.
+ _counter++;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // This method is rerun every time setState is called, for instance as done
+ // by the _incrementCounter method above.
+ //
+ // The Flutter framework has been optimized to make rerunning build methods
+ // fast, so that you can just rebuild anything that needs updating rather
+ // than having to individually change instances of widgets.
+ return Scaffold(
+ appBar: AppBar(
+ // Here we take the value from the MyHomePage object that was created by
+ // the App.build method, and use it to set our appbar title.
+ title: Text(widget.title),
+ ),
+ body: Center(
+ // Center is a layout widget. It takes a single child and positions it
+ // in the middle of the parent.
+ child: Column(
+ // Column is also a layout widget. It takes a list of children and
+ // arranges them vertically. By default, it sizes itself to fit its
+ // children horizontally, and tries to be as tall as its parent.
+ //
+ // Invoke "debug painting" (press "p" in the console, choose the
+ // "Toggle Debug Paint" action from the Flutter Inspector in Android
+ // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
+ // to see the wireframe for each widget.
+ //
+ // Column has various properties to control how it sizes itself and
+ // how it positions its children. Here we use mainAxisAlignment to
+ // center the children vertically; the main axis here is the vertical
+ // axis because Columns are vertical (the cross axis would be
+ // horizontal).
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Text(
+ 'You have pushed the button this many times:',
+ ),
+ Text(
+ '$_counter',
+ style: Theme.of(context).textTheme.headline4,
+ ),
+ ],
+ ),
+ ),
+ floatingActionButton: FloatingActionButton(
+ onPressed: _incrementCounter,
+ tooltip: 'Increment',
+ child: const Icon(Icons.add),
+ ), // This trailing comma makes auto-formatting nicer for build methods.
+ );
+ }
+}
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/.gitignore b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/.gitignore
new file mode 100644
index 0000000000..d3896c9844
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/.gitignore
@@ -0,0 +1 @@
+flutter/ephemeral
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/CMakeLists.txt b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/CMakeLists.txt
new file mode 100644
index 0000000000..12f2c270d1
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/CMakeLists.txt
@@ -0,0 +1,138 @@
+# Project-level configuration.
+cmake_minimum_required(VERSION 3.10)
+project(runner LANGUAGES CXX)
+
+# The name of the executable created for the application. Change this to change
+# the on-disk name of your application.
+set(BINARY_NAME "flutter_hello")
+# The unique GTK application identifier for this application. See:
+# https://wiki.gnome.org/HowDoI/ChooseApplicationID
+set(APPLICATION_ID "com.example.flutter_hello")
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(SET CMP0063 NEW)
+
+# Load bundled libraries from the lib/ directory relative to the binary.
+set(CMAKE_INSTALL_RPATH "$ORIGIN/lib")
+
+# Root filesystem for cross-building.
+if(FLUTTER_TARGET_PLATFORM_SYSROOT)
+ set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT})
+ set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT})
+ set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
+ set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
+ set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
+ set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
+endif()
+
+# Define build configuration options.
+if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
+ set(CMAKE_BUILD_TYPE "Debug" CACHE
+ STRING "Flutter build mode" FORCE)
+ set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
+ "Debug" "Profile" "Release")
+endif()
+
+# Compilation settings that should be applied to most targets.
+#
+# Be cautious about adding new options here, as plugins use this function by
+# default. In most cases, you should add new options to specific targets instead
+# of modifying this function.
+function(APPLY_STANDARD_SETTINGS TARGET)
+ target_compile_features(${TARGET} PUBLIC cxx_std_14)
+ target_compile_options(${TARGET} PRIVATE -Wall -Werror)
+ target_compile_options(${TARGET} PRIVATE "$<$>:-O3>")
+ target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>")
+endfunction()
+
+# Flutter library and tool build rules.
+set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter")
+add_subdirectory(${FLUTTER_MANAGED_DIR})
+
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+
+add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}")
+
+# Define the application target. To change its name, change BINARY_NAME above,
+# not the value here, or `flutter run` will no longer work.
+#
+# Any new source files that you add to the application should be added here.
+add_executable(${BINARY_NAME}
+ "main.cc"
+ "my_application.cc"
+ "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc"
+)
+
+# Apply the standard set of build settings. This can be removed for applications
+# that need different build settings.
+apply_standard_settings(${BINARY_NAME})
+
+# Add dependency libraries. Add any application-specific dependencies here.
+target_link_libraries(${BINARY_NAME} PRIVATE flutter)
+target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK)
+
+# Run the Flutter tool portions of the build. This must not be removed.
+add_dependencies(${BINARY_NAME} flutter_assemble)
+
+# Only the install-generated bundle's copy of the executable will launch
+# correctly, since the resources must in the right relative locations. To avoid
+# people trying to run the unbundled copy, put it in a subdirectory instead of
+# the default top-level location.
+set_target_properties(${BINARY_NAME}
+ PROPERTIES
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run"
+)
+
+# Generated plugin build rules, which manage building the plugins and adding
+# them to the application.
+include(flutter/generated_plugins.cmake)
+
+
+# === Installation ===
+# By default, "installing" just makes a relocatable bundle in the build
+# directory.
+set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle")
+if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
+ set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
+endif()
+
+# Start with a clean build bundle directory every time.
+install(CODE "
+ file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\")
+ " COMPONENT Runtime)
+
+set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data")
+set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib")
+
+install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}"
+ COMPONENT Runtime)
+
+install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+
+foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES})
+ install(FILES "${bundled_library}"
+ DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endforeach(bundled_library)
+
+# Fully re-copy the assets directory on each build to avoid having stale files
+# from a previous install.
+set(FLUTTER_ASSET_DIR_NAME "flutter_assets")
+install(CODE "
+ file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\")
+ " COMPONENT Runtime)
+install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}"
+ DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime)
+
+# Install the AOT library on non-Debug builds only.
+if(NOT CMAKE_BUILD_TYPE MATCHES "Debug")
+ install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}"
+ COMPONENT Runtime)
+endif()
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/CMakeLists.txt b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/CMakeLists.txt
new file mode 100644
index 0000000000..d5bd01648a
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/CMakeLists.txt
@@ -0,0 +1,88 @@
+# This file controls Flutter-level build steps. It should not be edited.
+cmake_minimum_required(VERSION 3.10)
+
+set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
+
+# Configuration provided via flutter tool.
+include(${EPHEMERAL_DIR}/generated_config.cmake)
+
+# TODO: Move the rest of this into files in ephemeral. See
+# https://github.com/flutter/flutter/issues/57146.
+
+# Serves the same purpose as list(TRANSFORM ... PREPEND ...),
+# which isn't available in 3.10.
+function(list_prepend LIST_NAME PREFIX)
+ set(NEW_LIST "")
+ foreach(element ${${LIST_NAME}})
+ list(APPEND NEW_LIST "${PREFIX}${element}")
+ endforeach(element)
+ set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
+endfunction()
+
+# === Flutter Library ===
+# System-level dependencies.
+find_package(PkgConfig REQUIRED)
+pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
+pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
+pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
+
+set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
+
+# Published to parent scope for install step.
+set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
+set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
+set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
+set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
+
+list(APPEND FLUTTER_LIBRARY_HEADERS
+ "fl_basic_message_channel.h"
+ "fl_binary_codec.h"
+ "fl_binary_messenger.h"
+ "fl_dart_project.h"
+ "fl_engine.h"
+ "fl_json_message_codec.h"
+ "fl_json_method_codec.h"
+ "fl_message_codec.h"
+ "fl_method_call.h"
+ "fl_method_channel.h"
+ "fl_method_codec.h"
+ "fl_method_response.h"
+ "fl_plugin_registrar.h"
+ "fl_plugin_registry.h"
+ "fl_standard_message_codec.h"
+ "fl_standard_method_codec.h"
+ "fl_string_codec.h"
+ "fl_value.h"
+ "fl_view.h"
+ "flutter_linux.h"
+)
+list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
+add_library(flutter INTERFACE)
+target_include_directories(flutter INTERFACE
+ "${EPHEMERAL_DIR}"
+)
+target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
+target_link_libraries(flutter INTERFACE
+ PkgConfig::GTK
+ PkgConfig::GLIB
+ PkgConfig::GIO
+)
+add_dependencies(flutter flutter_assemble)
+
+# === Flutter tool backend ===
+# _phony_ is a non-existent file to force this command to run every time,
+# since currently there's no way to get a full input/output list from the
+# flutter tool.
+add_custom_command(
+ OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
+ ${CMAKE_CURRENT_BINARY_DIR}/_phony_
+ COMMAND ${CMAKE_COMMAND} -E env
+ ${FLUTTER_TOOL_ENVIRONMENT}
+ "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
+ ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
+ VERBATIM
+)
+add_custom_target(flutter_assemble DEPENDS
+ "${FLUTTER_LIBRARY}"
+ ${FLUTTER_LIBRARY_HEADERS}
+)
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/generated_plugin_registrant.cc b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/generated_plugin_registrant.cc
new file mode 100644
index 0000000000..e71a16d23d
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/generated_plugin_registrant.cc
@@ -0,0 +1,11 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#include "generated_plugin_registrant.h"
+
+
+void fl_register_plugins(FlPluginRegistry* registry) {
+}
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/generated_plugin_registrant.h b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/generated_plugin_registrant.h
new file mode 100644
index 0000000000..e0f0a47bc0
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/generated_plugin_registrant.h
@@ -0,0 +1,15 @@
+//
+// Generated file. Do not edit.
+//
+
+// clang-format off
+
+#ifndef GENERATED_PLUGIN_REGISTRANT_
+#define GENERATED_PLUGIN_REGISTRANT_
+
+#include
+
+// Registers Flutter plugins.
+void fl_register_plugins(FlPluginRegistry* registry);
+
+#endif // GENERATED_PLUGIN_REGISTRANT_
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/generated_plugins.cmake b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/generated_plugins.cmake
new file mode 100644
index 0000000000..2e1de87a7e
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/flutter/generated_plugins.cmake
@@ -0,0 +1,23 @@
+#
+# Generated file, do not edit.
+#
+
+list(APPEND FLUTTER_PLUGIN_LIST
+)
+
+list(APPEND FLUTTER_FFI_PLUGIN_LIST
+)
+
+set(PLUGIN_BUNDLED_LIBRARIES)
+
+foreach(plugin ${FLUTTER_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin})
+ target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES $)
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries})
+endforeach(plugin)
+
+foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST})
+ add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin})
+ list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries})
+endforeach(ffi_plugin)
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/main.cc b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/main.cc
new file mode 100644
index 0000000000..e7c5c54370
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/main.cc
@@ -0,0 +1,6 @@
+#include "my_application.h"
+
+int main(int argc, char** argv) {
+ g_autoptr(MyApplication) app = my_application_new();
+ return g_application_run(G_APPLICATION(app), argc, argv);
+}
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/my_application.cc b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/my_application.cc
new file mode 100644
index 0000000000..9e28713e54
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/my_application.cc
@@ -0,0 +1,104 @@
+#include "my_application.h"
+
+#include
+#ifdef GDK_WINDOWING_X11
+#include
+#endif
+
+#include "flutter/generated_plugin_registrant.h"
+
+struct _MyApplication {
+ GtkApplication parent_instance;
+ char** dart_entrypoint_arguments;
+};
+
+G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
+
+// Implements GApplication::activate.
+static void my_application_activate(GApplication* application) {
+ MyApplication* self = MY_APPLICATION(application);
+ GtkWindow* window =
+ GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
+
+ // Use a header bar when running in GNOME as this is the common style used
+ // by applications and is the setup most users will be using (e.g. Ubuntu
+ // desktop).
+ // If running on X and not using GNOME then just use a traditional title bar
+ // in case the window manager does more exotic layout, e.g. tiling.
+ // If running on Wayland assume the header bar will work (may need changing
+ // if future cases occur).
+ gboolean use_header_bar = TRUE;
+#ifdef GDK_WINDOWING_X11
+ GdkScreen* screen = gtk_window_get_screen(window);
+ if (GDK_IS_X11_SCREEN(screen)) {
+ const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen);
+ if (g_strcmp0(wm_name, "GNOME Shell") != 0) {
+ use_header_bar = FALSE;
+ }
+ }
+#endif
+ if (use_header_bar) {
+ GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new());
+ gtk_widget_show(GTK_WIDGET(header_bar));
+ gtk_header_bar_set_title(header_bar, "flutter_hello");
+ gtk_header_bar_set_show_close_button(header_bar, TRUE);
+ gtk_window_set_titlebar(window, GTK_WIDGET(header_bar));
+ } else {
+ gtk_window_set_title(window, "flutter_hello");
+ }
+
+ gtk_window_set_default_size(window, 1280, 720);
+ gtk_widget_show(GTK_WIDGET(window));
+
+ g_autoptr(FlDartProject) project = fl_dart_project_new();
+ fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
+
+ FlView* view = fl_view_new(project);
+ gtk_widget_show(GTK_WIDGET(view));
+ gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
+
+ fl_register_plugins(FL_PLUGIN_REGISTRY(view));
+
+ gtk_widget_grab_focus(GTK_WIDGET(view));
+}
+
+// Implements GApplication::local_command_line.
+static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) {
+ MyApplication* self = MY_APPLICATION(application);
+ // Strip out the first argument as it is the binary name.
+ self->dart_entrypoint_arguments = g_strdupv(*arguments + 1);
+
+ g_autoptr(GError) error = nullptr;
+ if (!g_application_register(application, nullptr, &error)) {
+ g_warning("Failed to register: %s", error->message);
+ *exit_status = 1;
+ return TRUE;
+ }
+
+ g_application_activate(application);
+ *exit_status = 0;
+
+ return TRUE;
+}
+
+// Implements GObject::dispose.
+static void my_application_dispose(GObject* object) {
+ MyApplication* self = MY_APPLICATION(object);
+ g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev);
+ G_OBJECT_CLASS(my_application_parent_class)->dispose(object);
+}
+
+static void my_application_class_init(MyApplicationClass* klass) {
+ G_APPLICATION_CLASS(klass)->activate = my_application_activate;
+ G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line;
+ G_OBJECT_CLASS(klass)->dispose = my_application_dispose;
+}
+
+static void my_application_init(MyApplication* self) {}
+
+MyApplication* my_application_new() {
+ return MY_APPLICATION(g_object_new(my_application_get_type(),
+ "application-id", APPLICATION_ID,
+ "flags", G_APPLICATION_NON_UNIQUE,
+ nullptr));
+}
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/my_application.h b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/my_application.h
new file mode 100644
index 0000000000..72271d5e41
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/linux/my_application.h
@@ -0,0 +1,18 @@
+#ifndef FLUTTER_MY_APPLICATION_H_
+#define FLUTTER_MY_APPLICATION_H_
+
+#include
+
+G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION,
+ GtkApplication)
+
+/**
+ * my_application_new:
+ *
+ * Creates a new Flutter-based application.
+ *
+ * Returns: a new #MyApplication.
+ */
+MyApplication* my_application_new();
+
+#endif // FLUTTER_MY_APPLICATION_H_
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/pubspec.lock b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/pubspec.lock
new file mode 100644
index 0000000000..0f3a736aea
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/pubspec.lock
@@ -0,0 +1,160 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ async:
+ dependency: transitive
+ description:
+ name: async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.9.0"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.1"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.1"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.16.0"
+ cupertino_icons:
+ dependency: "direct main"
+ description:
+ name: cupertino_icons
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.0.5"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.3.1"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_lints:
+ dependency: "direct dev"
+ description:
+ name: flutter_lints
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.1"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ lints:
+ dependency: transitive
+ description:
+ name: lints
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.0.1"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.12.12"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.1.5"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.8.2"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.99"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.9.0"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.10.0"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.0"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.1.1"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "1.2.1"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.4.12"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "2.1.2"
+sdks:
+ dart: ">=2.18.6 <3.0.0"
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/pubspec.yaml b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/pubspec.yaml
new file mode 100644
index 0000000000..da4590f864
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/pubspec.yaml
@@ -0,0 +1,91 @@
+name: flutter_hello
+description: A new Flutter project.
+
+# The following line prevents the package from being accidentally published to
+# pub.dev using `flutter pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+# In Windows, build-name is used as the major, minor, and patch parts
+# of the product and file versions while build-number is used as the build suffix.
+version: 1.0.0+1
+
+environment:
+ sdk: '>=2.18.6 <3.0.0'
+
+# Dependencies specify other packages that your package needs in order to work.
+# To automatically upgrade your package dependencies to the latest versions
+# consider running `flutter pub upgrade --major-versions`. Alternatively,
+# dependencies can be manually updated by changing the version numbers below to
+# the latest version available on pub.dev. To see which dependencies have newer
+# versions available, run `flutter pub outdated`.
+dependencies:
+ flutter:
+ sdk: flutter
+
+
+ # The following adds the Cupertino Icons font to your application.
+ # Use with the CupertinoIcons class for iOS style icons.
+ cupertino_icons: ^1.0.2
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+ # The "flutter_lints" package below contains a set of recommended lints to
+ # encourage good coding practices. The lint set provided by the package is
+ # activated in the `analysis_options.yaml` file located at the root of your
+ # package. See that file for information about deactivating specific lint
+ # rules and activating additional ones.
+ flutter_lints: ^2.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+
+ # The following line ensures that the Material Icons font is
+ # included with your application, so that you can use the icons in
+ # the material Icons class.
+ uses-material-design: true
+
+ # To add assets to your application, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/assets-and-images/#resolution-aware
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/assets-and-images/#from-packages
+
+ # To add custom fonts to your application, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts from package dependencies,
+ # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/snap/snapcraft.yaml b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/snap/snapcraft.yaml
new file mode 100644
index 0000000000..bbe6ff6e93
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/snap/snapcraft.yaml
@@ -0,0 +1,16 @@
+name: flutter-hello
+version: "1.0"
+summary: simple flutter application
+description: build a flutter application using core22
+base: core22
+confinement: strict
+
+apps:
+ flutter-hello:
+ command: "flutter_hello"
+ extensions: [gnome]
+
+parts:
+ hello:
+ source: .
+ plugin: flutter
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/test/widget_test.dart b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/test/widget_test.dart
new file mode 100644
index 0000000000..f5811219ce
--- /dev/null
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/flutter-hello/test/widget_test.dart
@@ -0,0 +1,30 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:flutter_hello/main.dart';
+
+void main() {
+ testWidgets('Counter increments smoke test', (WidgetTester tester) async {
+ // Build our app and trigger a frame.
+ await tester.pumpWidget(const MyApp());
+
+ // Verify that our counter starts at 0.
+ expect(find.text('0'), findsOneWidget);
+ expect(find.text('1'), findsNothing);
+
+ // Tap the '+' icon and trigger a frame.
+ await tester.tap(find.byIcon(Icons.add));
+ await tester.pump();
+
+ // Verify that our counter has incremented.
+ expect(find.text('0'), findsNothing);
+ expect(find.text('1'), findsOneWidget);
+ });
+}
diff --git a/tests/spread/plugins/craft-parts/build-and-run-hello/task.yaml b/tests/spread/plugins/craft-parts/build-and-run-hello/task.yaml
index 33c47f77a6..428094a170 100644
--- a/tests/spread/plugins/craft-parts/build-and-run-hello/task.yaml
+++ b/tests/spread/plugins/craft-parts/build-and-run-hello/task.yaml
@@ -5,6 +5,7 @@ summary: >-
environment:
SNAP/conda: conda-hello
SNAP/colcon_ros2_humble: colcon-ros2-humble-hello
+ SNAP/flutter: flutter-hello
SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"
prepare: |
@@ -26,6 +27,7 @@ restore: |
[ -f src/hello.cpp ] && git checkout src/hello.cpp
[ -f src/main.rs ] && git checkout src/main.rs
[ -f lib/src/lib.rs ] && git checkout lib/src/lib.rs
+ [ -f lib/main.dart ] && git checkout lib/main.dart
#shellcheck source=tests/spread/tools/snapcraft-yaml.sh
. "$TOOLS_DIR/snapcraft-yaml.sh"
@@ -40,13 +42,19 @@ execute: |
# Build what we have and verify the snap runs as expected.
snapcraft
snap install "${SNAP}"_1.0_*.snap --dangerous
- [ "$($SNAP)" = "hello world" ]
+
+ if [ "${SNAP}" != "flutter-hello" ]; then
+ [ "$($SNAP)" = "hello world" ]
+ fi
# Clean the hello part, then build and run again.
snapcraft clean hello
snapcraft
snap install "${SNAP}"_1.0_*.snap --dangerous
- [ "$($SNAP)" = "hello world" ]
+
+ if [ "${SNAP}" != "flutter-hello" ]; then
+ [ "$($SNAP)" = "hello world" ]
+ fi
# Make sure that what we built runs with the changes applied.
if [ -f hello ]; then
@@ -65,6 +73,8 @@ execute: |
modified_file=src/main.rs
elif [ -f say/src/lib.rs ]; then
modified_file=say/src/lib.rs
+ elif [ -f lib/main.dart ]; then
+ modified_file=lib/main.dart
else
FATAL "Cannot setup ${SNAP} for rebuilding"
fi
@@ -73,4 +83,7 @@ execute: |
snapcraft
snap install "${SNAP}"_1.0_*.snap --dangerous
- [ "$($SNAP)" = "hello rebuilt world" ]
+
+ if [ "${SNAP}" != "flutter-hello" ]; then
+ [ "$($SNAP)" = "hello rebuilt world" ]
+ fi
diff --git a/tests/spread/plugins/v1/gradle/snaps/gradlew-hello/gradlew b/tests/spread/plugins/v1/gradle/snaps/gradlew-hello/gradlew
index 27309d9231..23d6786470 100755
--- a/tests/spread/plugins/v1/gradle/snaps/gradlew-hello/gradlew
+++ b/tests/spread/plugins/v1/gradle/snaps/gradlew-hello/gradlew
@@ -1,5 +1,7 @@
#!/usr/bin/env bash
+# shellcheck disable=all
+
##############################################################################
##
## Gradle start up script for UN*X
diff --git a/tests/spread/plugins/v2/catkin-ros1-catkin-packages-ignore/task.yaml b/tests/spread/plugins/v2/catkin-ros1-catkin-packages-ignore/task.yaml
index 7729f57f49..9ec4cbe8da 100644
--- a/tests/spread/plugins/v2/catkin-ros1-catkin-packages-ignore/task.yaml
+++ b/tests/spread/plugins/v2/catkin-ros1-catkin-packages-ignore/task.yaml
@@ -5,7 +5,6 @@ kill-timeout: 180m
environment:
SNAP/catkin_catkin_packages_ignore: catkin-catkin-packages-ignore
- SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"
systems:
- ubuntu-20.04
diff --git a/tests/spread/plugins/v2/catkin-ros1-run/task.yaml b/tests/spread/plugins/v2/catkin-ros1-run/task.yaml
index 43898d703d..44486fb896 100644
--- a/tests/spread/plugins/v2/catkin-ros1-run/task.yaml
+++ b/tests/spread/plugins/v2/catkin-ros1-run/task.yaml
@@ -4,7 +4,6 @@ kill-timeout: 180m
environment:
SNAP/catkin_ros1_run: catkin-ros1-run
- SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"
systems:
- ubuntu-20.04
diff --git a/tests/spread/plugins/v2/colcon-ros2-daemon/task.yaml b/tests/spread/plugins/v2/colcon-ros2-daemon/task.yaml
index 5b2c710db5..e35c624bba 100644
--- a/tests/spread/plugins/v2/colcon-ros2-daemon/task.yaml
+++ b/tests/spread/plugins/v2/colcon-ros2-daemon/task.yaml
@@ -4,7 +4,6 @@ priority: 100 # Run this test early so we're not waiting for it
environment:
SNAP_DIR: ../snaps/colcon-daemon
- SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"
prepare: |
#shellcheck source=tests/spread/tools/snapcraft-yaml.sh
diff --git a/tests/spread/plugins/v2/colcon-ros2-hello/task.yaml b/tests/spread/plugins/v2/colcon-ros2-hello/task.yaml
index 545e2c612e..4133d7eeae 100644
--- a/tests/spread/plugins/v2/colcon-ros2-hello/task.yaml
+++ b/tests/spread/plugins/v2/colcon-ros2-hello/task.yaml
@@ -7,7 +7,6 @@ kill-timeout: 180m
environment:
SNAP/colcon_ros2_foxy_rlcpp_hello: colcon-ros2-foxy-rlcpp-hello
SNAP/colcon_subdir: colcon-subdir
- SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"
systems:
- ubuntu-20.04
diff --git a/tests/spread/plugins/v2/colcon-ros2-stage-snap/task.yaml b/tests/spread/plugins/v2/colcon-ros2-stage-snap/task.yaml
index f920c99761..46a0b62dc8 100644
--- a/tests/spread/plugins/v2/colcon-ros2-stage-snap/task.yaml
+++ b/tests/spread/plugins/v2/colcon-ros2-stage-snap/task.yaml
@@ -6,7 +6,6 @@ kill-timeout: 180m
environment:
SNAP/colcon_stage_snaps: colcon-stage-snaps
- SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"
systems:
- ubuntu-20.04
diff --git a/tests/spread/plugins/v2/ros-plugins-error-code/task.yaml b/tests/spread/plugins/v2/ros-plugins-error-code/task.yaml
index bd9d96dcb8..ae364f484c 100644
--- a/tests/spread/plugins/v2/ros-plugins-error-code/task.yaml
+++ b/tests/spread/plugins/v2/ros-plugins-error-code/task.yaml
@@ -7,7 +7,6 @@ environment:
SNAP/catkin_noetic_hello: catkin-noetic-hello
SNAP/catkin_tools_noetic_hello: catkin-tools-noetic-hello
SNAP/colcon_ros2_foxy_rlcpp_hello: colcon-ros2-foxy-rlcpp-hello
- SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"
systems:
- ubuntu-20.04
diff --git a/tests/spread/plugins/v2/ros1-cmake-args/task.yaml b/tests/spread/plugins/v2/ros1-cmake-args/task.yaml
index 31e3c29acb..4adbfbb72b 100644
--- a/tests/spread/plugins/v2/ros1-cmake-args/task.yaml
+++ b/tests/spread/plugins/v2/ros1-cmake-args/task.yaml
@@ -6,7 +6,6 @@ kill-timeout: 180m
environment:
SNAP/catkin_cmake_args: catkin-cmake-args
SNAP/catkin_tools_cmake_args: catkin-tools-cmake-args
- SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"
systems:
- ubuntu-20.04
diff --git a/tests/spread/plugins/v2/ros1-hello/task.yaml b/tests/spread/plugins/v2/ros1-hello/task.yaml
index c8bbff6c66..3e483e96e9 100644
--- a/tests/spread/plugins/v2/ros1-hello/task.yaml
+++ b/tests/spread/plugins/v2/ros1-hello/task.yaml
@@ -9,7 +9,6 @@ environment:
SNAP/catkin_noetic_subdir: catkin-noetic-subdir
SNAP/catkin_tools_noetic_hello: catkin-tools-noetic-hello
SNAP/catkin_tools_noetic_subdir: catkin-tools-noetic-subdir
- SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"
systems:
- ubuntu-20.04
diff --git a/tests/spread/plugins/v2/ros1-in-source-hello/task.yaml b/tests/spread/plugins/v2/ros1-in-source-hello/task.yaml
index a0cbfafa9c..ac2dc355d1 100644
--- a/tests/spread/plugins/v2/ros1-in-source-hello/task.yaml
+++ b/tests/spread/plugins/v2/ros1-in-source-hello/task.yaml
@@ -7,7 +7,6 @@ kill-timeout: 180m
environment:
SNAP/catkin_noetic_in_source_hello: catkin-noetic-in-source-hello
SNAP/catkin_tools_noetic_in_source_hello: catkin-tools-noetic-in-source-hello
- SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"
systems:
- ubuntu-20.04
diff --git a/tests/spread/plugins/v2/ros1-specific-packages/task.yaml b/tests/spread/plugins/v2/ros1-specific-packages/task.yaml
index 6b0824a2f1..6f160b940b 100644
--- a/tests/spread/plugins/v2/ros1-specific-packages/task.yaml
+++ b/tests/spread/plugins/v2/ros1-specific-packages/task.yaml
@@ -6,7 +6,6 @@ kill-timeout: 180m
environment:
SNAP/catkin_specific_packages: catkin-specific-packages
SNAP/catkin_tools_specific_packages: catkin-tools-specific-packages
- SNAPCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS: "1"
systems:
- ubuntu-20.04
diff --git a/tests/spread/providers/legacy/multipass-basic/task.yaml b/tests/spread/providers/multipass-basic/task.yaml
similarity index 74%
rename from tests/spread/providers/legacy/multipass-basic/task.yaml
rename to tests/spread/providers/multipass-basic/task.yaml
index c7a7084ca3..b08200ea7d 100644
--- a/tests/spread/providers/legacy/multipass-basic/task.yaml
+++ b/tests/spread/providers/multipass-basic/task.yaml
@@ -1,8 +1,10 @@
summary: Build a basic snap using multipass and ensure that it runs
+
+# this test does not work on google's spread runners due to a lack of virtualization support
manual: true
environment:
- SNAP_DIR: ../../snaps/make-hello
+ SNAP_DIR: ../snaps/make-hello
prepare: |
snap list multipass || snap install --classic multipass
@@ -14,8 +16,7 @@ prepare: |
restore: |
cd "$SNAP_DIR"
- # Unset SNAPCRAFT_BUILD_ENVIRONMENT=host.
- unset SNAPCRAFT_BUILD_ENVIRONMENT
+ export SNAPCRAFT_BUILD_ENVIRONMENT=multipass
snapcraft clean
rm -f ./*.snap
@@ -27,8 +28,7 @@ restore: |
execute: |
cd "$SNAP_DIR"
- # Unset SNAPCRAFT_BUILD_ENVIRONMENT=host.
- unset SNAPCRAFT_BUILD_ENVIRONMENT
+ export SNAPCRAFT_BUILD_ENVIRONMENT=multipass
snapcraft
sudo snap install make-hello_*.snap --dangerous
diff --git a/tests/spread/providers/legacy/multipass-error-handling/task.yaml b/tests/spread/providers/multipass-error-handling/task.yaml
similarity index 79%
rename from tests/spread/providers/legacy/multipass-error-handling/task.yaml
rename to tests/spread/providers/multipass-error-handling/task.yaml
index 01075d49f9..0d21475426 100644
--- a/tests/spread/providers/legacy/multipass-error-handling/task.yaml
+++ b/tests/spread/providers/multipass-error-handling/task.yaml
@@ -1,8 +1,10 @@
summary: A faulty snap to test error handling
+
+# this test does not work on google's spread runners due to a lack of virtualization support
manual: true
environment:
- SNAP_DIR: ../../snaps/exit1
+ SNAP_DIR: ../snaps/exit1
prepare: |
snap list multipass || snap install --classic multipass
@@ -14,8 +16,7 @@ prepare: |
restore: |
cd "$SNAP_DIR"
- # Unset SNAPCRAFT_BUILD_ENVIRONMENT=host.
- unset SNAPCRAFT_BUILD_ENVIRONMENT
+ export SNAPCRAFT_BUILD_ENVIRONMENT=multipass
snapcraft clean
rm -f ./*.snap
@@ -27,8 +28,7 @@ restore: |
execute: |
cd "$SNAP_DIR"
- # Unset SNAPCRAFT_BUILD_ENVIRONMENT=host.
- unset SNAPCRAFT_BUILD_ENVIRONMENT
+ export SNAPCRAFT_BUILD_ENVIRONMENT=multipass
# Building this snap returns an error inside the provider. This error must
# be handled by the outer environment and return code 2 instead of raising
diff --git a/tests/spread/tools/prepare.sh b/tests/spread/tools/prepare.sh
new file mode 100644
index 0000000000..75d3de6f65
--- /dev/null
+++ b/tests/spread/tools/prepare.sh
@@ -0,0 +1,18 @@
+#!/bin/bash -e
+
+install_snapcraft()
+{
+ # If $SNAPCRAFT_CHANNEL is defined, install snapcraft from that channel.
+ # Otherwise, look for it in /snapcraft/.
+ if [ -z "$SNAPCRAFT_CHANNEL" ]; then
+ if stat /snapcraft/tests/*.snap 2>/dev/null; then
+ snap install --classic --dangerous /snapcraft/tests/*.snap
+ else
+ echo "Expected a snap to exist in /snapcraft/tests/. If your intention"\
+ "was to install from the store, set \$SNAPCRAFT_CHANNEL."
+ exit 1
+ fi
+ else
+ snap install --classic snapcraft --channel="$SNAPCRAFT_CHANNEL"
+ fi
+}
diff --git a/tests/spread/tools/snapd-testing-tools b/tests/spread/tools/snapd-testing-tools
new file mode 160000
index 0000000000..d5cb68cb95
--- /dev/null
+++ b/tests/spread/tools/snapd-testing-tools
@@ -0,0 +1 @@
+Subproject commit d5cb68cb9558f60b019bb8304f3e010d83425044
diff --git a/tests/unit/cli/test_verbosity.py b/tests/unit/cli/test_verbosity.py
index 40ff1f6780..7e108ebbda 100644
--- a/tests/unit/cli/test_verbosity.py
+++ b/tests/unit/cli/test_verbosity.py
@@ -15,12 +15,14 @@
# along with this program. If not, see .
import pytest
+from craft_cli import ArgumentParsingError
from snapcraft import cli
@pytest.mark.parametrize("value", ["yes", "YES", "1", "y", "Y"])
def test_developer_debug_in_env_sets_debug(monkeypatch, value):
+ """DEVELOPER_DEBUG sets verbosity level to 'debug'."""
monkeypatch.setenv("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG", value)
assert cli.get_verbosity() == cli.EmitterMode.DEBUG
@@ -28,6 +30,7 @@ def test_developer_debug_in_env_sets_debug(monkeypatch, value):
@pytest.mark.parametrize("value", ["no", "NO", "0", "n", "N"])
def test_developer_debug_in_env_sets_brief(monkeypatch, value):
+ """Default verbosity is used when DEVELOPER_DEBUG=0."""
monkeypatch.setenv("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG", value)
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
@@ -36,6 +39,7 @@ def test_developer_debug_in_env_sets_brief(monkeypatch, value):
@pytest.mark.parametrize("value", ["foo", "BAR", "2"])
def test_parse_issue_in_developer_debug_in_env_sets_brief(monkeypatch, value):
+ """Invalid values for DEVELOPER_DEBUG are silently ignored."""
monkeypatch.setenv("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG", value)
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
@@ -43,6 +47,7 @@ def test_parse_issue_in_developer_debug_in_env_sets_brief(monkeypatch, value):
def test_no_env_returns_brief(monkeypatch):
+ """Default verbosity is used when there is no value for DEVELOPER_DEBUG."""
monkeypatch.delenv("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG", False)
monkeypatch.setattr("sys.stdin.isatty", lambda: True)
@@ -50,6 +55,8 @@ def test_no_env_returns_brief(monkeypatch):
def test_sdtin_no_tty_returns_verbose(monkeypatch):
+ """Default verbosity for environments with a closed stdin is used when there is no
+ value for DEVELOPER_DEBUG."""
monkeypatch.delenv("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG", False)
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
@@ -57,7 +64,35 @@ def test_sdtin_no_tty_returns_verbose(monkeypatch):
def test_developer_debug_and_sdtin_no_tty_returns_debug(monkeypatch):
- monkeypatch.setenv("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG", True)
+ """DEVELOPER_DEBUG sets verbosity in an environment with a closed stdin."""
+ monkeypatch.setenv("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG", "y")
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
assert cli.get_verbosity() == cli.EmitterMode.DEBUG
+
+
+@pytest.mark.parametrize("developer_debug", ["n", "y"])
+@pytest.mark.parametrize("isatty", [False, True])
+@pytest.mark.parametrize("verbosity", ["quiet", "brief", "verbose", "debug", "trace"])
+def test_env_var(monkeypatch, developer_debug, isatty, verbosity):
+ """Environment variable sets verbosity, even when DEVELOPER_DEBUG is defined or
+ stdin is closed."""
+ monkeypatch.setenv("SNAPCRAFT_VERBOSITY_LEVEL", verbosity)
+ monkeypatch.setenv("SNAPCRAFT_ENABLE_DEVELOPER_DEBUG", developer_debug)
+ monkeypatch.setattr("sys.stdin.isatty", lambda: isatty)
+
+ assert cli.get_verbosity() == cli.EmitterMode[verbosity.upper()]
+
+
+def test_env_var_invalid(monkeypatch):
+ """Error is raised when environmental variable is invalid."""
+ monkeypatch.setenv("SNAPCRAFT_VERBOSITY_LEVEL", "invalid")
+
+ with pytest.raises(ArgumentParsingError) as raised:
+ cli.get_verbosity()
+
+ assert str(raised.value) == (
+ "cannot parse verbosity level 'invalid' from environment "
+ "variable SNAPCRAFT_VERBOSITY_LEVEL (valid values are 'quiet', 'brief', "
+ "'verbose', 'debug', and 'trace')"
+ )
diff --git a/tests/unit/commands/test_lifecycle.py b/tests/unit/commands/test_lifecycle.py
index 30d0cae852..adbe57723d 100644
--- a/tests/unit/commands/test_lifecycle.py
+++ b/tests/unit/commands/test_lifecycle.py
@@ -27,6 +27,7 @@
PullCommand,
SnapCommand,
StageCommand,
+ TryCommand,
)
@@ -38,6 +39,7 @@
("stage", StageCommand),
("prime", PrimeCommand),
("clean", CleanCommand),
+ ("try", TryCommand),
],
)
def test_lifecycle_command(cmd_name, cmd_class, mocker):
diff --git a/tests/unit/commands/test_list_extensions.py b/tests/unit/commands/test_list_extensions.py
index 95417e0715..2fef48836d 100644
--- a/tests/unit/commands/test_list_extensions.py
+++ b/tests/unit/commands/test_list_extensions.py
@@ -31,7 +31,7 @@ def test_command(emitter, command):
dedent(
"""\
Extension name Supported bases
- ---------------- -----------------
+ ---------------- ----------------------
fake-extension core22
flutter-beta core18
flutter-dev core18
@@ -41,7 +41,7 @@ def test_command(emitter, command):
gnome-3-28 core18
gnome-3-34 core18
gnome-3-38 core20
- kde-neon core18, core20
+ kde-neon core18, core20, core22
ros1-noetic core20
ros2-foxy core20
ros2-humble core22"""
@@ -58,7 +58,7 @@ def test_command_extension_dups(emitter, command):
dedent(
"""\
Extension name Supported bases
- ---------------- -----------------
+ ---------------- ----------------------
flutter-beta core18
flutter-dev core18
flutter-master core18
@@ -67,7 +67,7 @@ def test_command_extension_dups(emitter, command):
gnome-3-28 core18
gnome-3-34 core18
gnome-3-38 core20
- kde-neon core18, core20
+ kde-neon core18, core20, core22
ros1-noetic core20
ros2-foxy core20
ros2-humble core22"""
diff --git a/tests/unit/elf/test_elf_file.py b/tests/unit/elf/test_elf_file.py
index a393ae6461..72904d944a 100644
--- a/tests/unit/elf/test_elf_file.py
+++ b/tests/unit/elf/test_elf_file.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright 2016-2022 Canonical Ltd.
+# Copyright 2016-2023 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
@@ -92,7 +92,7 @@ def test_get_libraries(self, new_dir, fake_elf, fake_libs):
)
# bar.so.2 comes from fake ldd result
- assert libs == set([str(fake_libs["foo.so.1"]), "/usr/lib/bar.so.2"])
+ assert libs == set([fake_libs["foo.so.1"], Path("/usr/lib/bar.so.2")])
def test_get_libraries_missing_libs(self, new_dir, fake_elf, fake_libs):
elf_file = fake_elf("fake_elf-with-missing-libs")
@@ -103,7 +103,7 @@ def test_get_libraries_missing_libs(self, new_dir, fake_elf, fake_libs):
content_dirs=[],
)
- assert libs == {str(fake_libs["foo.so.1"]), "missing.so.2"}
+ assert libs == {fake_libs["foo.so.1"], Path("missing.so.2")}
def test_get_libraries_with_soname_cache(self, new_dir, fake_elf, fake_libs):
elf_file = fake_elf("fake_elf-2.23")
@@ -121,7 +121,7 @@ def test_get_libraries_with_soname_cache(self, new_dir, fake_elf, fake_libs):
)
# With no cache this would have returned '/usr/lib/bar.so.2'
- assert libs == {str(fake_libs["foo.so.1"]), "/lib/bar.so.2"}
+ assert libs == {fake_libs["foo.so.1"], Path("/lib/bar.so.2")}
def test_primed_libraries_are_preferred(self, new_dir, fake_elf, fake_libs):
elf_file = fake_elf("fake_elf-2.23")
@@ -132,7 +132,7 @@ def test_primed_libraries_are_preferred(self, new_dir, fake_elf, fake_libs):
content_dirs=[],
)
- assert libs == frozenset([str(fake_libs["foo.so.1"]), "/usr/lib/bar.so.2"])
+ assert libs == frozenset([fake_libs["foo.so.1"], Path("/usr/lib/bar.so.2")])
def test_non_elf_primed_sonames_matches_are_ignored(self, new_dir, fake_elf):
primed_foo = new_dir / "foo.so.1"
@@ -146,7 +146,7 @@ def test_non_elf_primed_sonames_matches_are_ignored(self, new_dir, fake_elf):
content_dirs=[],
)
- assert libs == frozenset(["/lib/foo.so.1", "/usr/lib/bar.so.2"])
+ assert libs == frozenset([Path("/lib/foo.so.1"), Path("/usr/lib/bar.so.2")])
def test_get_libraries_excludes_slash_snap(self, new_dir, fake_elf, fake_libs):
elf_file = fake_elf("fake_elf-with-core-libs")
@@ -157,7 +157,7 @@ def test_get_libraries_excludes_slash_snap(self, new_dir, fake_elf, fake_libs):
content_dirs=[],
)
- assert libs == {str(fake_libs["foo.so.1"]), "/usr/lib/bar.so.2"}
+ assert libs == {fake_libs["foo.so.1"], Path("/usr/lib/bar.so.2")}
def test_get_libraries_ldd_failure_logs_warning(self, emitter, new_dir, fake_elf):
elf_file = fake_elf("fake_elf-bad-ldd")
@@ -198,7 +198,7 @@ def _is_valid_elf(self, resolved_path: Path) -> bool:
content_dirs=[],
)
- assert libs == {str(fake_libs["moo.so.2"])}
+ assert libs == {fake_libs["moo.so.2"]}
class TestLibrary:
diff --git a/tests/unit/elf/test_elf_utils.py b/tests/unit/elf/test_elf_utils.py
index 05ce95edc4..f823383149 100644
--- a/tests/unit/elf/test_elf_utils.py
+++ b/tests/unit/elf/test_elf_utils.py
@@ -143,3 +143,46 @@ def test_get_dynamic_linker_not_found(self, mocker):
assert str(err.value) == (
"Dynamic linker 'prime/lib64/ld-linux-x86-64.so.2' not found."
)
+
+
+class TestArchConfig:
+ """Test architecture config functionality."""
+
+ @pytest.mark.parametrize(
+ "machine, expected_arch_triplet",
+ [
+ ("aarch64", "aarch64-linux-gnu"),
+ ("armv7l", "arm-linux-gnueabihf"),
+ ("ppc64le", "powerpc64le-linux-gnu"),
+ ("riscv64", "riscv64-linux-gnu"),
+ ("s390x", "s390x-linux-gnu"),
+ ("x86_64", "x86_64-linux-gnu"),
+ ],
+ )
+ def test_get_arch_triplet(self, mocker, machine, expected_arch_triplet):
+ """Verify `get_arch_triplet()` gets the host's architecture triplet."""
+ mocker.patch("snapcraft.elf.elf_utils.platform.machine", return_value=machine)
+ arch_triplet = elf_utils.get_arch_triplet()
+
+ assert arch_triplet == expected_arch_triplet
+
+ def test_get_arch_triplet_error(self, mocker):
+ """Verify `get_arch_triplet()` raises an error for invalid machines."""
+ mocker.patch("snapcraft.elf.elf_utils.platform.machine", return_value="4004")
+ with pytest.raises(RuntimeError) as raised:
+ elf_utils.get_arch_triplet()
+
+ assert str(raised.value) == "Arch triplet not defined for arch '4004'"
+
+ def test_get_all_arch_triplets(self):
+ """Verify `get_all_arch_triplets()` gets a list of all architecture triplets."""
+ arch_triplets = elf_utils.get_all_arch_triplets()
+
+ assert arch_triplets == [
+ "aarch64-linux-gnu",
+ "arm-linux-gnueabihf",
+ "powerpc64le-linux-gnu",
+ "riscv64-linux-gnu",
+ "s390x-linux-gnu",
+ "x86_64-linux-gnu",
+ ]
diff --git a/tests/unit/elf/test_patcher.py b/tests/unit/elf/test_patcher.py
index 0f3ee0d810..9fc181fcd3 100644
--- a/tests/unit/elf/test_patcher.py
+++ b/tests/unit/elf/test_patcher.py
@@ -13,7 +13,6 @@
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-
import shutil
from pathlib import Path
from unittest.mock import ANY, call
@@ -23,6 +22,8 @@
from snapcraft import elf
from snapcraft.elf import errors
+PATCHELF_PATH = "/path/to/patchelf"
+
@pytest.mark.usefixtures("fake_tools")
def test_patcher(fake_elf):
@@ -79,12 +80,13 @@ def patcher():
yield elf.Patcher(
dynamic_linker="/my/dynamic/linker",
root_path=Path("/snap/foo/current"),
+ preferred_patchelf=PATCHELF_PATH,
)
def test_patcher_patch_rpath(mocker, patcher, elf_file):
run_mock = mocker.patch("subprocess.check_call")
-
+ mocker.patch("subprocess.check_output", return_value=b"\n")
expected_proposed_rpath = list(elf_file.dependencies)[0].path.parent
assert patcher.get_current_rpath(elf_file) == []
@@ -92,7 +94,7 @@ def test_patcher_patch_rpath(mocker, patcher, elf_file):
assert run_mock.mock_calls == [
call(
[
- "/usr/bin/patchelf",
+ PATCHELF_PATH,
"--set-interpreter",
"/my/dynamic/linker",
"--force-rpath",
@@ -118,7 +120,7 @@ def test_patcher_patch_existing_rpath_origin(mocker, patcher, elf_file):
assert run_mock.mock_calls == [
call(
[
- "/usr/bin/patchelf",
+ PATCHELF_PATH,
"--set-interpreter",
"/my/dynamic/linker",
"--force-rpath",
@@ -144,7 +146,7 @@ def test_patcher_patch_existing_rpath_not_origin(mocker, patcher, elf_file):
assert run_mock.mock_calls == [
call(
[
- "/usr/bin/patchelf",
+ PATCHELF_PATH,
"--set-interpreter",
"/my/dynamic/linker",
"--force-rpath",
@@ -158,6 +160,7 @@ def test_patcher_patch_existing_rpath_not_origin(mocker, patcher, elf_file):
def test_patcher_patch_rpath_same_interpreter(mocker, patcher, elf_file):
run_mock = mocker.patch("subprocess.check_call")
+ mocker.patch("subprocess.check_output", return_value=b"\n")
patcher._dynamic_linker = elf_file.interp
expected_proposed_rpath = list(elf_file.dependencies)[0].path.parent
@@ -167,7 +170,7 @@ def test_patcher_patch_rpath_same_interpreter(mocker, patcher, elf_file):
assert run_mock.mock_calls == [
call(
[
- "/usr/bin/patchelf",
+ PATCHELF_PATH,
"--force-rpath",
"--set-rpath",
str(expected_proposed_rpath),
@@ -190,7 +193,7 @@ def test_patcher_patch_rpath_already_set(mocker, patcher, elf_file):
assert run_mock.mock_calls == [
call(
[
- "/usr/bin/patchelf",
+ PATCHELF_PATH,
"--set-interpreter",
"/my/dynamic/linker",
ANY,
diff --git a/tests/unit/extensions/test_kde_neon.py b/tests/unit/extensions/test_kde_neon.py
new file mode 100644
index 0000000000..57add69535
--- /dev/null
+++ b/tests/unit/extensions/test_kde_neon.py
@@ -0,0 +1,349 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2022 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 pytest
+
+from snapcraft.extensions import kde_neon
+from snapcraft.extensions.extension import get_extensions_data_dir
+
+############
+# Fixtures #
+############
+
+
+@pytest.fixture
+def kde_neon_extension():
+ return kde_neon.KDENeon(
+ yaml_data={"base": "core22", "parts": {}}, arch="amd64", target_arch="amd64"
+ )
+
+
+@pytest.fixture
+def kde_neon_extension_with_build_snap():
+ return kde_neon.KDENeon(
+ yaml_data={
+ "base": "core22",
+ "parts": {
+ "part1": {
+ "build-snaps": [
+ "kde-frameworks-5-102-qt-5-15-8-core22-sd/latest/stable"
+ ]
+ }
+ },
+ },
+ arch="amd64",
+ target_arch="amd64",
+ )
+
+
+@pytest.fixture
+def kde_neon_extension_with_default_build_snap_from_latest_edge():
+ return kde_neon.KDENeon(
+ yaml_data={
+ "base": "core22",
+ "parts": {
+ "part1": {
+ "build-snaps": [
+ "kde-frameworks-5-102-qt-5-15-8-core22-sd/latest/edge"
+ ]
+ }
+ },
+ },
+ arch="amd64",
+ target_arch="amd64",
+ )
+
+
+###################
+# KDENeon Extension #
+###################
+
+
+def test_get_supported_bases(kde_neon_extension):
+ assert kde_neon_extension.get_supported_bases() == ("core22",)
+
+
+def test_get_supported_confinement(kde_neon_extension):
+ assert kde_neon_extension.get_supported_confinement() == ("strict", "devmode")
+
+
+def test_is_experimental():
+ assert kde_neon.KDENeon.is_experimental(base="core22") is False
+
+
+def test_get_app_snippet(kde_neon_extension):
+ assert kde_neon_extension.get_app_snippet() == {
+ "command-chain": ["snap/command-chain/desktop-launch"],
+ "plugs": ["desktop", "desktop-legacy", "opengl", "wayland", "x11"],
+ }
+
+
+def test_get_root_snippet(kde_neon_extension):
+ assert kde_neon_extension.get_root_snippet() == {
+ "assumes": ["snapd2.43"],
+ "compression": "lzo",
+ "environment": {"SNAP_DESKTOP_RUNTIME": "$SNAP/kf5"},
+ "hooks": {
+ "configure": {
+ "plugs": ["desktop"],
+ "command-chain": ["snap/command-chain/hooks-configure-desktop"],
+ }
+ },
+ "layout": {"/usr/share/X11": {"symlink": "$SNAP/kf5/usr/share/X11"}},
+ "plugs": {
+ "desktop": {"mount-host-font-cache": False},
+ "icon-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/icons",
+ "default-provider": "gtk-common-themes",
+ },
+ "sound-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/sounds",
+ "default-provider": "gtk-common-themes",
+ },
+ "kde-frameworks-5-102-qt-5-15-8-core22": {
+ "content": "kde-frameworks-5-102-qt-5-15-8-core22-all",
+ "interface": "content",
+ "default-provider": "kde-frameworks-5-102-qt-5-15-8-core22",
+ "target": "$SNAP/kf5",
+ },
+ },
+ }
+
+
+def test_get_root_snippet_with_external_sdk(kde_neon_extension_with_build_snap):
+ assert kde_neon_extension_with_build_snap.get_root_snippet() == {
+ "assumes": ["snapd2.43"],
+ "compression": "lzo",
+ "environment": {"SNAP_DESKTOP_RUNTIME": "$SNAP/kf5"},
+ "hooks": {
+ "configure": {
+ "plugs": ["desktop"],
+ "command-chain": ["snap/command-chain/hooks-configure-desktop"],
+ }
+ },
+ "layout": {"/usr/share/X11": {"symlink": "$SNAP/kf5/usr/share/X11"}},
+ "plugs": {
+ "desktop": {"mount-host-font-cache": False},
+ "icon-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/icons",
+ "default-provider": "gtk-common-themes",
+ },
+ "sound-themes": {
+ "interface": "content",
+ "target": "$SNAP/data-dir/sounds",
+ "default-provider": "gtk-common-themes",
+ },
+ "kde-frameworks-5-102-qt-5-15-8-core22": {
+ "content": "kde-frameworks-5-102-qt-5-15-8-core22-all",
+ "interface": "content",
+ "default-provider": "kde-frameworks-5-102-qt-5-15-8-core22",
+ "target": "$SNAP/kf5",
+ },
+ },
+ }
+
+
+class TestGetPartSnippet:
+ """Tests for KDENeon.get_part_snippet when using the default sdk snap name."""
+
+ def test_get_part_snippet(self, kde_neon_extension):
+ self.assert_get_part_snippet(kde_neon_extension)
+
+ def test_get_part_snippet_latest_edge(
+ self, kde_neon_extension_with_default_build_snap_from_latest_edge
+ ):
+ self.assert_get_part_snippet(
+ kde_neon_extension_with_default_build_snap_from_latest_edge
+ )
+
+ @staticmethod
+ def assert_get_part_snippet(kde_neon_instance):
+ assert kde_neon_instance.get_part_snippet() == {
+ "build-environment": [
+ {
+ "PATH": (
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/bin${PATH:+:$PATH}"
+ )
+ },
+ {
+ "XDG_DATA_DIRS": (
+ "$SNAPCRAFT_STAGE/usr/share:"
+ + "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd"
+ "/current/usr/share:/usr/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
+ )
+ },
+ {
+ "LD_LIBRARY_PATH": ":".join(
+ [
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "lib/$CRAFT_ARCH_TRIPLET",
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib/$CRAFT_ARCH_TRIPLET",
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib",
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib/vala-current",
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib/$CRAFT_ARCH_TRIPLET/pulseaudio",
+ ]
+ )
+ + "${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+ },
+ {
+ "PKG_CONFIG_PATH": (
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib/$CRAFT_ARCH_TRIPLET/pkgconfig:"
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib/pkgconfig:"
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/share/pkgconfig"
+ )
+ + "${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
+ },
+ {
+ "ACLOCAL_PATH": (
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/share/aclocal"
+ )
+ + "${ACLOCAL_PATH:+:$ACLOCAL_PATH}"
+ },
+ {
+ "SNAPCRAFT_CMAKE_ARGS": (
+ "-DCMAKE_FIND_ROOT_PATH="
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current"
+ "${SNAPCRAFT_CMAKE_ARGS:+:$SNAPCRAFT_CMAKE_ARGS}"
+ )
+ },
+ ]
+ }
+
+
+def test_get_part_snippet_with_external_sdk(kde_neon_extension_with_build_snap):
+ assert kde_neon_extension_with_build_snap.get_part_snippet() == {
+ "build-environment": [
+ {
+ "PATH": (
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/bin${PATH:+:$PATH}"
+ )
+ },
+ {
+ "XDG_DATA_DIRS": (
+ "$SNAPCRAFT_STAGE/usr/share:/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd"
+ "/current/usr/share:/usr/share${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
+ )
+ },
+ {
+ "LD_LIBRARY_PATH": ":".join(
+ [
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "lib/$CRAFT_ARCH_TRIPLET",
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib/$CRAFT_ARCH_TRIPLET",
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib",
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib/vala-current",
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib/$CRAFT_ARCH_TRIPLET/pulseaudio",
+ ]
+ )
+ + "${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
+ },
+ {
+ "PKG_CONFIG_PATH": (
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib/$CRAFT_ARCH_TRIPLET/pkgconfig:"
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/lib/pkgconfig:"
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/share/pkgconfig"
+ )
+ + "${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
+ },
+ {
+ "ACLOCAL_PATH": (
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current/"
+ "usr/share/aclocal"
+ )
+ + "${ACLOCAL_PATH:+:$ACLOCAL_PATH}"
+ },
+ {
+ "SNAPCRAFT_CMAKE_ARGS": (
+ "-DCMAKE_FIND_ROOT_PATH="
+ "/snap/kde-frameworks-5-102-qt-5-15-8-core22-sd/current"
+ "${SNAPCRAFT_CMAKE_ARGS:+:$SNAPCRAFT_CMAKE_ARGS}"
+ )
+ },
+ ]
+ }
+
+
+def test_get_parts_snippet(kde_neon_extension):
+ source = get_extensions_data_dir() / "desktop" / "command-chain"
+
+ assert kde_neon_extension.get_parts_snippet() == {
+ "kde-neon-extension/sdk": {
+ "source": str(source),
+ "source-subdir": "kde-neon",
+ "plugin": "make",
+ "make-parameters": ["PLATFORM_PLUG=kde-frameworks-5-102-qt-5-15-8-core22"],
+ "build-packages": ["g++"],
+ "build-snaps": ["kde-frameworks-5-102-qt-5-15-8-core22-sd/current/stable"],
+ }
+ }
+
+
+def test_get_parts_snippet_with_external_sdk(kde_neon_extension_with_build_snap):
+ source = get_extensions_data_dir() / "desktop" / "command-chain"
+
+ assert kde_neon_extension_with_build_snap.get_parts_snippet() == {
+ "kde-neon-extension/sdk": {
+ "source": str(source),
+ "source-subdir": "kde-neon",
+ "plugin": "make",
+ "make-parameters": ["PLATFORM_PLUG=kde-frameworks-5-102-qt-5-15-8-core22"],
+ "build-packages": ["g++"],
+ "build-snaps": ["kde-frameworks-5-102-qt-5-15-8-core22-sd/current/stable"],
+ }
+ }
+
+
+def test_get_parts_snippet_with_external_sdk_different_channel(
+ kde_neon_extension_with_default_build_snap_from_latest_edge,
+):
+ source = get_extensions_data_dir() / "desktop" / "command-chain"
+ assert (
+ kde_neon_extension_with_default_build_snap_from_latest_edge.get_parts_snippet()
+ == {
+ "kde-neon-extension/sdk": {
+ "source": str(source),
+ "source-subdir": "kde-neon",
+ "plugin": "make",
+ "make-parameters": [
+ "PLATFORM_PLUG=kde-frameworks-5-102-qt-5-15-8-core22"
+ ],
+ "build-packages": ["g++"],
+ "build-snaps": [
+ "kde-frameworks-5-102-qt-5-15-8-core22-sd/current/stable"
+ ],
+ }
+ }
+ )
diff --git a/tests/unit/extensions/test_registry.py b/tests/unit/extensions/test_registry.py
index d4f2accf5b..118e815171 100644
--- a/tests/unit/extensions/test_registry.py
+++ b/tests/unit/extensions/test_registry.py
@@ -26,6 +26,7 @@ def test_get_extension_names():
assert extensions.get_extension_names() == [
"gnome",
"ros2-humble",
+ "kde-neon",
"fake-extension-experimental",
"fake-extension-extra",
"fake-extension",
diff --git a/tests/unit/linters/test_classic_linter.py b/tests/unit/linters/test_classic_linter.py
index ddcc613c07..3ebc8be4c4 100644
--- a/tests/unit/linters/test_classic_linter.py
+++ b/tests/unit/linters/test_classic_linter.py
@@ -89,18 +89,21 @@ def test_classic_linter(mocker, new_dir, confinement, stage_libc, text):
f"ELF interpreter should be set to "
f"'/snap/{snap_name}/current/lib64/ld-linux-x86-64.so.2'."
),
+ url="https://snapcraft.io/docs/linters-classic",
),
LinterIssue(
name="classic",
result=LinterResult.WARNING,
filename="elf.bin",
text="ELF rpath should be set to '/snap/core22/current/lib/x86_64-linux-gnu'.",
+ url="https://snapcraft.io/docs/linters-classic",
),
LinterIssue(
name="classic",
result=LinterResult.WARNING,
filename="elf.lib",
text="ELF rpath should be set to '/snap/core22/current/lib/x86_64-linux-gnu'.",
+ url="https://snapcraft.io/docs/linters-classic",
),
]
else:
diff --git a/tests/unit/linters/test_library_linter.py b/tests/unit/linters/test_library_linter.py
index af00a63b6d..d662b8678a 100644
--- a/tests/unit/linters/test_library_linter.py
+++ b/tests/unit/linters/test_library_linter.py
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
-# Copyright 2022 Canonical Ltd.
+# Copyright 2022-2023 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
@@ -16,9 +16,12 @@
import shutil
from pathlib import Path
+from unittest.mock import Mock
+
+import pytest
from snapcraft import linters, projects
-from snapcraft.elf import elf_utils
+from snapcraft.elf import _elf_file, elf_utils
from snapcraft.linters.base import LinterIssue, LinterResult
from snapcraft.linters.library_linter import LibraryLinter
from snapcraft.meta import snap_yaml
@@ -28,7 +31,8 @@ def setup_function():
elf_utils.get_elf_files.cache_clear()
-def test_library_linter(mocker, new_dir):
+def test_library_linter_missing_library(mocker, new_dir):
+ """Verify missing libraries are caught by the linter."""
shutil.copy("/bin/true", "elf.bin")
mocker.patch("snapcraft.linters.linters.LINTERS", {"library": LibraryLinter})
@@ -64,17 +68,73 @@ def test_library_linter(mocker, new_dir):
result=LinterResult.WARNING,
filename="elf.bin",
text="missing dependency 'libbar.so.5'.",
+ url="https://snapcraft.io/docs/linters-library",
),
LinterIssue(
name="library",
result=LinterResult.WARNING,
filename="elf.bin",
text="missing dependency 'libfoo.so.1'.",
+ url="https://snapcraft.io/docs/linters-library",
+ ),
+ ]
+
+
+def test_library_linter_unused_library(mocker, new_dir):
+ """Verify unused libraries are caught by the linter."""
+ # mock an elf file
+ mock_elf_file = Mock(spec=_elf_file.ElfFile)
+ mock_elf_file.soname = ""
+ mock_elf_file.path = Path("elf.bin")
+ mock_elf_file.load_dependencies.return_value = []
+
+ # mock a library
+ mock_library = Mock(spec=_elf_file.ElfFile)
+ mock_library.soname = "libfoo.so"
+ mock_library.path = Path("lib/libfoo.so")
+ mock_library.load_dependencies.return_value = []
+
+ mocker.patch(
+ "snapcraft.linters.library_linter.elf_utils.get_elf_files",
+ return_value=[mock_elf_file, mock_library],
+ )
+
+ mocker.patch("snapcraft.linters.library_linter.ElfFile", return_value=mock_library)
+ mocker.patch("snapcraft.linters.library_linter.Path.is_file", return_value=True)
+ mocker.patch("snapcraft.linters.linters.LINTERS", {"library": LibraryLinter})
+
+ yaml_data = {
+ "name": "mytest",
+ "version": "1.29.3",
+ "base": "core22",
+ "summary": "Single-line elevator pitch for your amazing snap",
+ "description": "test-description",
+ "confinement": "strict",
+ "parts": {},
+ }
+
+ project = projects.Project.unmarshal(yaml_data)
+ snap_yaml.write(
+ project,
+ prime_dir=Path(new_dir),
+ arch="amd64",
+ arch_triplet="x86_64-linux-gnu",
+ )
+
+ issues = linters.run_linters(new_dir, lint=None)
+ assert issues == [
+ LinterIssue(
+ name="library",
+ result=LinterResult.WARNING,
+ filename="libfoo.so",
+ text="unused library 'lib/libfoo.so'.",
+ url="https://snapcraft.io/docs/linters-library",
),
]
-def test_library_linter_filter(mocker, new_dir):
+def test_library_linter_filter_missing_library(mocker, new_dir):
+ """Verify missing libraries can be filtered out."""
shutil.copy("/bin/true", "elf.bin")
mocker.patch("snapcraft.linters.linters.LINTERS", {"library": LibraryLinter})
@@ -107,3 +167,85 @@ def test_library_linter_filter(mocker, new_dir):
new_dir, lint=projects.Lint(ignore=[{"library": ["elf.*"]}])
)
assert issues == []
+
+
+def test_library_linter_filter_unused_library(mocker, new_dir):
+ """Verify unused libraries can be filtered out."""
+ # mock an elf file
+ mock_elf_file = Mock(spec=_elf_file.ElfFile)
+ mock_elf_file.soname = ""
+ mock_elf_file.path = Path("elf.bin")
+ mock_elf_file.load_dependencies.return_value = []
+
+ # mock a library
+ mock_library = Mock(spec=_elf_file.ElfFile)
+ mock_library.soname = "libfoo.so"
+ mock_library.path = Path("lib/libfoo.so")
+ mock_library.load_dependencies.return_value = []
+
+ mocker.patch(
+ "snapcraft.linters.library_linter.elf_utils.get_elf_files",
+ return_value=[mock_elf_file, mock_library],
+ )
+
+ mocker.patch("snapcraft.linters.library_linter.Path.is_file", return_value=True)
+ mocker.patch("snapcraft.linters.linters.LINTERS", {"library": LibraryLinter})
+
+ yaml_data = {
+ "name": "mytest",
+ "version": "1.29.3",
+ "base": "core22",
+ "summary": "Single-line elevator pitch for your amazing snap",
+ "description": "test-description",
+ "confinement": "strict",
+ "parts": {},
+ }
+
+ project = projects.Project.unmarshal(yaml_data)
+ snap_yaml.write(
+ project,
+ prime_dir=Path(new_dir),
+ arch="amd64",
+ arch_triplet="x86_64-linux-gnu",
+ )
+
+ issues = linters.run_linters(
+ new_dir, lint=projects.Lint(ignore=[{"library": ["lib/libfoo.*"]}])
+ )
+ assert issues == []
+
+
+@pytest.mark.parametrize(
+ "path, expected_result",
+ [
+ # files not in library paths should return False
+ (Path("/test/file"), False),
+ (Path("/lib/x86_64-linux-gnu/subdir/libtest.so"), False),
+ (Path("/usr/lib/x86_64-linux-gnu/subdir/libtest.so"), False),
+ (Path("/usr/lib/arm-linux-gnueabihf/subdir/libtest.so"), False),
+ # files in library paths should return True
+ (Path("/lib/libtest.so"), True),
+ (Path("/usr/lib/libtest.so"), True),
+ (Path("/usr/lib32/libtest.so"), True),
+ (Path("/usr/lib64/libtest.so"), True),
+ (Path("/usr/lib/x86_64-linux-gnu/libtest.so"), True),
+ (Path("/root/stage/lib/libtest.so"), True),
+ (Path("/root/stage/lib/x86_64-linux-gnu/libtest.so"), True),
+ ],
+)
+def test_is_library_path(mocker, path, expected_result):
+ """Check if filepaths are inside a library directory."""
+ mocker.patch("snapcraft.linters.library_linter.Path.is_file", return_value=True)
+ linter = LibraryLinter(name="library", snap_metadata=Mock(), lint=None)
+ result = linter._is_library_path(path=path)
+
+ assert result == expected_result
+
+
+def test_is_library_path_directory(mocker):
+ """Running `is_library_path()` on a directory should always return False."""
+ mocker.patch("snapcraft.linters.library_linter.Path.is_file", return_value=False)
+ linter = LibraryLinter(name="library", snap_metadata=Mock(), lint=None)
+ result = linter._is_library_path(path=Path("/test/dir"))
+
+ assert not result
diff --git a/tests/unit/meta/test_snap_yaml.py b/tests/unit/meta/test_snap_yaml.py
index da9e849a52..d0b29ceeb1 100644
--- a/tests/unit/meta/test_snap_yaml.py
+++ b/tests/unit/meta/test_snap_yaml.py
@@ -199,6 +199,9 @@ def complex_project():
snap_daemon:
scope: shared
snap_microk8s: shared
+ snap_aziotedge: shared
+ snap_aziotdu:
+ scope: shared
layout:
/usr/share/libdrm:
@@ -207,6 +210,8 @@ def complex_project():
bind: $SNAP/gnome-platform/usr/lib/x86_64-linux-gnu/webkit2gtk-4.0
/usr/share/xml/iso-codes:
bind: $SNAP/gnome-platform/usr/share/xml/iso-codes
+
+ provenance: test-provenance-1
"""
)
data = yaml.safe_load(snapcraft_yaml)
@@ -338,6 +343,10 @@ def test_complex_snap_yaml(complex_project, new_dir):
snap_daemon:
scope: shared
snap_microk8s: shared
+ snap_aziotedge: shared
+ snap_aziotdu:
+ scope: shared
+ provenance: test-provenance-1
"""
)
diff --git a/tests/unit/parts/extensions/test_ros2_humble.py b/tests/unit/parts/extensions/test_ros2_humble.py
index 94aa1e2dbd..1cb1a4d57c 100644
--- a/tests/unit/parts/extensions/test_ros2_humble.py
+++ b/tests/unit/parts/extensions/test_ros2_humble.py
@@ -14,9 +14,6 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-import os
-import sys
-
import pytest
import snapcraft.extensions.registry as reg
@@ -53,7 +50,7 @@ def test_is_registered(self):
try:
reg.get_extension_class(_EXTENSION_NAME)
except errors.ExtensionError as exc:
- assert False, f"Couldn't get extension '{_EXTENSION_NAME}': {exc}"
+ raise AssertionError(f"Couldn't get extension '{_EXTENSION_NAME}': {exc}")
def test_ros_version(self, setup_method_fixture):
extension = setup_method_fixture()
diff --git a/tests/unit/parts/plugins/test_colcon.py b/tests/unit/parts/plugins/test_colcon.py
index 391fc62b75..e35fa3ab2c 100644
--- a/tests/unit/parts/plugins/test_colcon.py
+++ b/tests/unit/parts/plugins/test_colcon.py
@@ -72,7 +72,7 @@ def test_property_default(self):
try:
colcon.ColconPlugin.properties_class.unmarshal({"source": "."})
except ValidationError as e:
- assert False, f"{e}"
+ raise AssertionError(f"{e}") from e
def test_property_all(self):
try:
@@ -87,7 +87,7 @@ def test_property_all(self):
}
)
except ValidationError as e:
- assert False, f"{e}"
+ raise AssertionError(f"{e}") from e
assert properties.source == "." # type: ignore
assert properties.colcon_ament_cmake_args == ["ament", "args..."] # type: ignore
@@ -129,9 +129,13 @@ def test_get_build_commands(self, setup_method_fixture, new_dir, monkeypatch):
assert plugin.get_build_commands() == [
'state="$(set +o); set -$-"',
"set +u",
- 'if [ -f "${CRAFT_PART_INSTALL}/opt/ros/snap/setup.sh" ]; then',
+ 'if [ -f "${CRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}/local_setup.sh" ]; then',
+ 'COLCON_CURRENT_PREFIX="${CRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}" . '
+ '"${CRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}/local_setup.sh"',
+ "fi",
+ 'if [ -f "${CRAFT_PART_INSTALL}/opt/ros/snap/local_setup.sh" ]; then',
'COLCON_CURRENT_PREFIX="${CRAFT_PART_INSTALL}/opt/ros/snap" . '
- '"${CRAFT_PART_INSTALL}/opt/ros/snap/setup.sh"',
+ '"${CRAFT_PART_INSTALL}/opt/ros/snap/local_setup.sh"',
"fi",
'. "/opt/ros/${ROS_DISTRO}/local_setup.sh"',
'eval "${state}"',
@@ -192,9 +196,13 @@ def test_get_build_commands_with_all_properties(
assert plugin.get_build_commands() == [
'state="$(set +o); set -$-"',
"set +u",
- 'if [ -f "${CRAFT_PART_INSTALL}/opt/ros/snap/setup.sh" ]; then',
+ 'if [ -f "${CRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}/local_setup.sh" ]; then',
+ 'COLCON_CURRENT_PREFIX="${CRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}" . '
+ '"${CRAFT_PART_INSTALL}/opt/ros/${ROS_DISTRO}/local_setup.sh"',
+ "fi",
+ 'if [ -f "${CRAFT_PART_INSTALL}/opt/ros/snap/local_setup.sh" ]; then',
'COLCON_CURRENT_PREFIX="${CRAFT_PART_INSTALL}/opt/ros/snap" . '
- '"${CRAFT_PART_INSTALL}/opt/ros/snap/setup.sh"',
+ '"${CRAFT_PART_INSTALL}/opt/ros/snap/local_setup.sh"',
"fi",
'. "/opt/ros/${ROS_DISTRO}/local_setup.sh"',
'eval "${state}"',
diff --git a/tests/unit/parts/plugins/test_flutter_plugin.py b/tests/unit/parts/plugins/test_flutter_plugin.py
new file mode 100644
index 0000000000..b5c6628350
--- /dev/null
+++ b/tests/unit/parts/plugins/test_flutter_plugin.py
@@ -0,0 +1,115 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
+#
+# Copyright 2023 Canonical Ltd.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see .
+
+
+import pytest
+from craft_parts import Part, PartInfo, ProjectInfo
+
+from snapcraft.parts.plugins import FlutterPlugin
+
+
+@pytest.fixture(autouse=True)
+def part_info(new_dir):
+ yield PartInfo(
+ project_info=ProjectInfo(
+ application_name="test", project_name="test-snap", cache_dir=new_dir
+ ),
+ part=Part("my-part", {}),
+ )
+
+
+def test_get_build_snaps(part_info):
+ properties = FlutterPlugin.properties_class.unmarshal({"source": "."})
+ plugin = FlutterPlugin(properties=properties, part_info=part_info)
+ assert plugin.get_build_snaps() == set()
+
+
+def test_get_build_packages(part_info):
+ properties = FlutterPlugin.properties_class.unmarshal({"source": "."})
+ plugin = FlutterPlugin(properties=properties, part_info=part_info)
+ assert plugin.get_build_packages() == {
+ "clang",
+ "git",
+ "cmake",
+ "ninja-build",
+ "unzip",
+ }
+
+
+def test_get_build_environment(part_info):
+ properties = FlutterPlugin.properties_class.unmarshal({"source": "."})
+ plugin = FlutterPlugin(properties=properties, part_info=part_info)
+
+ flutter_bin = plugin._part_info.part_build_dir / "flutter-distro" / "bin"
+ assert plugin.get_build_environment() == {"PATH": f"{flutter_bin}:${{PATH}}"}
+
+
+def test_get_build_commands(part_info):
+ properties = FlutterPlugin.properties_class.unmarshal({"source": "."})
+ plugin = FlutterPlugin(properties=properties, part_info=part_info)
+
+ assert plugin.get_build_commands() == [
+ "git clone --depth 1 -b stable https://github.com/flutter/flutter.git "
+ f"{plugin.flutter_dir}",
+ "flutter precache --linux",
+ "flutter pub get",
+ "flutter build linux --release --verbose --target lib/main.dart",
+ "cp -r build/linux/*/release/bundle/* $CRAFT_PART_INSTALL/",
+ ]
+
+
+def test_get_build_commands_alternative_target(part_info):
+ properties = FlutterPlugin.properties_class.unmarshal(
+ {"flutter-target": "lib/not-main.dart", "source": "."}
+ )
+ plugin = FlutterPlugin(properties=properties, part_info=part_info)
+
+ assert plugin.get_build_commands() == [
+ f"git clone --depth 1 -b stable https://github.com/flutter/flutter.git "
+ f"{plugin.flutter_dir}",
+ "flutter precache --linux",
+ "flutter pub get",
+ "flutter build linux --release --verbose --target lib/not-main.dart",
+ "cp -r build/linux/*/release/bundle/* $CRAFT_PART_INSTALL/",
+ ]
+
+
+@pytest.mark.parametrize("value", ["stable", "master", "beta"])
+def test_get_build_commands_different_channels(part_info, value):
+ properties = FlutterPlugin.properties_class.unmarshal(
+ {"flutter-channel": value, "source": "."}
+ )
+ plugin = FlutterPlugin(properties=properties, part_info=part_info)
+
+ assert plugin.get_build_commands() == [
+ f"git clone --depth 1 -b {value} https://github.com/flutter/flutter.git "
+ f"{plugin.flutter_dir}",
+ "flutter precache --linux",
+ "flutter pub get",
+ "flutter build linux --release --verbose --target lib/main.dart",
+ "cp -r build/linux/*/release/bundle/* $CRAFT_PART_INSTALL/",
+ ]
+
+
+def test_get_build_commands_flutter_bin_exists(part_info):
+ properties = FlutterPlugin.properties_class.unmarshal({"source": "."})
+ plugin = FlutterPlugin(properties=properties, part_info=part_info)
+ plugin.flutter_dir.mkdir(parents=True)
+
+ assert plugin.get_build_commands() == [
+ "flutter build linux --release --verbose --target lib/main.dart",
+ "cp -r build/linux/*/release/bundle/* $CRAFT_PART_INSTALL/",
+ ]
diff --git a/tests/unit/parts/test_lifecycle.py b/tests/unit/parts/test_lifecycle.py
index b0231f33a3..6fe8943a70 100644
--- a/tests/unit/parts/test_lifecycle.py
+++ b/tests/unit/parts/test_lifecycle.py
@@ -15,6 +15,7 @@
# along with this program. If not, see .
import argparse
+import shutil
import textwrap
from datetime import datetime
from pathlib import Path
@@ -874,7 +875,7 @@ def test_extract_parse_info():
"name": "foo",
"parts": {"p1": {"plugin": "nil", "parse-info": "foo/metadata.xml"}, "p2": {}},
}
- parse_info = parts_lifecycle._extract_parse_info(yaml_data)
+ parse_info = parts_lifecycle.extract_parse_info(yaml_data)
assert yaml_data == {"name": "foo", "parts": {"p1": {"plugin": "nil"}, "p2": {}}}
assert parse_info == {"p1": "foo/metadata.xml"}
@@ -1119,6 +1120,9 @@ def test_lifecycle_run_in_provider_default(
mock_ensure_provider_is_available = mocker.patch(
"snapcraft.parts.lifecycle.providers.ensure_provider_is_available"
)
+ mock_prepare_instance = mocker.patch(
+ "snapcraft.parts.lifecycle.providers.prepare_instance"
+ )
mocker.patch("snapcraft.projects.Project.get_build_on", return_value="test-arch-1")
mocker.patch("snapcraft.projects.Project.get_build_for", return_value="test-arch-2")
@@ -1163,8 +1167,8 @@ def test_lifecycle_run_in_provider_default(
build_base="22.04",
instance_name="test-instance-name",
)
- mock_instance.mount.assert_called_with(
- host_source=tmp_path, target=Path("/root/project")
+ mock_prepare_instance.assert_called_with(
+ instance=mock_instance, host_project_path=tmp_path, bind_ssh=False
)
mock_instance.execute_run.assert_called_once_with(
expected_command, check=True, cwd=Path("/root/project")
@@ -1182,6 +1186,7 @@ def test_lifecycle_run_in_provider_default(
(EmitterMode.TRACE, "--verbosity=trace"),
],
)
+# pylint: disable-next=too-many-locals
def test_lifecycle_run_in_provider_all_options(
mock_get_instance_name,
mock_instance,
@@ -1204,6 +1209,9 @@ def test_lifecycle_run_in_provider_all_options(
mock_ensure_provider_is_available = mocker.patch(
"snapcraft.parts.lifecycle.providers.ensure_provider_is_available"
)
+ mock_prepare_instance = mocker.patch(
+ "snapcraft.parts.lifecycle.providers.prepare_instance"
+ )
mocker.patch("snapcraft.projects.Project.get_build_on", return_value="test-arch-1")
mocker.patch("snapcraft.projects.Project.get_build_for", return_value="test-arch-2")
@@ -1282,11 +1290,8 @@ def test_lifecycle_run_in_provider_all_options(
build_base="22.04",
instance_name="test-instance-name",
)
- mock_instance.mount.assert_has_calls(
- [
- call(host_source=tmp_path, target=Path("/root/project")),
- call(host_source=Path().home() / ".ssh", target=Path("/root/.ssh")),
- ]
+ mock_prepare_instance.assert_called_with(
+ instance=mock_instance, host_project_path=tmp_path, bind_ssh=True
)
mock_instance.execute_run.assert_called_once_with(
expected_command, check=True, cwd=Path("/root/project")
@@ -1294,6 +1299,58 @@ def test_lifecycle_run_in_provider_all_options(
mock_capture_logs_from_instance.assert_called_once()
+def test_lifecycle_run_in_provider_try(
+ mock_get_instance_name,
+ mock_instance,
+ mock_provider,
+ mocker,
+ snapcraft_yaml,
+ tmp_path,
+):
+ """Test that "snapcraft try" mounts the host's prime dir before priming in the instance"""
+ mock_base_configuration = Mock()
+ mocker.patch(
+ "snapcraft.parts.lifecycle.providers.get_base_configuration",
+ return_value=mock_base_configuration,
+ )
+ mocker.patch("snapcraft.parts.lifecycle.providers.capture_logs_from_instance")
+ mocker.patch("snapcraft.parts.lifecycle.providers.ensure_provider_is_available")
+ mocker.patch("snapcraft.parts.lifecycle.providers.prepare_instance")
+ mocker.patch("snapcraft.projects.Project.get_build_on", return_value="test-arch-1")
+ mocker.patch("snapcraft.projects.Project.get_build_for", return_value="test-arch-2")
+
+ project = Project.unmarshal(snapcraft_yaml(base="core22"))
+ parts_lifecycle._run_in_provider(
+ project=project,
+ command_name="try",
+ parsed_args=argparse.Namespace(
+ use_lxd=False,
+ debug=False,
+ bind_ssh=False,
+ http_proxy=None,
+ https_proxy=None,
+ ),
+ )
+
+ expected_command = [
+ "snapcraft",
+ "try",
+ "--verbosity=quiet",
+ "--build-for",
+ "test-arch-2",
+ ]
+
+ # Make sure the calls are made in the correct order: first the host 'prime' dir
+ # is mounted, and _then_ the command is run in the instance.
+ mock_instance.assert_has_calls(
+ [
+ call.mount(host_source=tmp_path / "prime", target=Path("/root/prime")),
+ call.execute_run(expected_command, check=True, cwd=Path("/root/project")),
+ ],
+ any_order=False,
+ )
+
+
@pytest.fixture
def minimal_yaml_data():
return {
@@ -1457,3 +1514,67 @@ def test_get_build_plan_list_without_matching_element_and_build_for_arg(
)
== []
)
+
+
+def test_patch_elf(snapcraft_yaml, mocker, new_dir):
+ """Patch binaries if the ``enable-patchelf`` build attribute is defined."""
+ run_patchelf_mock = mocker.patch("snapcraft.elf._patcher.Patcher._run_patchelf")
+ shutil.copy("/bin/true", "elf.bin")
+ callbacks.register_post_step(parts_lifecycle._patch_elf, step_list=[Step.PRIME])
+
+ mocker.patch(
+ "snapcraft.elf.elf_utils.get_dynamic_linker",
+ return_value="/snap/core22/current/lib64/ld-linux-x86-64.so.2",
+ )
+ mocker.patch(
+ "snapcraft.elf._patcher.Patcher.get_proposed_rpath",
+ return_value=["/snap/core22/current/lib/x86_64-linux-gnu"],
+ )
+ mocker.patch("snapcraft.elf._patcher.Patcher.get_current_rpath", return_value=[])
+
+ yaml_data = {
+ "base": "core22",
+ "confinement": "classic",
+ "parts": {
+ "p1": {
+ "plugin": "dump",
+ "source": ".",
+ "build-attributes": ["enable-patchelf"],
+ }
+ },
+ }
+ project = Project.unmarshal(snapcraft_yaml(**yaml_data))
+
+ parts_lifecycle._run_command(
+ "pack",
+ project=project,
+ parse_info={},
+ assets_dir=Path(),
+ start_time=datetime.now(),
+ parallel_build_count=1,
+ parsed_args=argparse.Namespace(
+ directory=None,
+ output=None,
+ debug=False,
+ destructive_mode=True,
+ shell=False,
+ shell_after=False,
+ use_lxd=False,
+ enable_manifest=False,
+ ua_token=None,
+ parts=["p1"],
+ ),
+ )
+
+ assert run_patchelf_mock.mock_calls == [
+ call(
+ patchelf_args=[
+ "--set-interpreter",
+ "/snap/core22/current/lib64/ld-linux-x86-64.so.2",
+ "--force-rpath",
+ "--set-rpath",
+ "/snap/core22/current/lib/x86_64-linux-gnu",
+ ],
+ elf_file_path=new_dir / "prime/elf.bin",
+ )
+ ]
diff --git a/tests/unit/parts/test_parts.py b/tests/unit/parts/test_parts.py
index 666f7836b0..cab64ecf60 100644
--- a/tests/unit/parts/test_parts.py
+++ b/tests/unit/parts/test_parts.py
@@ -48,6 +48,7 @@ def test_parts_lifecycle_run(mocker, parts_data, step_name, new_dir, emitter):
parse_info={},
project_vars={"version": "1", "grade": "stable"},
extra_build_snaps=["core22"],
+ track_stage_packages=True,
target_arch="amd64",
)
lifecycle.run(step_name)
@@ -63,6 +64,7 @@ def test_parts_lifecycle_run(mocker, parts_data, step_name, new_dir, emitter):
base="core22",
ignore_local_sources=["*.snap"],
extra_build_snaps=["core22"],
+ track_stage_packages=True,
parallel_build_count=8,
project_name="test-project",
project_vars_part_name=None,
@@ -86,6 +88,7 @@ def test_parts_lifecycle_run_bad_step(parts_data, new_dir):
project_name="test-project",
project_vars={"version": "1", "grade": "stable"},
target_arch="amd64",
+ track_stage_packages=True,
)
with pytest.raises(RuntimeError) as raised:
lifecycle.run("invalid")
@@ -106,6 +109,7 @@ def test_parts_lifecycle_run_internal_error(parts_data, new_dir, mocker):
parse_info={},
project_vars={"version": "1", "grade": "stable"},
target_arch="amd64",
+ track_stage_packages=True,
)
mocker.patch("craft_parts.LifecycleManager.plan", side_effect=RuntimeError("crash"))
with pytest.raises(RuntimeError) as raised:
@@ -127,6 +131,7 @@ def test_parts_lifecycle_run_parts_error(new_dir):
parse_info={},
project_vars={"version": "1", "grade": "stable"},
target_arch="amd64",
+ track_stage_packages=True,
)
with pytest.raises(errors.PartsLifecycleError) as raised:
lifecycle.run("prime")
@@ -149,6 +154,7 @@ def test_parts_lifecycle_clean(parts_data, new_dir, emitter):
parse_info={},
project_vars={"version": "1", "grade": "stable"},
target_arch="amd64",
+ track_stage_packages=True,
)
lifecycle.clean(part_names=None)
emitter.assert_progress("Cleaning all parts")
@@ -168,6 +174,7 @@ def test_parts_lifecycle_clean_parts(parts_data, new_dir, emitter):
parse_info={},
project_vars={"version": "1", "grade": "stable"},
target_arch="amd64",
+ track_stage_packages=True,
)
lifecycle.clean(part_names=["p1"])
emitter.assert_progress("Cleaning parts: p1")
@@ -211,6 +218,7 @@ def test_parts_lifecycle_initialize_with_package_repositories_deps_not_installed
parse_info={},
project_vars={"version": "1", "grade": "stable"},
extra_build_snaps=["core22"],
+ track_stage_packages=True,
target_arch="amd64",
)
@@ -259,6 +267,7 @@ def test_parts_lifecycle_initialize_with_package_repositories_deps_installed(
parse_info={},
project_vars={"version": "1", "grade": "stable"},
extra_build_snaps=["core22"],
+ track_stage_packages=True,
target_arch="amd64",
)
@@ -275,6 +284,7 @@ def test_parts_lifecycle_bad_architecture(parts_data, new_dir):
assets_dir=new_dir,
base="core22",
parallel_build_count=8,
+ track_stage_packages=True,
part_names=[],
package_repositories=[],
adopt_info=None,
@@ -298,6 +308,7 @@ def test_parts_lifecycle_run_with_all_architecture(mocker, parts_data, new_dir):
assets_dir=new_dir,
base="core22",
parallel_build_count=8,
+ track_stage_packages=True,
part_names=[],
package_repositories=[],
adopt_info=None,
@@ -318,6 +329,7 @@ def test_parts_lifecycle_run_with_all_architecture(mocker, parts_data, new_dir):
base="core22",
ignore_local_sources=["*.snap"],
extra_build_snaps=None,
+ track_stage_packages=True,
parallel_build_count=8,
project_name="test-project",
project_vars_part_name=None,
diff --git a/tests/unit/parts/test_setup_assets.py b/tests/unit/parts/test_setup_assets.py
index 45f4e9dc8f..eaf75825b1 100644
--- a/tests/unit/parts/test_setup_assets.py
+++ b/tests/unit/parts/test_setup_assets.py
@@ -442,7 +442,6 @@ def test_setup_assets_hook_command_chain_error(self, yaml_data, new_dir):
)
def test_command_chain_path_not_found(self, new_dir):
-
with pytest.raises(errors.SnapcraftError) as raised:
_validate_command_chain(["file-not-found"], name="foo", prime_dir=new_dir)
diff --git a/tests/unit/store/test_channel_map.py b/tests/unit/store/test_channel_map.py
index 062bb5e408..50a100e4fc 100644
--- a/tests/unit/store/test_channel_map.py
+++ b/tests/unit/store/test_channel_map.py
@@ -390,7 +390,6 @@ def test_channel_map():
def test_channel_map_from_list_releases_model():
-
list_releases = SnapListReleasesModel.unmarshal(
{
"channel-map": [
diff --git a/tests/unit/test_projects.py b/tests/unit/test_projects.py
index 82866777a9..fadc38e17b 100644
--- a/tests/unit/test_projects.py
+++ b/tests/unit/test_projects.py
@@ -104,6 +104,8 @@ def test_project_defaults(self, project_yaml_data):
)
]
assert project.ua_services is None
+ assert project.system_usernames is None
+ assert project.provenance is None
def test_app_defaults(self, project_yaml_data):
data = project_yaml_data(apps={"app1": {"command": "/bin/true"}})
@@ -1018,6 +1020,47 @@ def test_app_sockets_valid_socket_mode(self, socket_mode, socket_yaml_data):
with pytest.raises(errors.ProjectValidationError, match=error):
Project.unmarshal(data)
+ @pytest.mark.parametrize(
+ "system_username",
+ [
+ {"snap_daemon": {"scope": "shared"}},
+ {"snap_microk8s": {"scope": "shared"}},
+ {"snap_aziotedge": {"scope": "shared"}},
+ {"snap_aziotdu": {"scope": "shared"}},
+ {"snap_daemon": "shared"},
+ {"snap_microk8s": "shared"},
+ {"snap_aziotedge": "shared"},
+ {"snap_aziotdu": "shared"},
+ ],
+ )
+ def test_project_system_usernames_valid(self, system_username, project_yaml_data):
+ project = Project.unmarshal(project_yaml_data(system_usernames=system_username))
+ assert project.system_usernames == system_username
+
+ @pytest.mark.parametrize(
+ "system_username",
+ [
+ 0,
+ "string",
+ ],
+ )
+ def test_project_system_usernames_invalid(self, system_username, project_yaml_data):
+ error = "- value is not a valid dict"
+ with pytest.raises(errors.ProjectValidationError, match=error):
+ Project.unmarshal(project_yaml_data(system_usernames=system_username))
+
+ def test_project_provenance(self, project_yaml_data):
+ """Verify provenance is parsed."""
+ project = Project.unmarshal(project_yaml_data(provenance="test-provenance-1"))
+ assert project.provenance == "test-provenance-1"
+
+ @pytest.mark.parametrize("provenance", ["invalid$", "invalid_invalid"])
+ def test_project_provenance_invalid(self, provenance, project_yaml_data):
+ """Verify invalid provenance values raises an error."""
+ error = "provenance must consist of alphanumeric characters and/or hyphens."
+ with pytest.raises(errors.ProjectValidationError, match=error):
+ Project.unmarshal(project_yaml_data(provenance=provenance))
+
class TestGrammarValidation:
"""Basic grammar validation testing."""
@@ -1148,31 +1191,6 @@ def test_grammar_syntax_error(self, project_yaml_data):
with pytest.raises(errors.ProjectValidationError, match=error):
GrammarAwareProject.validate_grammar(data)
- @pytest.mark.parametrize(
- "system_username",
- [
- {"snap_daemon": {"scope": "shared"}},
- {"snap_microk8s": {"scope": "shared"}},
- {"snap_daemon": "shared"},
- {"snap_microk8s": "shared"},
- ],
- )
- def test_project_system_usernames_valid(self, system_username, project_yaml_data):
- project = Project.unmarshal(project_yaml_data(system_usernames=system_username))
- assert project.system_usernames == system_username
-
- @pytest.mark.parametrize(
- "system_username",
- [
- 0,
- "string",
- ],
- )
- def test_project_system_usernames_invalid(self, system_username, project_yaml_data):
- error = "- value is not a valid dict"
- with pytest.raises(errors.ProjectValidationError, match=error):
- Project.unmarshal(project_yaml_data(system_usernames=system_username))
-
def test_get_snap_project_with_base(snapcraft_yaml):
project = Project.unmarshal(snapcraft_yaml(base="core22"))
diff --git a/tests/unit/test_providers.py b/tests/unit/test_providers.py
index 6850ffca4e..224bd3f9e9 100644
--- a/tests/unit/test_providers.py
+++ b/tests/unit/test_providers.py
@@ -15,7 +15,7 @@
# along with this program. If not, see .
from pathlib import Path
-from unittest.mock import MagicMock, Mock, call, patch
+from unittest.mock import ANY, MagicMock, Mock, call, patch
import pytest
from craft_providers import ProviderError, bases
@@ -254,6 +254,7 @@ def test_get_command_environment_passthrough(
monkeypatch.setenv("SNAPCRAFT_BUILD_FOR", "test-build-for")
monkeypatch.setenv("SNAPCRAFT_BUILD_INFO", "test-build-info")
monkeypatch.setenv("SNAPCRAFT_IMAGE_INFO", "test-image-info")
+ monkeypatch.setenv("SNAPCRAFT_MAX_PARALLEL_BUILD_COUNT", "test-build-count")
# ensure other variables are not being passed
monkeypatch.setenv("other_var", "test-other-var")
@@ -270,6 +271,7 @@ def test_get_command_environment_passthrough(
"SNAPCRAFT_BUILD_FOR": "test-build-for",
"SNAPCRAFT_BUILD_INFO": "test-build-info",
"SNAPCRAFT_IMAGE_INFO": "test-image-info",
+ "SNAPCRAFT_MAX_PARALLEL_BUILD_COUNT": "test-build-count",
}
@@ -497,3 +499,24 @@ def test_get_provider_snap_config_default(mocker, platform, expected_provider):
actual_provider = providers.get_provider()
assert isinstance(actual_provider, expected_provider)
+
+
+@pytest.mark.parametrize("bind_ssh", [False, True])
+def test_prepare_instance(bind_ssh, mock_instance, mocker, tmp_path):
+ """Verify instance is properly prepared."""
+ providers.prepare_instance(
+ instance=mock_instance, host_project_path=tmp_path, bind_ssh=bind_ssh
+ )
+
+ mock_instance.mount.assert_has_calls(
+ [call(host_source=tmp_path, target=Path("/root/project"))]
+ )
+
+ if bind_ssh:
+ mock_instance.mount.assert_has_calls(
+ [call(host_source=Path().home() / ".ssh", target=Path("/root/.ssh"))]
+ )
+
+ mock_instance.push_file_io.assert_called_with(
+ content=ANY, destination=Path("/root/.bashrc"), file_mode="644"
+ )
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index fcf63cb669..450aa9dc12 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -18,12 +18,28 @@
from pathlib import Path
from textwrap import dedent
from typing import List
+from unittest.mock import call, patch
import pytest
from snapcraft import errors, utils
+@pytest.fixture
+def mock_isatty(mocker):
+ yield mocker.patch("snapcraft.utils.sys.stdin.isatty", return_value=True)
+
+
+@pytest.fixture
+def mock_input(mocker):
+ yield mocker.patch("snapcraft.utils.input", return_value="")
+
+
+@pytest.fixture
+def mock_is_managed_mode(mocker):
+ yield mocker.patch("snapcraft.utils.is_managed_mode", return_value=False)
+
+
@pytest.mark.parametrize(
"value",
[
@@ -281,6 +297,21 @@ def test_humanize_list(items, conjunction, expected):
assert utils.humanize_list(items, conjunction) == expected
+def test_humanize_list_sorted():
+ """Verify `sort` parameter."""
+ input_list = ["z", "a", "m test", "1"]
+
+ # unsorted list is in the same order as the original list
+ expected_list_unsorted = "'z', 'a', 'm test', and '1'"
+
+ # sorted list is sorted alphanumerically
+ expected_list_sorted = "'1', 'a', 'm test', and 'z'"
+
+ assert utils.humanize_list(input_list, "and") == expected_list_sorted
+ assert utils.humanize_list(input_list, "and", sort=True) == expected_list_sorted
+ assert utils.humanize_list(input_list, "and", sort=False) == expected_list_unsorted
+
+
#################
# Library Paths #
#################
@@ -486,3 +517,73 @@ def test_is_snapcraft_running_from_snap(monkeypatch, snap_name, snap, result):
monkeypatch.setenv("SNAP", snap)
assert utils.is_snapcraft_running_from_snap() == result
+
+
+#####################
+# Confirm with user #
+#####################
+
+
+def test_confirm_with_user_defaults_with_tty(mock_input, mock_isatty):
+ mock_input.return_value = ""
+ mock_isatty.return_value = True
+
+ assert utils.confirm_with_user("prompt", default=True) is True
+ assert mock_input.mock_calls == [call("prompt [Y/n]: ")]
+ mock_input.reset_mock()
+
+ assert utils.confirm_with_user("prompt", default=False) is False
+ assert mock_input.mock_calls == [call("prompt [y/N]: ")]
+
+
+def test_confirm_with_user_defaults_without_tty(mock_input, mock_isatty):
+ mock_isatty.return_value = False
+
+ assert utils.confirm_with_user("prompt", default=True) is True
+ assert utils.confirm_with_user("prompt", default=False) is False
+
+ assert mock_input.mock_calls == []
+
+
+@pytest.mark.parametrize(
+ "user_input,expected",
+ [
+ ("y", True),
+ ("Y", True),
+ ("yes", True),
+ ("YES", True),
+ ("n", False),
+ ("N", False),
+ ("no", False),
+ ("NO", False),
+ ("other", False),
+ ("", False),
+ ],
+)
+def test_confirm_with_user(user_input, expected, mock_input, mock_isatty):
+ """Verify different inputs are accepted with a tendency to interpret as 'no'."""
+ mock_input.return_value = user_input
+
+ assert utils.confirm_with_user("prompt") == expected
+ assert mock_input.mock_calls == [call("prompt [y/N]: ")]
+
+
+def test_confirm_with_user_errors_in_managed_mode(mock_is_managed_mode):
+ mock_is_managed_mode.return_value = True
+
+ with pytest.raises(RuntimeError):
+ utils.confirm_with_user("prompt")
+
+
+def test_confirm_with_user_pause_emitter(mock_isatty, emitter):
+ """The emitter should be paused when using the terminal."""
+ mock_isatty.return_value = True
+
+ # pylint: disable-next=unused-argument
+ def fake_input(prompt):
+ """Check if the Emitter is paused."""
+ assert emitter.paused
+ return ""
+
+ with patch("snapcraft.utils.input", fake_input):
+ utils.confirm_with_user("prompt")
diff --git a/tools/api/conf.py b/tools/api/conf.py
index bfaeac6471..ea48e0bbba 100644
--- a/tools/api/conf.py
+++ b/tools/api/conf.py
@@ -55,7 +55,7 @@
# General information about the project.
project = "snapcraft"
-copyright = "2017, Canonical"
+copyright = "2017, Canonical" # noqa A001 Variable `copyright` is shadowing a python builtin
author = "Canonical"
# The version info for the project you're documenting, acts as replacement for
diff --git a/tools/environment-setup-local.sh b/tools/environment-setup-local.sh
index 347c84e09b..0b88eed6d4 100755
--- a/tools/environment-setup-local.sh
+++ b/tools/environment-setup-local.sh
@@ -47,5 +47,8 @@ sudo snap install black --beta
# Install shellcheck for static tests.
sudo snap install shellcheck
+# Install pyright for static tests.
+sudo snap install pyright --classic
+
echo "Virtual environment may be activated by running:"
echo "source ${SNAPCRAFT_VIRTUAL_ENV_DIR}/bin/activate"
diff --git a/tools/run_ppa_autopkgtests.py b/tools/run_ppa_autopkgtests.py
index e7c7086578..8f5450c4a2 100755
--- a/tools/run_ppa_autopkgtests.py
+++ b/tools/run_ppa_autopkgtests.py
@@ -17,6 +17,7 @@
import os
import subprocess
+import sys
import tempfile
from launchpadlib.launchpad import Launchpad
@@ -84,7 +85,7 @@ def request_autopkgtest_execution(cookie_path, distro, architecture, version):
]
)
if "Test request submitted" not in output:
- exit("Failed to request the autopkgtest")
+ sys.exit("Failed to request the autopkgtest")
if __name__ == "__main__":