diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d552a4..e3562af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,7 @@ jobs: "node", "playwright-deps", "python", + "rust", "sonar-scanner-cli", "system-packages", "timezone", diff --git a/README.md b/README.md index 32ce2a0..66de47e 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ Below is a list with included features, click on the link for more details. | [node](./features/src/node/README.md) | Installs Node.js. | | [playwright-deps](./features/src/playwright-deps/README.md) | Installs all dependencies required to run Playwright. | | [python](./features/src/python/README.md) | Installs Python. | +| [rust](./features/src/rust/README.md) | A package which installs Rust, common Rust utilities and their required dependencies. | | [sonar-scanner-cli](./features/src/sonar-scanner-cli/README.md) | Installs the SonarScanner CLI. | | [system-packages](./features/src/system-packages/README.md) | Install arbitrary system packages using the system package manager. | | [timezone](./features/src/timezone/README.md) | Allows setting the timezone. | diff --git a/build/build.go b/build/build.go index 4dc885b..c42217b 100644 --- a/build/build.go +++ b/build/build.go @@ -180,6 +180,11 @@ func init() { gotaskr.Task("Feature:python:Test", func() error { return testFeature("python") }) gotaskr.Task("Feature:python:Publish", func() error { return publishFeature("python") }) + ////////// rust + gotaskr.Task("Feature:rust:Package", func() error { return packageFeature("rust") }) + gotaskr.Task("Feature:rust:Test", func() error { return testFeature("rust") }) + gotaskr.Task("Feature:rust:Publish", func() error { return publishFeature("rust") }) + ////////// sonar-scanner-cli gotaskr.Task("Feature:sonar-scanner-cli:Package", func() error { return packageFeature("sonar-scanner-cli") }) gotaskr.Task("Feature:sonar-scanner-cli:Test", func() error { return testFeature("sonar-scanner-cli") }) diff --git a/features/src/rust/NOTES.md b/features/src/rust/NOTES.md new file mode 100644 index 0000000..b0ad17d --- /dev/null +++ b/features/src/rust/NOTES.md @@ -0,0 +1,10 @@ +## Notes + +### System Compatibility + +Debian, Ubuntu + +### Accessed Urls + +Needs access to the following URL for downloading and resolving: +* https://static.rust-lang.org diff --git a/features/src/rust/README.md b/features/src/rust/README.md new file mode 100755 index 0000000..1a52763 --- /dev/null +++ b/features/src/rust/README.md @@ -0,0 +1,47 @@ +# Rust (rust) + +A package which installs Rust, common Rust utilities and their required dependencies. + +## Example Usage + +```json +"features": { + "ghcr.io/postfinance/devcontainer-features/rust:0.1.0": { + "version": "latest", + "rustupVersion": "latest", + "profile": "minimal", + "components": "rustfmt,rust-analyzer,rust-src,clippy", + "enableWindowsTarget": false + } +} +``` + +## Options + +| Option | Description | Type | Default Value | Proposals | +|-----|-----|-----|-----|-----| +| version | The version of Rust to install. | string | latest | latest, 1.93.0 | +| rustupVersion | The version of rustup to install. | string | latest | latest, 1.27.1 | +| profile | The rustup profile to install. | string | minimal | minimal, default, complete | +| components | A comma separated list with components that should be installed. | string | rustfmt,rust-analyzer,rust-src,clippy | , rustfmt,rust-analyzer, rls,rust-analysis | +| enableWindowsTarget | A flag to indicate if the Windows target (and needed tools) should be installed. | boolean | false | true, false | + +## Customizations + +### VS Code Extensions + +- `vadimcn.vscode-lldb` +- `rust-lang.rust-analyzer` +- `tamasfe.even-better-toml` +- `serayuzgur.crates` + +## Notes + +### System Compatibility + +Debian, Ubuntu + +### Accessed Urls + +Needs access to the following URL for downloading and resolving: +* https://static.rust-lang.org diff --git a/features/src/rust/devcontainer-feature.json b/features/src/rust/devcontainer-feature.json new file mode 100644 index 0000000..dae2d9a --- /dev/null +++ b/features/src/rust/devcontainer-feature.json @@ -0,0 +1,66 @@ +{ + "id": "rust", + "version": "0.1.0", + "name": "Rust", + "description": "A package which installs Rust, common Rust utilities and their required dependencies.", + "options": { + "version": { + "type": "string", + "proposals": [ + "latest", + "1.93.0" + ], + "default": "latest", + "description": "The version of Rust to install." + }, + "rustupVersion": { + "type": "string", + "proposals": [ + "latest", + "1.27.1" + ], + "default": "latest", + "description": "The version of rustup to install." + }, + "profile": { + "type": "string", + "proposals": [ + "minimal", + "default", + "complete" + ], + "default": "minimal", + "description": "The rustup profile to install." + }, + "components": { + "type": "string", + "proposals": [ + "", + "rustfmt,rust-analyzer", + "rls,rust-analysis" + ], + "default": "rustfmt,rust-analyzer,rust-src,clippy", + "description": "A comma separated list with components that should be installed." + }, + "enableWindowsTarget": { + "type": "boolean", + "default": false, + "description": "A flag to indicate if the Windows target (and needed tools) should be installed." + } + }, + "customizations": { + "vscode": { + "extensions": [ + "vadimcn.vscode-lldb", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "serayuzgur.crates" + ] + } + }, + "containerEnv": { + "CARGO_HOME": "/usr/local/cargo", + "RUSTUP_HOME": "/usr/local/rustup", + "PATH": "/usr/local/cargo/bin:${PATH}" + } +} \ No newline at end of file diff --git a/features/src/rust/install.sh b/features/src/rust/install.sh new file mode 100755 index 0000000..1df7c09 --- /dev/null +++ b/features/src/rust/install.sh @@ -0,0 +1,8 @@ +. ./functions.sh + +"./installer_$(detect_arch)" \ + -version="${VERSION:-"latest"}" \ + -rustupVersion="${RUSTUPVERSION:-"latest"}" \ + -profile="${PROFILE:-"minimal"}" \ + -components="${COMPONENTS:-"rustfmt,rust-analyzer,rust-src,clippy"}" \ + -enableWindowsTarget="${ENABLEWINDOWSTARGET:-"false"}" diff --git a/features/src/rust/installer.go b/features/src/rust/installer.go new file mode 100644 index 0000000..109c497 --- /dev/null +++ b/features/src/rust/installer.go @@ -0,0 +1,183 @@ +package main + +import ( + "builder/installer" + "flag" + "fmt" + "os" + "regexp" + "strings" + + "github.com/roemer/gotaskr/execr" + "github.com/roemer/gover" +) + +////////// +// Configuration +////////// + +// Regex with 2-3 digits like 1.0 or 1.79.0 +var threeDigitRegex *regexp.Regexp = regexp.MustCompile(`^?(\d+)\.(\d+)(?:\.(\d+))?$`) + +// Full Regex versioning, like 1.0.0-alpha.2 +var semVerRegex *regexp.Regexp = regexp.MustCompile(`^?(\d+)\.(\d+)(?:\.(\d+))?(?:-([a-z]+)(?:\.?(\d+))?)?$`) + +////////// +// Main +////////// + +func main() { + if err := runMain(); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } +} + +func runMain() error { + // Handle the flags + version := flag.String("version", "lts", "") + rustupVersion := flag.String("rustupVersion", "latest", "") + profile := flag.String("profile", "minimal", "") + components := flag.String("components", "rustfmt,rust-analyzer,rust-src,clippy", "") + enableWindowsTarget := flag.Bool("enableWindowsTarget", false, "") + flag.Parse() + + // Create and process the feature + feature := installer.NewFeature("Rust", true, + &rustupComponent{ + ComponentBase: installer.NewComponentBase("rustup", *rustupVersion), + profile: *profile, + }, + &rustComponent{ + ComponentBase: installer.NewComponentBase("rust", *version), + components: *components, + profile: *profile, + }, + &buildEssentialComponent{ + ComponentBase: installer.NewComponentBase("build-essential", installer.VERSION_SYSTEM_DEFAULT), + }, + ) + // Optional component + if *enableWindowsTarget { + feature.AddComponents(&windowsTargetComponent{ + ComponentBase: installer.NewComponentBase("windows-target", installer.VERSION_IRRELEVANT), + }) + } + // Last component + feature.AddComponents(&permissionsComponent{ + ComponentBase: installer.NewComponentBase("permissions", installer.VERSION_IRRELEVANT), + }) + return feature.Process() +} + +////////// +// Implementation +////////// + +type rustupComponent struct { + *installer.ComponentBase + profile string +} + +func (c *rustupComponent) GetAllVersions() ([]*gover.Version, error) { + allTags, err := installer.Tools.GitHub.GetTags("rust-lang", "rustup") + if err != nil { + return nil, err + } + return installer.Tools.Versioning.ParseVersionsFromList(allTags, threeDigitRegex, true) +} + +func (c *rustupComponent) InstallVersion(version *gover.Version) error { + // Download the file + archPart, err := installer.Tools.System.MapArchitecture(map[string]string{ + installer.AMD64: "x86_64", + installer.ARM64: "aarch64", + }) + if err != nil { + return err + } + fileName := "rustup-init" + downloadUrl := fmt.Sprintf("https://static.rust-lang.org/rustup/archive/%s/%s-unknown-linux-gnu/rustup-init", version.Raw, archPart) + if err := installer.Tools.Download.ToFile(downloadUrl, fileName, "Rustup-Init"); err != nil { + return err + } + // Install it + if err := os.Chmod(fileName, os.ModePerm); err != nil { + return err + } + if err := execr.Run(true, "./"+fileName, "-y", "--default-toolchain", "none", "--no-modify-path", "--profile", c.profile); err != nil { + return err + } + // Cleanup + if err := os.Remove(fileName); err != nil { + return err + } + return nil +} + +type rustComponent struct { + *installer.ComponentBase + profile string + components string +} + +func (c *rustComponent) GetAllVersions() ([]*gover.Version, error) { + allTags, err := installer.Tools.GitHub.GetTags("rust-lang", "rust") + if err != nil { + return nil, err + } + return installer.Tools.Versioning.ParseVersionsFromList(allTags, semVerRegex, true) +} + +func (c *rustComponent) InstallVersion(version *gover.Version) error { + // Install it + if err := execr.Run(true, "rustup", "toolchain", "install", "--profile", c.profile, "--no-self-update", version.Raw); err != nil { + return err + } + // Installing the components + fmt.Printf("Installing components: %s\n", c.components) + args := []string{ + "component", + "add", + } + for _, component := range strings.Split(c.components, ",") { + trimmed := strings.TrimSpace(component) + if trimmed != "" { + args = append(args, trimmed) + } + } + if err := execr.Run(true, "rustup", args...); err != nil { + return err + } + return nil +} + +type buildEssentialComponent struct { + *installer.ComponentBase +} + +func (c *buildEssentialComponent) InstallVersion(version *gover.Version) error { + return installer.Tools.System.InstallPackages("build-essential") +} + +type windowsTargetComponent struct { + *installer.ComponentBase +} + +func (c *windowsTargetComponent) InstallVersion(version *gover.Version) error { + if err := execr.Run(true, "rustup", "target", "add", "x86_64-pc-windows-gnu"); err != nil { + return err + } + if err := installer.Tools.System.InstallPackages("mingw-w64"); err != nil { + return err + } + return nil +} + +type permissionsComponent struct { + *installer.ComponentBase +} + +func (c *permissionsComponent) InstallVersion(version *gover.Version) error { + return execr.Run(true, "chmod", "-R", "777", os.Getenv("RUSTUP_HOME"), os.Getenv("CARGO_HOME")) +} diff --git a/features/test/rust/install-with-windows-target.sh b/features/test/rust/install-with-windows-target.sh new file mode 100755 index 0000000..7f2bd71 --- /dev/null +++ b/features/test/rust/install-with-windows-target.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_version "$(rustup --version 2>/dev/null)" "rustup 1.27.1 (54dd3d00f 2024-04-24)" +check_version "$(rustc --version)" "rustc 1.76.0 (07dca489a 2024-02-04)" +check_version "$(rustup target list --installed)" $'x86_64-pc-windows-gnu\nx86_64-unknown-linux-gnu' diff --git a/features/test/rust/install-without-windows-target.sh b/features/test/rust/install-without-windows-target.sh new file mode 100755 index 0000000..199e041 --- /dev/null +++ b/features/test/rust/install-without-windows-target.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_version "$(rustup --version 2>/dev/null)" "rustup 1.27.1 (54dd3d00f 2024-04-24)" +check_version "$(rustc --version)" "rustc 1.76.0 (07dca489a 2024-02-04)" +check_version "$(rustup target list --installed)" $'x86_64-unknown-linux-gnu' diff --git a/features/test/rust/scenarios.json b/features/test/rust/scenarios.json new file mode 100644 index 0000000..1eadbe4 --- /dev/null +++ b/features/test/rust/scenarios.json @@ -0,0 +1,36 @@ +{ + "install-with-windows-target": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./rust": { + "version": "1.76", + "rustupVersion": "1.27.1", + "profile": "minimal", + "components": "rustfmt,rust-analyzer,rust-src,clippy", + "enableWindowsTarget": "true" + } + } + }, + "install-without-windows-target": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "./rust": { + "version": "1.76", + "rustupVersion": "1.27.1", + "profile": "minimal", + "components": "rustfmt,rust-analyzer,rust-src,clippy", + "enableWindowsTarget": "false" + } + } + } +} \ No newline at end of file diff --git a/features/test/rust/test-images.json b/features/test/rust/test-images.json new file mode 100644 index 0000000..e1b870e --- /dev/null +++ b/features/test/rust/test-images.json @@ -0,0 +1,6 @@ +[ + "mcr.microsoft.com/devcontainers/base:debian-11", + "mcr.microsoft.com/devcontainers/base:debian-12", + "mcr.microsoft.com/devcontainers/base:debian-13", + "mcr.microsoft.com/devcontainers/base:ubuntu-24.04" +] \ No newline at end of file