From b357ef5e737348a66ae98cf5842a5de39b41c2da Mon Sep 17 00:00:00 2001 From: liushuyu Date: Wed, 20 Sep 2023 18:03:15 -0600 Subject: [PATCH] feat(plugins): add rustup support and more config options (#4297) Add rustup support, ability to enable `no-default-features`, install virtual workspace crates, and enable LTO. Co-authored-by: Claudio Matsuoka Co-authored-by: Zixing Liu --- snapcraft_legacy/plugins/v2/rust.py | 164 +++++++++++++++++----- tests/legacy/unit/plugins/v2/test_rust.py | 86 +++++++++--- 2 files changed, 193 insertions(+), 57 deletions(-) diff --git a/snapcraft_legacy/plugins/v2/rust.py b/snapcraft_legacy/plugins/v2/rust.py index afb1c17e0e..d4c965e268 100644 --- a/snapcraft_legacy/plugins/v2/rust.py +++ b/snapcraft_legacy/plugins/v2/rust.py @@ -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 @@ -14,7 +14,9 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -"""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. @@ -23,22 +25,44 @@ '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 logging +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 @@ -51,8 +75,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": ["."], @@ -63,6 +85,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"], } @@ -71,39 +105,95 @@ 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} +""" + ] + + 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}", ] - if self.options.rust_features: - cmd.extend( - ["--features", "'{}'".format(" ".join(self.options.rust_features))] + def get_build_commands(self) -> List[str]: + 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'", + ] ) - return " ".join(cmd) - - def get_build_commands(self) -> List[str]: - return [self._get_rustup_command(), self._get_install_command()] + 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) + # 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 + 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 diff --git a/tests/legacy/unit/plugins/v2/test_rust.py b/tests/legacy/unit/plugins/v2/test_rust.py index d3ab8832b8..111a114293 100644 --- a/tests/legacy/unit/plugins/v2/test_rust.py +++ b/tests/legacy/unit/plugins/v2/test_rust.py @@ -31,25 +31,36 @@ 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", } ), ) @@ -57,7 +68,10 @@ def test_schema(self): 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) @@ -69,40 +83,72 @@ def test_get_build_environment(self): def test_get_build_commands(self): class Options: - rust_channel = "" + rust_channel = "stable" rust_path = ["."] rust_features = [] + rust_no_default_features = True + rust_use_global_lto = False plugin = RustPlugin(part_name="my-part", options=Options()) + plugin._check_rustup = lambda: True self.assertThat( plugin.get_build_commands(), Equals( [ + "rustup update stable", + "rustup default stable", 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 - """ + if cargo read-manifest --manifest-path "."/Cargo.toml > /dev/null; then + cargo install -f --locked --path "." --root "${SNAPCRAFT_PART_INSTALL}" --no-default-features + # 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 --no-default-features + # install the final binaries + find ./target/release -maxdepth 1 -executable -exec install -Dvm755 {} "${SNAPCRAFT_PART_INSTALL}" ';' + popd + fi""" ), - 'cargo install --locked --path . --root "${SNAPCRAFT_PART_INSTALL}" --force', ] ), ) def test_get_install_command_with_features(self): class Options: - rust_channel = "" + rust_channel = "none" rust_path = ["path"] rust_features = ["my-feature", "your-feature"] + rust_no_default_features = False + rust_use_global_lto = False plugin = RustPlugin(part_name="my-part", options=Options()) + plugin._check_rustup = lambda: False self.assertThat( - plugin._get_install_command(), + plugin.get_build_commands(), Equals( - "cargo install --locked --path path --root \"${SNAPCRAFT_PART_INSTALL}\" --force --features 'my-feature your-feature'" + [ + dedent( + """\ + if cargo read-manifest --manifest-path "path"/Cargo.toml > /dev/null; then + cargo install -f --locked --path "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 "path" + cargo build --workspace --release --features 'my-feature your-feature' + # install the final binaries + find ./target/release -maxdepth 1 -executable -exec install -Dvm755 {} "${SNAPCRAFT_PART_INSTALL}" ';' + popd + fi""" + ) + ] ), )