From 82df3467a6d3be76b5a40a6e1a4166e0e886ca04 Mon Sep 17 00:00:00 2001 From: Max Fierke Date: Mon, 12 Feb 2024 22:53:13 -0600 Subject: [PATCH] Implement a rustup RuntimeManager --- spec/mstrap/configuration_spec.cr | 119 ++++++++++++++++++++++- src/mstrap/runtime_managers/rustup.cr | 83 ++++++++++++++++ src/mstrap/steps/dependencies_step.cr | 17 +++- src/mstrap/supports/ca_cert_installer.cr | 2 +- src/mstrap/supports/rustup_installer.cr | 48 +++++++++ 5 files changed, 264 insertions(+), 5 deletions(-) create mode 100644 src/mstrap/runtime_managers/rustup.cr create mode 100644 src/mstrap/supports/rustup_installer.cr diff --git a/spec/mstrap/configuration_spec.cr b/spec/mstrap/configuration_spec.cr index 58b2f09..e9aef11 100644 --- a/spec/mstrap/configuration_spec.cr +++ b/spec/mstrap/configuration_spec.cr @@ -317,12 +317,12 @@ Spectator.describe MStrap::Configuration do end end - describe "#runtime_manager" do + describe "#default_runtime_manager" do context "for v1.0 configs" do subject { MStrap::Configuration.new(config_def_v1_0) } it "defaults to asdf" do - expect(subject.runtime_manager).to be_a(MStrap::RuntimeManagers::ASDF) + expect(subject.default_runtime_manager).to be_a(MStrap::RuntimeManagers::ASDF) end end @@ -350,7 +350,120 @@ Spectator.describe MStrap::Configuration do subject { MStrap::Configuration.new(config_def_v1_1) } it "can be set through runtimes.default_manager" do - expect(subject.runtime_manager).to be_a(MStrap::RuntimeManagers::Mise) + expect(subject.default_runtime_manager).to be_a(MStrap::RuntimeManagers::Mise) + end + end + end + + describe "#runtime_managers" do + context "for v1.0 configs" do + subject { MStrap::Configuration.new(config_def_v1_0) } + + it "defaults to [asdf]" do + expect(subject.runtime_managers).to eq([MStrap::RuntimeManager.for("asdf")]) + end + end + + context "for v1.1 configs" do + let(config_def_v1_1) do + MStrap::Defs::ConfigDef.from_hcl(<<-HCL) + version = "1.1" + + runtimes { + default_manager = "mise" + + runtime "rust" { + manager = "rustup" + } + } + + user { + name = "Reginald Testington" + email = "reginald@testington.biz" + } + + profile "personal" { + url = "ssh://git@gitprovider.biz/reggiemctest/mstrap-personal.git" + } + + HCL + end + + subject { MStrap::Configuration.new(config_def_v1_1) } + + it "it derived through runtimes.default_manager and runtimes.runtime[*].manager" do + expect(subject.runtime_managers).to eq([ + MStrap::RuntimeManager.for("mise"), + MStrap::RuntimeManager.for("rustup"), + ]) + end + end + end + + describe "#runtimes" do + context "for v1.0 configs" do + subject { MStrap::Configuration.new(config_def_v1_0) } + + it "defaults all to ASDF-provided runtimes" do + %w(crystal go node php python ruby rust).each do |language_name| + expect(subject.runtimes[language_name].runtime_manager).to be_a(MStrap::RuntimeManagers::ASDF) + end + end + + it "returns the expected language runtimes" do + expect(subject.runtimes["crystal"]).to be_a(MStrap::Runtimes::Crystal) + expect(subject.runtimes["go"]).to be_a(MStrap::Runtimes::Go) + expect(subject.runtimes["node"]).to be_a(MStrap::Runtimes::Node) + expect(subject.runtimes["php"]).to be_a(MStrap::Runtimes::Php) + expect(subject.runtimes["python"]).to be_a(MStrap::Runtimes::Python) + expect(subject.runtimes["ruby"]).to be_a(MStrap::Runtimes::Ruby) + expect(subject.runtimes["rust"]).to be_a(MStrap::Runtimes::Rust) + end + end + + context "for v1.1 configs" do + let(config_def_v1_1) do + MStrap::Defs::ConfigDef.from_hcl(<<-HCL) + version = "1.1" + + runtimes { + default_manager = "mise" + + runtime "rust" { + manager = "rustup" + } + } + + user { + name = "Reginald Testington" + email = "reginald@testington.biz" + } + + profile "personal" { + url = "ssh://git@gitprovider.biz/reggiemctest/mstrap-personal.git" + } + + HCL + end + + subject { MStrap::Configuration.new(config_def_v1_1) } + + it "allows overriding language manager for specific languages" do + %w(crystal go node php python ruby).each do |language_name| + expect(subject.runtimes[language_name].runtime_manager).to be_a(MStrap::RuntimeManagers::Mise) + end + + expect(subject.runtimes["rust"].runtime_manager).to be_a(MStrap::RuntimeManagers::Rustup) + end + + it "returns the expected language runtimes" do + expect(subject.runtimes["crystal"]).to be_a(MStrap::Runtimes::Crystal) + expect(subject.runtimes["go"]).to be_a(MStrap::Runtimes::Go) + expect(subject.runtimes["node"]).to be_a(MStrap::Runtimes::Node) + expect(subject.runtimes["php"]).to be_a(MStrap::Runtimes::Php) + expect(subject.runtimes["python"]).to be_a(MStrap::Runtimes::Python) + expect(subject.runtimes["ruby"]).to be_a(MStrap::Runtimes::Ruby) + expect(subject.runtimes["rust"]).to be_a(MStrap::Runtimes::Rust) end end end diff --git a/src/mstrap/runtime_managers/rustup.cr b/src/mstrap/runtime_managers/rustup.cr new file mode 100644 index 0000000..2f818c7 --- /dev/null +++ b/src/mstrap/runtime_managers/rustup.cr @@ -0,0 +1,83 @@ +module MStrap + module RuntimeManagers + class Rustup < RuntimeManager + def current_version(language_name : String) : String? + current_toolchain = `rustup show active-toolchain`.chomp + extract_rust_version_from_toolchain(current_toolchain) + end + + # Execute a command using a specific language runtime version + def runtime_exec(language_name : String, command : String, args : Array(String)? = nil, runtime_version : String? = nil) + exec_args = [] of String + exec_args << runtime_version if runtime_version + + cmd_args = ["run"] + exec_args + ["--", command] + cmd_args += args if args + + if command && (!args || args.empty?) + cmd "rustup #{cmd_args.join(' ')}", quiet: true + else + cmd "rustup", cmd_args, quiet: true + end + end + + # Returns whether the mise plugin has been installed for a language runtime + # or not + def has_plugin?(language_name : String) : Bool + language_name == "rust" + end + + def install_plugin(language_name : String) : Bool + language_name == "rust" + end + + def install_version(language_name : String, version : String) : Bool + cmd("rustup toolchain install #{version}", quiet: true) + end + + # Returns a list of the versions of the language runtime installed + # by mise. + def installed_versions(language_name : String) : Array(String) + rust_versions_list = `rustup toolchain list`.chomp.split("\n").map do |version| + extract_rust_version_from_toolchain(version) + end + + rust_versions_list + end + + # Rustup doesn't support querying this, but "stable" is _probably_ safe + def latest_version(language_name : String) : String + "stable" + end + + # Rust is the only language managed by rustup, so this is always "rust"! + def plugin_name(language_name : String) : String? + "rust" + end + + def set_version(language_name : String, version : String?) : Bool + cmd "rustup override set #{version}", quiet: true + end + + def set_global_version(language_name, version : String) : Bool + cmd "rustup default #{version}", quiet: true + end + + def shell_activation(shell_name : String) : String + <<-SHELL + # Activate rustup for Rust compiler version management + export PATH="$HOME/.cargo/bin:$PATH" + SHELL + end + + def supported_languages : Array(String) + %w(rust) + end + + private def extract_rust_version_from_toolchain(rustup_toolchain : String) + version, _, _ = rustup_toolchain.partition('-') + version + end + end + end +end diff --git a/src/mstrap/steps/dependencies_step.cr b/src/mstrap/steps/dependencies_step.cr index 47def93..47de22d 100644 --- a/src/mstrap/steps/dependencies_step.cr +++ b/src/mstrap/steps/dependencies_step.cr @@ -16,7 +16,8 @@ module MStrap end def bootstrap - install_mise if config.default_runtime_manager.name == "mise" + install_mise if runtime_managers.any? { |rm| rm.name == "mise" } + install_rustup if runtime_managers.any? { |rm| rm.name == "rustup" } set_strap_env! strap_sh load_profile! @@ -68,6 +69,20 @@ module MStrap end end + def install_rustup + rustup_installer = RustupInstaller.new + + log "==> Checking for rustup: " + if rustup_installer.installed? && !options.force? + success "OK" + else + logn "Not installed".colorize(:yellow) + log "--> Installing rustup for language runtime version management: " + rustup_installer.install! + success "OK" + end + end + private def load_profile! log "--> Reloading profile: " config.reload! diff --git a/src/mstrap/supports/ca_cert_installer.cr b/src/mstrap/supports/ca_cert_installer.cr index e56981b..2dc44b6 100644 --- a/src/mstrap/supports/ca_cert_installer.cr +++ b/src/mstrap/supports/ca_cert_installer.cr @@ -10,7 +10,7 @@ module MStrap FileUtils.mkdir_p(Paths::RC_DIR, 0o755) Dir.cd(Paths::RC_DIR) do unless cmd("curl -L --silent --remote-name --time-cond cacert.pem https://curl.se/ca/cacert.pem") - logc "There was an error fetching the cURL CA Cert bundle, which is needed to verify HTTPS certificates. mstrap cannot continue." + logc "There was an error fetching the cURL CA Cert bundle, which is needed to verify HTTPS certificates. mstrap cannot continue. Please ensure that `curl` is installed." end File.chmod(Paths::CA_CERT_BUNDLE, 0o600) end diff --git a/src/mstrap/supports/rustup_installer.cr b/src/mstrap/supports/rustup_installer.cr new file mode 100644 index 0000000..478d365 --- /dev/null +++ b/src/mstrap/supports/rustup_installer.cr @@ -0,0 +1,48 @@ +module MStrap + # Manages the install of rustup for managing Rust + # + # NOTE: See `MStrap::RuntimeManagers::Rustup` for how rustup is integrated + class RustupInstaller + include DSL + + # :nodoc: + RUSTUP_INIT_SH_PATH = File.join(MStrap::Paths::RC_DIR, "vendor", "rustup-init.sh") + + # :nodoc: + RUSTUP_INIT_SH_URL = "https://sh.rustup.rs" + + # :nodoc: + RUSTUP_BIN_DIR_PATH = File.join(ENV["HOME"], ".cargo", "bin") + + def install! + fetch_installer! + + install_args = [RUSTUP_INIT_SH_PATH, "--no-modify-path"] + + if MStrap.debug? + install_args << "--verbose" + else + install_args << "--quiet" + install_args << "-y" + end + + unless cmd "sh", install_args + logc "Failed to install rustup" + end + + # "Activate" it + path = ENV["PATH"] + ENV["PATH"] = "#{RUSTUP_BIN_DIR_PATH}:#{path}" + end + + def installed? + `command -v rustup` && $?.success? + end + + private def fetch_installer! + HTTP::Client.get(RUSTUP_INIT_SH_URL, tls: MStrap.tls_client) do |response| + File.write(RUSTUP_INIT_SH_PATH, response.body_io.gets_to_end) + end + end + end +end