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__":