Skip to content

Commit

Permalink
plugins(legacy/v2): match the Rust plugin implementation ...
Browse files Browse the repository at this point in the history
... with the new one landed in the craft-parts
  • Loading branch information
liushuyu committed Sep 10, 2023
1 parent c8e9b2e commit ffe64d5
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 54 deletions.
170 changes: 133 additions & 37 deletions snapcraft_legacy/plugins/v2/rust.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2020 Canonical Ltd
# Copyright (C) 2020-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
Expand All @@ -14,7 +14,9 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""This rust plugin is useful for building rust based parts.
"""A Snapcraft plugin for Rust applications.
This Rust plugin is useful for building Rust based parts.
Rust uses cargo to drive the build.
Expand All @@ -23,23 +25,46 @@
'sources' topic for the latter.
Additionally, this plugin uses the following plugin-specific keywords:
- rust-channel
(string, default "stable")
Used to select which Rust channel or version to use.
It can be one of "stable", "beta", "nightly" or a version number.
If you don't want this plugin to install Rust toolchain for you,
you can put "none" for this option.
- rust-features
(list of strings)
Features used to build optional dependencies
(list of strings)
Features used to build optional dependencies
- rust-path
(list of strings, default [.])
Build specific workspace crates
Only one item is currently supported.
(list of strings, default [.])
Build specific crates inside the workspace
- rust-no-default-features
(boolean, default False)
Whether to disable the default features in this crate.
Equivalent to setting `--no-default-features` on the commandline.
- rust-use-global-lto
(boolean, default False)
Whether to use global LTO.
This option may significantly impact the build performance but
reducing the final binary size.
This will forcibly enable LTO for all the crates you specified,
regardless of whether you have LTO enabled in the Cargo.toml file
"""

import subprocess

from textwrap import dedent
from typing import Any, Dict, List, Set

from snapcraft_legacy.plugins.v2 import PluginV2


logger = logging.getLogger(__name__)


class RustPlugin(PluginV2):
@classmethod
def get_schema(cls) -> Dict[str, Any]:
Expand All @@ -51,8 +76,6 @@ def get_schema(cls) -> Dict[str, Any]:
"rust-path": {
"type": "array",
"minItems": 1,
# TODO support more than one item.
"maxItems": 1,
"uniqueItems": True,
"items": {"type": "string"},
"default": ["."],
Expand All @@ -63,6 +86,18 @@ def get_schema(cls) -> Dict[str, Any]:
"items": {"type": "string"},
"default": [],
},
"rust-channel": {
"type": ["string", "null"],
"default": None,
},
"rust-use-global-lto": {
"type": "boolean",
"default": False,
},
"rust-no-default-features": {
"type": "boolean",
"default": False,
},
},
"required": ["source"],
}
Expand All @@ -71,39 +106,100 @@ def get_build_snaps(self) -> Set[str]:
return set()

def get_build_packages(self) -> Set[str]:
return {"curl", "gcc", "git"}
return {"curl", "gcc", "git", "pkg-config", "findutils"}

def get_build_environment(self) -> Dict[str, str]:
return {"PATH": "${HOME}/.cargo/bin:${PATH}"}

def _get_rustup_command(self) -> str:
return dedent(
"""\
if ! command -v rustup 2>/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile=minimal
export PATH="${HOME}/.cargo/bin:${PATH}"
fi
"""
)

