From 276cd861c0d17d82b8937df8ceb20a77b71f1d9a Mon Sep 17 00:00:00 2001 From: Chris Patterson Date: Mon, 6 Jan 2020 14:42:16 -0500 Subject: [PATCH 1/4] errors: introduce SnapcraftPluginAssertionError() If a plugin detects an error prior to attempting to build a part's source, it can raise SnapcraftPluginAssertionError() with a brief reason and optional details. SnapcraftEnvironmentError() was also a viable choice, but it's slated to be nuked and as it is too generic. This seems like a fair middle ground that can be re-used across the the plugins when they make an assertion that fails to check out at runtime. Signed-off-by: Chris Patterson --- snapcraft/internal/errors.py | 16 ++++++++++++++++ tests/unit/test_errors.py | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/snapcraft/internal/errors.py b/snapcraft/internal/errors.py index 7c591de91f..8569b3ecba 100644 --- a/snapcraft/internal/errors.py +++ b/snapcraft/internal/errors.py @@ -736,3 +736,19 @@ def get_brief(self) -> str: def get_resolution(self) -> str: return "Remove the suspect files from the snap using the `stage` or `prime` keywords." + + +class SnapcraftPluginAssertionError(SnapcraftException): + def __init__(self, *, name: str, reason: str, details: str = "") -> None: + self._name = name + self._reason = reason + self._details = details + + def get_brief(self) -> str: + return f"Unable to build {self._name!r}: {self._reason}" + + def get_details(self) -> str: + return self._details + + def get_resolution(self) -> str: + return "Ensure the part's configuration and sources are correct." diff --git a/tests/unit/test_errors.py b/tests/unit/test_errors.py index 6e571d4486..8613ce5d3d 100644 --- a/tests/unit/test_errors.py +++ b/tests/unit/test_errors.py @@ -853,6 +853,18 @@ class SnapcraftExceptionTests(unit.TestCase): "expected_reportable": False, }, ), + ( + "SnapcraftPluginAssertionError", + { + "exception": errors.SnapcraftPluginAssertionError, + "kwargs": {"name": "part-name", "reason": "missing important file"}, + "expected_brief": "Unable to build 'part-name': missing important file", + "expected_resolution": "Ensure the part's configuration and sources are correct.", + "expected_details": "", + "expected_docs_url": None, + "expected_reportable": False, + }, + ), ( "XAttributeError", { From 817aba0a53a9803f6630f30af64cb3978461d595 Mon Sep 17 00:00:00 2001 From: Chris Patterson Date: Fri, 13 Dec 2019 14:44:28 -0500 Subject: [PATCH 2/4] rust plugin: support projects with workspaces Unfortunately, `cargo install` does not support workspaces yet: https://github.com/rust-lang/cargo/issues/7599 Alternatively, we use `cargo build`, which will respect the Cargo.lock configuration. It however, does require a bit more hoop jumping to determine which binaries were built and install them. Introudce _install_workspace_artifacts() to install the built executables into the correct paths. Testing has covered executables and libraries, though dynamic linking is not quite yet supported by the rust plugin (at least in my testing, it will have unmnet dependencies on libstd-.so). We can address that feature gap in the future, but likely doesn't affect snap users because they are probably using the standard linking process which doesn't require libstd (likely due to static linking of those dependencies). `cargo build` has an unstable flag option for `--out-dir` which may simplifiy the install process, but is currently unavailable for stable use: https://doc.rust-lang.org/cargo/reference/unstable.html#out-dir Add/update unit tests for coverage and add a spead test. Signed-off-by: Chris Patterson --- snapcraft/plugins/rust.py | 89 +++- .../rust/snaps/rust-workspace-fib/Cargo.toml | 6 + .../rust-workspace-fib/calc-fib/Cargo.toml | 11 + .../rust-workspace-fib/calc-fib/src/main.rs | 25 + .../snaps/rust-workspace-fib/fib/Cargo.toml | 9 + .../snaps/rust-workspace-fib/fib/src/lib.rs | 13 + .../rust-workspace-fib/snap/snapcraft.yaml | 17 + tests/spread/plugins/rust/workspace/task.yaml | 36 ++ tests/unit/plugins/test_rust.py | 440 ++++++++++-------- 9 files changed, 435 insertions(+), 211 deletions(-) create mode 100644 tests/spread/plugins/rust/snaps/rust-workspace-fib/Cargo.toml create mode 100644 tests/spread/plugins/rust/snaps/rust-workspace-fib/calc-fib/Cargo.toml create mode 100644 tests/spread/plugins/rust/snaps/rust-workspace-fib/calc-fib/src/main.rs create mode 100644 tests/spread/plugins/rust/snaps/rust-workspace-fib/fib/Cargo.toml create mode 100644 tests/spread/plugins/rust/snaps/rust-workspace-fib/fib/src/lib.rs create mode 100644 tests/spread/plugins/rust/snaps/rust-workspace-fib/snap/snapcraft.yaml create mode 100644 tests/spread/plugins/rust/workspace/task.yaml diff --git a/snapcraft/plugins/rust.py b/snapcraft/plugins/rust.py index 385b1131f8..adf6eb7839 100644 --- a/snapcraft/plugins/rust.py +++ b/snapcraft/plugins/rust.py @@ -45,6 +45,7 @@ import collections import logging import os +from pathlib import Path from contextlib import suppress from typing import List, Optional @@ -52,7 +53,7 @@ import snapcraft from snapcraft import sources -from snapcraft import shell_utils +from snapcraft import file_utils, shell_utils from snapcraft.internal import errors _RUSTUP = "https://sh.rustup.rs/" @@ -194,21 +195,78 @@ def _get_target(self) -> str: ) return rust_target.format("unknown-linux", "gnu") + def _project_uses_workspace(self) -> bool: + cargo_toml_path = Path(self.builddir, "Cargo.toml") + + try: + config = open(cargo_toml_path).read() + except FileNotFoundError: + raise errors.SnapcraftPluginAssertionError( + name=self.name, reason="missing required Cargo.toml in source directory" + ) + + return "workspace" in toml.loads(config) + + def _install_workspace_artifacts(self) -> None: + """Install workspace artifacts.""" + # Find artifacts in release directory. + release_dir = Path(self.builddir, "target", "release") + + # Install binaries to bin/. + bins_dir = Path(self.installdir, "bin") + bins_dir.mkdir(parents=True, exist_ok=True) + + # Install shared objects to usr/lib/. + # TODO: Dynamic library support needs to be properly added. + # Although we install libraries if we find them, they are most + # likely going to be missing dependencies, e.g.: + # /home/ubuntu/.cargo/toolchains/stable-x86_64-unknown-linux-gnu/lib/libstd-fae576517123aa4e.so + libs_dir = Path(self.installdir, "usr", "lib", self.project.arch_triplet) + libs_dir.mkdir(parents=True, exist_ok=True) + + # Cargo build marks binaries and shared objects executable... + # Search target directory to get these and install them to the + # correct location. + for path in release_dir.iterdir(): + if not os.path.isfile(path): + continue + if not os.access(path, os.X_OK): + continue + + # File is executable, now to determine if bin or lib... + if path.name.endswith(".so"): + file_utils.link_or_copy(path.as_posix(), libs_dir.as_posix()) + else: + file_utils.link_or_copy(path.as_posix(), bins_dir.as_posix()) + def build(self): super().build() - # Write a minimal config. - self._write_cargo_config() + uses_workspaces = self._project_uses_workspace() + + if uses_workspaces: + # This is a bit ugly because `cargo install` does not yet support + # workspaces. Alternatively, there is a perhaps better option + # to use `cargo-build --out-dir`, but `--out-dir` is considered + # unstable and unavailable for use yet on the stable channel. It + # may be better because the use of `cargo install` without `--locked` + # does not appear to honor Cargo.lock, while `cargo build` does by + # default, if it is present. + install_cmd = [self._cargo_cmd, "build", "--release"] + else: + # Write a minimal config. + self._write_cargo_config() + + install_cmd = [ + self._cargo_cmd, + "install", + "--path", + self.builddir, + "--root", + self.installdir, + "--force", + ] - install_cmd = [ - self._cargo_cmd, - "install", - "--path", - self.builddir, - "--root", - self.installdir, - "--force", - ] toolchain = self._get_toolchain() if toolchain is not None: install_cmd.insert(1, "+{}".format(toolchain)) @@ -224,11 +282,16 @@ def build(self): install_cmd.append(" ".join(self.options.rust_features)) # build and install. - self.run(install_cmd, env=self._build_env()) + self.run(install_cmd, env=self._build_env(), cwd=self.builddir) # Finally, record. self._record_manifest() + if uses_workspaces: + # We need to install the workspace artifacts as a workaround until + # `cargo build` supports `out-dir` in "stable". + self._install_workspace_artifacts() + def _build_env(self): env = os.environ.copy() diff --git a/tests/spread/plugins/rust/snaps/rust-workspace-fib/Cargo.toml b/tests/spread/plugins/rust/snaps/rust-workspace-fib/Cargo.toml new file mode 100644 index 0000000000..3cb5e22a99 --- /dev/null +++ b/tests/spread/plugins/rust/snaps/rust-workspace-fib/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] + +members = [ + "fib", + "calc-fib" +] diff --git a/tests/spread/plugins/rust/snaps/rust-workspace-fib/calc-fib/Cargo.toml b/tests/spread/plugins/rust/snaps/rust-workspace-fib/calc-fib/Cargo.toml new file mode 100644 index 0000000000..ff3525e453 --- /dev/null +++ b/tests/spread/plugins/rust/snaps/rust-workspace-fib/calc-fib/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "calc-fib" +version = "0.1.0" +authors = ["Chris Patterson "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +regex = "1" +fib = { path = "../fib" } diff --git a/tests/spread/plugins/rust/snaps/rust-workspace-fib/calc-fib/src/main.rs b/tests/spread/plugins/rust/snaps/rust-workspace-fib/calc-fib/src/main.rs new file mode 100644 index 0000000000..2399b32675 --- /dev/null +++ b/tests/spread/plugins/rust/snaps/rust-workspace-fib/calc-fib/src/main.rs @@ -0,0 +1,25 @@ +use std::env; +extern crate fib; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + eprintln!("please specify number"); + return + } + + let num_string = &args[1]; + let number: i32 = match num_string.parse() { + Ok(n) => { + n + }, + Err(_) => { + eprintln!("error: not an integer"); + return; + } + }; + + let sum = fib::fib(number); + println!("{}", sum); +} diff --git a/tests/spread/plugins/rust/snaps/rust-workspace-fib/fib/Cargo.toml b/tests/spread/plugins/rust/snaps/rust-workspace-fib/fib/Cargo.toml new file mode 100644 index 0000000000..b3c6f99252 --- /dev/null +++ b/tests/spread/plugins/rust/snaps/rust-workspace-fib/fib/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "fib" +version = "0.1.0" +authors = ["Chris Patterson "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/tests/spread/plugins/rust/snaps/rust-workspace-fib/fib/src/lib.rs b/tests/spread/plugins/rust/snaps/rust-workspace-fib/fib/src/lib.rs new file mode 100644 index 0000000000..575e07e3df --- /dev/null +++ b/tests/spread/plugins/rust/snaps/rust-workspace-fib/fib/src/lib.rs @@ -0,0 +1,13 @@ +// Terribly inefficient recursive fib routine. +pub fn fib(n: i32) -> u64 { + if n < 0 { + panic!("invalid: {}", n); + } + if n == 0 { + return 0; + } else if n == 1 { + return 1; + } else { + return fib(n - 1) + fib(n - 2) + } +} diff --git a/tests/spread/plugins/rust/snaps/rust-workspace-fib/snap/snapcraft.yaml b/tests/spread/plugins/rust/snaps/rust-workspace-fib/snap/snapcraft.yaml new file mode 100644 index 0000000000..e3f7241416 --- /dev/null +++ b/tests/spread/plugins/rust/snaps/rust-workspace-fib/snap/snapcraft.yaml @@ -0,0 +1,17 @@ +name: calc-fib +version: "1.0" +summary: Test the rust plugin with sworkspaces. +description: | + This is a basic rust snap with workspaces. + +grade: devel +confinement: strict + +apps: + calc-fib: + command: bin/calc-fib + +parts: + calc-fib: + plugin: rust + source: . diff --git a/tests/spread/plugins/rust/workspace/task.yaml b/tests/spread/plugins/rust/workspace/task.yaml new file mode 100644 index 0000000000..6f76e4e925 --- /dev/null +++ b/tests/spread/plugins/rust/workspace/task.yaml @@ -0,0 +1,36 @@ +summary: Build and run a workspace rust snap + +systems: + - ubuntu-18.04 + - ubuntu-18.04-64 + - ubuntu-18.04-amd64 + - ubuntu-18.04-i386 + - ubuntu-18.04-armhf + - ubuntu-16.04 + - ubuntu-16.04-64 + - ubuntu-16.04-amd64 + - ubuntu-16.04-i386 + - ubuntu-16.04-armhf + +environment: + SNAP_DIR: ../snaps/rust-workspace-fib + +prepare: | + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + set_base "$SNAP_DIR/snap/snapcraft.yaml" + +restore: | + cd "$SNAP_DIR" + snapcraft clean + rm -f ./*.snap + + #shellcheck source=tests/spread/tools/snapcraft-yaml.sh + . "$TOOLS_DIR/snapcraft-yaml.sh" + restore_yaml "snap/snapcraft.yaml" + +execute: | + cd "$SNAP_DIR" + snapcraft + sudo snap install calc-fib_*.snap --dangerous + [ "$(calc-fib 8)" = "21" ] diff --git a/tests/unit/plugins/test_rust.py b/tests/unit/plugins/test_rust.py index a3b6e23524..6ea7f3df98 100644 --- a/tests/unit/plugins/test_rust.py +++ b/tests/unit/plugins/test_rust.py @@ -16,6 +16,7 @@ # along with this program. If not, see . import collections +from pathlib import Path import os import subprocess import textwrap @@ -66,6 +67,22 @@ def exists_mock(*args, **kwargs): patcher.start() self.addCleanup(patcher.stop) + self.project = snapcraft.project.Project( + snapcraft_yaml_file_path=self.make_snapcraft_yaml( + textwrap.dedent( + """\ + name: test-snap + base: core16 + """ + ) + ) + ) + + self.plugin = rust.RustPlugin("test-part", self.options, self.project) + os.makedirs(self.plugin.sourcedir) + os.makedirs(self.plugin.builddir) + open(Path(self.plugin.builddir, "Cargo.toml"), "w").write("") + class RustPluginPropertiesTest(unit.TestCase): def test_schema(self): @@ -132,6 +149,14 @@ class RustPluginCrossCompileTest(RustPluginBaseTest): def setUp(self): super().setUp() + patcher = mock.patch( + "snapcraft.project.Project.is_cross_compiling", return_value=True + ) + patcher.start() + self.addCleanup(patcher.stop) + + @mock.patch("snapcraft.internal.sources._script.Script.download") + def test_cross_compile(self, mock_download): self.project = snapcraft.project.Project( target_deb_arch=self.deb_arch, snapcraft_yaml_file_path=self.make_snapcraft_yaml( @@ -144,153 +169,158 @@ def setUp(self): ), ) - patcher = mock.patch( - "snapcraft.project.Project.is_cross_compiling", return_value=True - ) - patcher.start() - self.addCleanup(patcher.stop) - - @mock.patch("snapcraft.internal.sources._script.Script.download") - def test_cross_compile(self, mock_download): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - - plugin.pull() + self.plugin = rust.RustPlugin("test-part", self.options, self.project) + self.plugin.pull() self.assertThat(self.run_mock.call_count, Equals(4)) self.run_mock.assert_has_calls( [ mock.call( [ - os.path.join(plugin._rustup_dir, "rustup.sh"), + os.path.join(self.plugin._rustup_dir, "rustup.sh"), "-y", "--no-modify-path", "--default-toolchain", "none", ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ), mock.call( - [plugin._rustup_cmd, "install", "stable"], - cwd=plugin.builddir, - env=plugin._build_env(), + [self.plugin._rustup_cmd, "install", "stable"], + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), mock.call( [ - plugin._rustup_cmd, + self.plugin._rustup_cmd, "target", "add", "--toolchain", "stable", self.target, ], - cwd=plugin.builddir, - env=plugin._build_env(), + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), mock.call( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "+stable", "fetch", "--manifest-path", - os.path.join(plugin.sourcedir, "Cargo.toml"), + os.path.join(self.plugin.sourcedir, "Cargo.toml"), ], - cwd=plugin.builddir, - env=plugin._build_env(), + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), ] ) self.run_mock.reset_mock() - plugin.build() + self.plugin.build() - self.assertThat(os.path.join(plugin.builddir, ".cargo", "config"), FileExists()) + self.assertThat( + os.path.join(self.plugin.builddir, ".cargo", "config"), FileExists() + ) self.assertThat(self.run_mock.call_count, Equals(1)) self.run_mock.assert_has_calls( [ mock.call( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "+stable", "install", "--path", - plugin.builddir, + self.plugin.builddir, "--root", - plugin.installdir, + self.plugin.installdir, "--force", "--target", self.target, ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ) ] ) @mock.patch("snapcraft.internal.sources._script.Script.download") def test_cross_compile_with_rust_toolchain_file(self, mock_download): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - open(os.path.join(plugin.sourcedir, "rust-toolchain"), "w").close() + self.project = snapcraft.project.Project( + target_deb_arch=self.deb_arch, + snapcraft_yaml_file_path=self.make_snapcraft_yaml( + textwrap.dedent( + """\ + name: test-snap + base: core16 + """ + ) + ), + ) + + self.plugin = rust.RustPlugin("test-part", self.options, self.project) + open(os.path.join(self.plugin.sourcedir, "rust-toolchain"), "w").close() - plugin.pull() + self.plugin.pull() self.assertThat(self.run_mock.call_count, Equals(3)) self.run_mock.assert_has_calls( [ mock.call( [ - os.path.join(plugin._rustup_dir, "rustup.sh"), + os.path.join(self.plugin._rustup_dir, "rustup.sh"), "-y", "--no-modify-path", ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ), mock.call( - [plugin._rustup_cmd, "target", "add", self.target], - cwd=plugin.builddir, - env=plugin._build_env(), + [self.plugin._rustup_cmd, "target", "add", self.target], + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), mock.call( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "fetch", "--manifest-path", - os.path.join(plugin.sourcedir, "Cargo.toml"), + os.path.join(self.plugin.sourcedir, "Cargo.toml"), ], - cwd=plugin.builddir, - env=plugin._build_env(), + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), ] ) self.run_mock.reset_mock() - plugin.build() + self.plugin.build() - self.assertThat(os.path.join(plugin.builddir, ".cargo", "config"), FileExists()) + self.assertThat( + os.path.join(self.plugin.builddir, ".cargo", "config"), FileExists() + ) self.assertThat(self.run_mock.call_count, Equals(1)) self.run_mock.assert_has_calls( [ mock.call( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "install", "--path", - plugin.builddir, + self.plugin.builddir, "--root", - plugin.installdir, + self.plugin.installdir, "--force", "--target", self.target, ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ) ] ) @@ -300,28 +330,15 @@ class RustPluginTest(RustPluginBaseTest): def setUp(self): super().setUp() - self.project = snapcraft.project.Project( - snapcraft_yaml_file_path=self.make_snapcraft_yaml( - textwrap.dedent( - """\ - name: test-snap - base: core16 - """ - ) - ) - ) - if self.project.deb_arch == "s390x": self.skipTest("architecture is not supported by rust") @mock.patch.object(rust.sources, "Script") def test_pull(self, script_mock): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - plugin.options.rust_revision = [] - plugin.options.rust_channel = [] + self.plugin.options.rust_revision = [] + self.plugin.options.rust_channel = [] - plugin.pull() + self.plugin.pull() self.assertThat(self.run_mock.call_count, Equals(3)) @@ -329,44 +346,42 @@ def test_pull(self, script_mock): [ mock.call( [ - os.path.join(plugin._rustup_dir, "rustup.sh"), + os.path.join(self.plugin._rustup_dir, "rustup.sh"), "-y", "--no-modify-path", "--default-toolchain", "none", ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ), mock.call( - [plugin._rustup_cmd, "install", "stable"], - cwd=plugin.builddir, - env=plugin._build_env(), + [self.plugin._rustup_cmd, "install", "stable"], + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), mock.call( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "+stable", "fetch", "--manifest-path", - os.path.join(plugin.sourcedir, "Cargo.toml"), + os.path.join(self.plugin.sourcedir, "Cargo.toml"), ], - cwd=plugin.builddir, - env=plugin._build_env(), + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), ] ) @mock.patch.object(rust.sources, "Script") def test_pull_with_rust_toolchain_file(self, script_mock): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - open(os.path.join(plugin.sourcedir, "rust-toolchain"), "w").close() + open(os.path.join(self.plugin.sourcedir, "rust-toolchain"), "w").close() - plugin.options.rust_revision = [] - plugin.options.rust_channel = [] + self.plugin.options.rust_revision = [] + self.plugin.options.rust_channel = [] - plugin.pull() + self.plugin.pull() self.assertThat(self.run_mock.call_count, Equals(2)) @@ -374,34 +389,32 @@ def test_pull_with_rust_toolchain_file(self, script_mock): [ mock.call( [ - os.path.join(plugin._rustup_dir, "rustup.sh"), + os.path.join(self.plugin._rustup_dir, "rustup.sh"), "-y", "--no-modify-path", ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ), mock.call( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "fetch", "--manifest-path", - os.path.join(plugin.sourcedir, "Cargo.toml"), + os.path.join(self.plugin.sourcedir, "Cargo.toml"), ], - cwd=plugin.builddir, - env=plugin._build_env(), + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), ] ) @mock.patch.object(rust.sources, "Script") def test_pull_with_channel(self, script_mock): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - plugin.options.rust_revision = "" - plugin.options.rust_channel = "nightly" + self.plugin.options.rust_revision = "" + self.plugin.options.rust_channel = "nightly" - plugin.pull() + self.plugin.pull() self.assertThat(self.run_mock.call_count, Equals(3)) @@ -409,42 +422,40 @@ def test_pull_with_channel(self, script_mock): [ mock.call( [ - os.path.join(plugin._rustup_dir, "rustup.sh"), + os.path.join(self.plugin._rustup_dir, "rustup.sh"), "-y", "--no-modify-path", "--default-toolchain", "none", ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ), mock.call( - [plugin._rustup_cmd, "install", "nightly"], - cwd=plugin.builddir, - env=plugin._build_env(), + [self.plugin._rustup_cmd, "install", "nightly"], + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), mock.call( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "+nightly", "fetch", "--manifest-path", - os.path.join(plugin.sourcedir, "Cargo.toml"), + os.path.join(self.plugin.sourcedir, "Cargo.toml"), ], - cwd=plugin.builddir, - env=plugin._build_env(), + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), ] ) @mock.patch.object(rust.sources, "Script") def test_pull_with_revision(self, script_mock): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - plugin.options.rust_revision = "1.13.0" - plugin.options.rust_channel = "" + self.plugin.options.rust_revision = "1.13.0" + self.plugin.options.rust_channel = "" - plugin.pull() + self.plugin.pull() self.assertThat(self.run_mock.call_count, Equals(3)) @@ -452,41 +463,39 @@ def test_pull_with_revision(self, script_mock): [ mock.call( [ - os.path.join(plugin._rustup_dir, "rustup.sh"), + os.path.join(self.plugin._rustup_dir, "rustup.sh"), "-y", "--no-modify-path", "--default-toolchain", "none", ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ), mock.call( - [plugin._rustup_cmd, "install", "1.13.0"], - cwd=plugin.builddir, - env=plugin._build_env(), + [self.plugin._rustup_cmd, "install", "1.13.0"], + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), mock.call( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "+1.13.0", "fetch", "--manifest-path", - os.path.join(plugin.sourcedir, "Cargo.toml"), + os.path.join(self.plugin.sourcedir, "Cargo.toml"), ], - cwd=plugin.builddir, - env=plugin._build_env(), + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), ] ) @mock.patch.object(rust.sources, "Script") def test_pull_with_source_and_source_subdir(self, script_mock): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - plugin.options.source_subdir = "test-subdir" + self.plugin.options.source_subdir = "test-subdir" - plugin.pull() + self.plugin.pull() self.assertThat(self.run_mock.call_count, Equals(3)) @@ -494,146 +503,178 @@ def test_pull_with_source_and_source_subdir(self, script_mock): [ mock.call( [ - os.path.join(plugin._rustup_dir, "rustup.sh"), + os.path.join(self.plugin._rustup_dir, "rustup.sh"), "-y", "--no-modify-path", "--default-toolchain", "none", ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ), mock.call( - [plugin._rustup_cmd, "install", "stable"], - cwd=plugin.builddir, - env=plugin._build_env(), + [self.plugin._rustup_cmd, "install", "stable"], + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), mock.call( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "+stable", "fetch", "--manifest-path", - os.path.join(plugin.sourcedir, "test-subdir", "Cargo.toml"), + os.path.join( + self.plugin.sourcedir, "test-subdir", "Cargo.toml" + ), ], - cwd=plugin.builddir, - env=plugin._build_env(), + cwd=self.plugin.builddir, + env=self.plugin._build_env(), ), ] ) @mock.patch("snapcraft.ProjectOptions.deb_arch", "fantasy-arch") def test_unsupported_target_arch_raises_exception(self): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - - self.assertRaises(errors.SnapcraftEnvironmentError, plugin._get_target) + self.assertRaises(errors.SnapcraftEnvironmentError, self.plugin._get_target) def test_build(self): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - - plugin.build() + self.plugin.build() self.run_mock.assert_called_once_with( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "+stable", "install", "--path", - plugin.builddir, + self.plugin.builddir, "--root", - plugin.installdir, + self.plugin.installdir, "--force", ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), + ) + + def test_install_workspace_artifacts(self): + release_path = Path(self.plugin.builddir, "target", "release") + os.makedirs(release_path, exist_ok=True) + + p_nonexec = Path(release_path / "nonexec") + open(p_nonexec, "w").write("") + p_nonexec.chmod(0o664) + + p_exec = Path(release_path / "exec") + open(p_exec, "w").write("") + p_exec.chmod(0o755) + + p_exec_so = Path(release_path / "exec.so") + open(p_exec_so, "w").write("") + p_exec_so.chmod(0o755) + + self.plugin._install_workspace_artifacts() + + bindir = Path(self.plugin.installdir, "bin") + bins = list(bindir.iterdir()) + + libdir = Path(self.plugin.installdir, "usr", "lib", self.project.arch_triplet) + libs = list(libdir.iterdir()) + + self.assertThat(bins, Equals([bindir / "exec"])) + self.assertThat(libs, Equals([libdir / "exec.so"])) + + def test_build_workspace(self): + cargo_path = Path(self.plugin.builddir, "Cargo.toml") + with open(cargo_path, "w") as cargo_file: + cargo_file.write("[workspace]" + os.linesep) + release_path = Path(self.plugin.builddir, "target", "release") + os.makedirs(release_path, exist_ok=True) + + self.plugin.build() + + self.run_mock.assert_called_once_with( + [self.plugin._cargo_cmd, "+stable", "build", "--release"], + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ) def test_build_with_rust_toolchain_file(self): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - open(os.path.join(plugin.sourcedir, "rust-toolchain"), "w").close() + open(os.path.join(self.plugin.sourcedir, "rust-toolchain"), "w").close() - plugin.build() + self.plugin.build() self.run_mock.assert_called_once_with( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "install", "--path", - plugin.builddir, + self.plugin.builddir, "--root", - plugin.installdir, + self.plugin.installdir, "--force", ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ) def test_build_with_conditional_compilation(self): - plugin = rust.RustPlugin("test-part", self.options, self.project) - plugin.options.rust_features = ["conditional-compilation"] - os.makedirs(plugin.sourcedir) + self.plugin.options.rust_features = ["conditional-compilation"] - plugin.build() + self.plugin.build() self.run_mock.assert_called_once_with( [ - plugin._cargo_cmd, + self.plugin._cargo_cmd, "+stable", "install", "--path", - plugin.builddir, + self.plugin.builddir, "--root", - plugin.installdir, + self.plugin.installdir, "--force", "--features", "conditional-compilation", ], - cwd=os.path.join(plugin.partdir, "build"), - env=plugin._build_env(), + cwd=os.path.join(self.plugin.partdir, "build"), + env=self.plugin._build_env(), ) def test_get_manifest_with_cargo_lock_file(self): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - os.makedirs(plugin.builddir) - - with open(os.path.join(plugin.builddir, "Cargo.lock"), "w") as cargo_lock_file: + with open( + os.path.join(self.plugin.builddir, "Cargo.lock"), "w" + ) as cargo_lock_file: cargo_lock_file.write("test cargo lock contents") - plugin.build() + self.plugin.build() self.assertThat( - plugin.get_manifest()["cargo-lock-contents"], + self.plugin.get_manifest()["cargo-lock-contents"], Equals("test cargo lock contents"), ) def test_get_manifest_with_unexisting_cargo_lock(self): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - os.makedirs(plugin.builddir) + self.plugin.build() - plugin.build() + self.assertThat( + self.plugin.get_manifest(), Not(Contains("cargo-lock-contents")) + ) - self.assertThat(plugin.get_manifest(), Not(Contains("cargo-lock-contents"))) + def test_workspace(self): + self.plugin.build() - def test_get_manifest_with_cargo_lock_dir(self, *_): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - os.makedirs(plugin.builddir) + self.assertThat( + self.plugin.get_manifest(), Not(Contains("cargo-lock-contents")) + ) - os.mkdir(os.path.join(plugin.builddir, "Cargo.lock")) + def test_get_manifest_with_cargo_lock_dir(self, *_): + os.mkdir(os.path.join(self.plugin.builddir, "Cargo.lock")) - plugin.build() + self.plugin.build() - self.assertThat(plugin.get_manifest(), Not(Contains("cargo-lock-contents"))) + self.assertThat( + self.plugin.get_manifest(), Not(Contains("cargo-lock-contents")) + ) def test_get_manifest_with_versions(self): - plugin = rust.RustPlugin("test-part", self.options, self.project) - os.makedirs(plugin.sourcedir) - original_check_output = subprocess.check_output def side_effect(cmd, *args, **kwargs): @@ -644,20 +685,19 @@ def side_effect(cmd, *args, **kwargs): with mock.patch.object(rust.RustPlugin, "run_output") as run_output_mock: run_output_mock.side_effect = side_effect - plugin.build() + self.plugin.build() expected_manifest = collections.OrderedDict() expected_manifest["rustup-version"] = "test rustup version" expected_manifest["rustc-version"] = "test rustc version" expected_manifest["cargo-version"] = "test cargo version" - self.assertThat(plugin.get_manifest(), Equals(expected_manifest)) + self.assertThat(self.plugin.get_manifest(), Equals(expected_manifest)) def test_write_cargo_config(self): - plugin = rust.RustPlugin("test-part", self.options, self.project) config_toml_path = "config.toml" - plugin._write_cargo_config(cargo_config_path=config_toml_path) + self.plugin._write_cargo_config(cargo_config_path=config_toml_path) self.assertThat(config_toml_path, FileExists()) @@ -667,11 +707,15 @@ def test_write_cargo_config(self): config, Equals( dict( - rustdoc_cmd=plugin._rustdoc_cmd, - rustc_cmd=plugin._rustc_cmd, - arch_triplet=plugin.project.arch_triplet, - jobs=plugin.parallel_build_count, - target={plugin._get_target(): dict(linker=plugin._get_linker())}, + rustdoc_cmd=self.plugin._rustdoc_cmd, + rustc_cmd=self.plugin._rustc_cmd, + arch_triplet=self.plugin.project.arch_triplet, + jobs=self.plugin.parallel_build_count, + target={ + self.plugin._get_target(): dict( + linker=self.plugin._get_linker() + ) + }, ) ), ) From c5156c07cefdcb9778e9b79a3803f0daadc16442 Mon Sep 17 00:00:00 2001 From: Chris Patterson Date: Wed, 8 Jan 2020 00:34:59 +0000 Subject: [PATCH 3/4] rust plugin: use list comprehension when finding artifacts Signed-off-by: Chris Patterson --- snapcraft/plugins/rust.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/snapcraft/plugins/rust.py b/snapcraft/plugins/rust.py index adf6eb7839..5e81415599 100644 --- a/snapcraft/plugins/rust.py +++ b/snapcraft/plugins/rust.py @@ -226,18 +226,17 @@ def _install_workspace_artifacts(self) -> None: # Cargo build marks binaries and shared objects executable... # Search target directory to get these and install them to the - # correct location. - for path in release_dir.iterdir(): - if not os.path.isfile(path): - continue - if not os.access(path, os.X_OK): - continue - - # File is executable, now to determine if bin or lib... - if path.name.endswith(".so"): - file_utils.link_or_copy(path.as_posix(), libs_dir.as_posix()) + # correct location (*.so to lib directory, otherwise bin directory). + executables = [ + i + for i in release_dir.iterdir() + if os.path.isfile(i) and os.access(i, os.X_OK) + ] + for exe_path in executables: + if exe_path.name.endswith(".so"): + file_utils.link_or_copy(exe_path.as_posix(), libs_dir.as_posix()) else: - file_utils.link_or_copy(path.as_posix(), bins_dir.as_posix()) + file_utils.link_or_copy(exe_path.as_posix(), bins_dir.as_posix()) def build(self): super().build() From 51bf9b434f63584f9389e82a6d17052026b2ec68 Mon Sep 17 00:00:00 2001 From: Chris Patterson Date: Wed, 8 Jan 2020 00:35:43 +0000 Subject: [PATCH 4/4] rust plugin: write config config for workspaces Signed-off-by: Chris Patterson --- snapcraft/plugins/rust.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/snapcraft/plugins/rust.py b/snapcraft/plugins/rust.py index 5e81415599..869fca5f25 100644 --- a/snapcraft/plugins/rust.py +++ b/snapcraft/plugins/rust.py @@ -241,8 +241,10 @@ def _install_workspace_artifacts(self) -> None: def build(self): super().build() - uses_workspaces = self._project_uses_workspace() + # Write a minimal config. + self._write_cargo_config() + uses_workspaces = self._project_uses_workspace() if uses_workspaces: # This is a bit ugly because `cargo install` does not yet support # workspaces. Alternatively, there is a perhaps better option @@ -253,9 +255,6 @@ def build(self): # default, if it is present. install_cmd = [self._cargo_cmd, "build", "--release"] else: - # Write a minimal config. - self._write_cargo_config() - install_cmd = [ self._cargo_cmd, "install",