def _get_install_command(self) -> str:
cmd = [
"cargo",
"install",
"--locked",
"--path",
self.options.rust_path[0],
"--root",
'"${SNAPCRAFT_PART_INSTALL}"',
"--force",
def _check_system_rust(self) -> bool:
"""Check if Rust is installed on the system."""
try:
rust_version = subprocess.check_output(["rustc", "--version"], text=True)
cargo_version = subprocess.check_output(["cargo", "--version"], text=True)
return "rustc" in rust_version and "cargo" in cargo_version
except (subprocess.CalledProcessError, FileNotFoundError):
return False

def _check_rustup(self) -> bool:
try:
rustup_version = subprocess.check_output(["rustup", "--version"])
return "rustup" in rustup_version.decode("utf-8")
except (subprocess.CalledProcessError, FileNotFoundError):
return False

def _get_setup_rustup(self, channel: str) -> List[str]:
return [
f"""\
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --no-modify-path --profile=minimal --default-toolchain {channel}
"""
]

if self.options.rust_features:
cmd.extend(
["--features", "'{}'".format(" ".join(self.options.rust_features))]
)

return " ".join(cmd)
def _get_install_commands(self) -> List[str]:
"""Return a list of commands to run during the pull step."""
options = self.options
if not options.rust_channel and self._check_system_rust():
logger.info("Rust is installed on the system, skipping rustup")
return []

rust_channel = options.rust_channel or "stable"
if rust_channel == "none":
return []
if not self._check_rustup():
logger.info("Rustup not found, installing it")
return self._get_setup_rustup(rust_channel)
logger.info("Switch rustup channel to %s", rust_channel)
return [
f"rustup update {rust_channel}",
f"rustup default {rust_channel}",
]

def get_build_commands(self) -> List[str]:
return [self._get_rustup_command(), self._get_install_command()]
options = self.options

rust_build_cmd: List[str] = []
config_cmd: List[str] = []

if options.rust_features:
features_string = " ".join(options.rust_features)
config_cmd.extend(["--features", f"'{features_string}'"])

if options.rust_use_global_lto:
logger.info("Adding overrides for LTO support")
config_cmd.extend(
[
"--config 'profile.release.lto = true'",
"--config 'profile.release.codegen-units = 1'",
]
)

if options.rust_no_default_features:
config_cmd.append("--no-default-features")

for crate in options.rust_path:
logger.info("Generating build commands for %s", crate)
config_cmd_string = " ".join(config_cmd)
# TODO(liushuyu): the current fallback installation method will also install
# compiler plugins into the final Snap, which is likely not what we want.
# pylint: disable=line-too-long
rust_build_cmd_single = dedent(
f"""\
if cargo read-manifest --manifest-path "{crate}"/Cargo.toml > /dev/null; then
cargo install -f --locked --path "{crate}" --root "${{SNAPCRAFT_PART_INSTALL}}" {config_cmd_string}
# remove the installation metadata
rm -f "${{SNAPCRAFT_PART_INSTALL}}"/.crates{{.toml,2.json}}
else
# virtual workspace is a bit tricky,
# we need to build the whole workspace and then copy the binaries ourselves
pushd "{crate}"
cargo build --workspace --release {config_cmd_string}
# install the final binaries
# TODO(liushuyu): this will also install proc macros in the workspace,
# which the user may not want to keep in the final installation
find ./target/release -maxdepth 1 -executable -exec install -Dvm755 {{}} "${{SNAPCRAFT_PART_INSTALL}}" ';'
popd
fi\
"""
)
rust_build_cmd.append(rust_build_cmd_single)
return self._get_install_commands() + rust_build_cmd
82 changes: 65 additions & 17 deletions tests/legacy/unit/plugins/v2/test_rust.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,33 +31,47 @@ def test_schema(self):
Equals(
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"additionalProperties": False,
"properties": {
"rust-features": {
"default": [],
"items": {"type": "string"},
"rust-path": {
"type": "array",
"minItems": 1,
"uniqueItems": True,
},
"rust-path": {
"default": ["."],
"items": {"type": "string"},
"maxItems": 1,
"minItems": 1,
"default": ["."],
},
"rust-features": {
"type": "array",
"uniqueItems": True,
"items": {"type": "string"},
"default": [],
},
"rust-channel": {
"type": ["string", "null"],
"default": None,
},
"rust-use-global-lto": {
"type": "boolean",
"default": False,
},
"rust-no-default-features": {
"type": "boolean",
"default": False,
},
},
"required": ["source"],
"type": "object",
}
),
)

def test_get_build_packages(self):
plugin = RustPlugin(part_name="my-part", options=lambda: None)

self.assertThat(plugin.get_build_packages(), Equals({"curl", "gcc", "git"}))
self.assertThat(
plugin.get_build_packages(),
Equals({"curl", "gcc", "git", "pkg-config", "findutils"}),
)

def test_get_build_environment(self):
plugin = RustPlugin(part_name="my-part", options=lambda: None)
Expand All @@ -81,13 +95,29 @@ class Options:
[
dedent(
"""\
if ! command -v rustup 2>/dev/null; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --profile=minimal
export PATH="${HOME}/.cargo/bin:${PATH}"
fi
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --no-modify-path --profile=minimal --default-toolchain stable
"""
),
'cargo install --locked --path . --root "${SNAPCRAFT_PART_INSTALL}" --force',
dedent(
"""\
if cargo read-manifest --manifest-path "."/Cargo.toml > /dev/null; then
cargo install -f --locked --path "." --root "${SNAPCRAFT_PART_INSTALL}"
# remove the installation metadata
rm -f "${SNAPCRAFT_PART_INSTALL}"/.crates{.toml,2.json}
else
# virtual workspace is a bit tricky,
# we need to build the whole workspace and then copy the binaries ourselves
pushd "."
cargo build --workspace --release
# install the final binaries
# TODO(liushuyu): this will also install proc macros in the workspace,
# which the user may not want to keep in the final installation
find ./target/release -maxdepth 1 -executable -exec install -Dvm755 {} "${SNAPCRAFT_PART_INSTALL}" ';'
popd
fi\
"""
),
]
),
)
Expand All @@ -101,8 +131,26 @@ class Options:
plugin = RustPlugin(part_name="my-part", options=Options())

self.assertThat(
plugin._get_install_command(),
plugin.get_build_commands()[1],
Equals(
"cargo install --locked --path path --root \"${SNAPCRAFT_PART_INSTALL}\" --force --features 'my-feature your-feature'"
dedent(
"""\
if cargo read-manifest --manifest-path "."/Cargo.toml > /dev/null; then
cargo install -f --locked --path "." --root "${SNAPCRAFT_PART_INSTALL}" --features 'my-feature' 'your-feature'
# remove the installation metadata
rm -f "${SNAPCRAFT_PART_INSTALL}"/.crates{.toml,2.json}
else
# virtual workspace is a bit tricky,
# we need to build the whole workspace and then copy the binaries ourselves
pushd "."
cargo build --workspace --release --features 'my-feature' 'your-feature'
# install the final binaries
# TODO(liushuyu): this will also install proc macros in the workspace,
# which the user may not want to keep in the final installation
find ./target/release -maxdepth 1 -executable -exec install -Dvm755 {} "${SNAPCRAFT_PART_INSTALL}" ';'
popd
fi\
"""
)
),
)

0 comments on commit ffe64d5

Please sign in to comment.