diff --git a/CHANGELOG.md b/CHANGELOG.md index 7297b69..2e488d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Support for using [mise](https://mise.jdx.dev) to manage language runtime versions (#50). `config.hcl` can be configured as such to enable it: + ``` + version = "1.1" + + # ... + + runtimes { + default_manager = "mise" + } + ``` + + To switch, you'll need to re-run `mstrap` and restart your terminal windows. + Then, you can run `brew uninstall asdf --force` to uninstall asdf (`mstrap` + will have removed `asdf`'s activation from mstrap's `env.sh` already) + ### Changed ### Bugfixes diff --git a/meson.build b/meson.build index 3e204a6..e4a7556 100644 --- a/meson.build +++ b/meson.build @@ -2,7 +2,7 @@ project('mstrap', 'c', meson_version : '>= 0.60.0', license : 'MIT', - version : '0.6.0', + version : '0.7.0.dev', default_options : [ 'buildtype=debugoptimized', 'default_library=static' diff --git a/shard.yml b/shard.yml index fe591da..343b8c2 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: mstrap -version: 0.6.0 +version: 0.7.0.dev authors: - Max Fierke diff --git a/spec/mstrap/configuration_spec.cr b/spec/mstrap/configuration_spec.cr index 2bebec7..58b2f09 100644 --- a/spec/mstrap/configuration_spec.cr +++ b/spec/mstrap/configuration_spec.cr @@ -34,7 +34,7 @@ def delete_profile(name) end Spectator.describe MStrap::Configuration do - let(config_def) do + let(config_def_v1_0) do MStrap::Defs::ConfigDef.from_hcl(<<-HCL) version = "1.0" @@ -50,6 +50,8 @@ Spectator.describe MStrap::Configuration do HCL end + let(config_def) { config_def_v1_0 } + let(personal_profile_def) do MStrap::Defs::ProfileDef.from_hcl(<<-HCL) version = "1.0" @@ -315,6 +317,44 @@ Spectator.describe MStrap::Configuration do end end + describe "#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) + 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" + } + + 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 "can be set through runtimes.default_manager" do + expect(subject.runtime_manager).to be_a(MStrap::RuntimeManagers::Mise) + end + end + end + describe "#save!" do it "saves the loaded configuration back to disk" do subject = MStrap::Configuration.new(config_def) diff --git a/src/mstrap.cr b/src/mstrap.cr index 8f8bb1d..2b256e8 100644 --- a/src/mstrap.cr +++ b/src/mstrap.cr @@ -26,6 +26,8 @@ require "./mstrap/user" require "./mstrap/configuration" require "./mstrap/supports/**" require "./mstrap/web_bootstrapper" +require "./mstrap/runtime_manager" +require "./mstrap/runtime_managers/**" require "./mstrap/runtime" require "./mstrap/runtimes/**" require "./mstrap/project" diff --git a/src/mstrap/cli.cr b/src/mstrap/cli.cr index b2ff32c..7430e13 100644 --- a/src/mstrap/cli.cr +++ b/src/mstrap/cli.cr @@ -200,7 +200,7 @@ DESC project_def.runtimes = options.string["runtimes"].split(',') if options.string.has_key?("runtimes") project = MStrap::Project.for(project_def) - project.bootstrap + project.bootstrap(config.runtime_manager) end end @@ -235,6 +235,8 @@ DESC Signal::TERM.trap { exit 1 } Commander.run(cli, options.argv) + rescue e : MStrapError + logc e.message rescue e {% if flag?(:debug) %} raise e diff --git a/src/mstrap/configuration.cr b/src/mstrap/configuration.cr index a93e7a8..b5e276b 100644 --- a/src/mstrap/configuration.cr +++ b/src/mstrap/configuration.cr @@ -7,6 +7,7 @@ module MStrap @loaded_profiles : Array(Defs::ProfileDef) @known_profile_configs : Array(Defs::ProfileConfigDef) @resolved_profile : Defs::ProfileDef + @runtime_manager : RuntimeManager @user : User DEFAULT_PROFILE_CONFIG_DEF = Defs::DefaultProfileConfigDef.new @@ -27,6 +28,9 @@ module MStrap # profiles with the default profiles. getter :resolved_profile + # Returns the runtime manager specified by the configuration + getter :runtime_manager + # Returns the mstrap user getter :user @@ -40,6 +44,7 @@ module MStrap @loaded_profiles = [] of Defs::ProfileDef @known_profile_configs = config.profiles + [DEFAULT_PROFILE_CONFIG_DEF] @resolved_profile = Defs::ProfileDef.new + @runtime_manager = RuntimeManager.for(config.runtimes.default_manager) @user = User.new(user: config.user) end diff --git a/src/mstrap/defs/config_def.cr b/src/mstrap/defs/config_def.cr index 5411ad1..e473808 100644 --- a/src/mstrap/defs/config_def.cr +++ b/src/mstrap/defs/config_def.cr @@ -4,15 +4,18 @@ module MStrap include HCL::Serializable @[HCL::Attribute] - property version = "1.0" + property version = "1.1" @[HCL::Block(key: "profile")] property profiles = [] of ::MStrap::Defs::ProfileConfigDef + @[HCL::Block] + property runtimes = ::MStrap::Defs::RuntimesConfigDef.new + @[HCL::Block] property user = ::MStrap::Defs::UserDef.new - def_equals_and_hash @version, @profiles, @user + def_equals_and_hash @version, @profiles, @runtimes, @user def self.from_url(url : String) HTTP::Client.get(url, tls: MStrap.tls_client) do |response| @@ -20,7 +23,11 @@ module MStrap end end - def initialize(@user = UserDef.new, @profiles = Array(ProfileConfigDef).new) + def initialize( + @user = UserDef.new, + @profiles = Array(ProfileConfigDef).new, + @runtimes = RuntimesConfigDef.new + ) end end end diff --git a/src/mstrap/defs/runtimes_config_def.cr b/src/mstrap/defs/runtimes_config_def.cr new file mode 100644 index 0000000..0df0c6e --- /dev/null +++ b/src/mstrap/defs/runtimes_config_def.cr @@ -0,0 +1,15 @@ +module MStrap + module Defs + class RuntimesConfigDef + include HCL::Serializable + + @[HCL::Attribute] + property default_manager = "asdf" + + def_equals_and_hash @default_manager + + def initialize + end + end + end +end diff --git a/src/mstrap/errors.cr b/src/mstrap/errors.cr index 80d927f..97c463a 100644 --- a/src/mstrap/errors.cr +++ b/src/mstrap/errors.cr @@ -32,6 +32,12 @@ module MStrap end end + class InvalidRuntimeManagerError < MStrapError + def initialize(manager_name) + super("Runtime manager is not recognized or supported by mstrap: #{manager_name}") + end + end + # Exception class to indicate a failure involving language runtime setup class RuntimeSetupError < MStrapError def initialize(language_name, message) diff --git a/src/mstrap/project.cr b/src/mstrap/project.cr index 95ac134..5f11b66 100644 --- a/src/mstrap/project.cr +++ b/src/mstrap/project.cr @@ -168,7 +168,7 @@ module MStrap # Executes `script/bootstrap` and `script/setup` (if either exists and are # configured to run) or executes conventional runtime bootstrapping as # determined by mstrap. - def bootstrap + def bootstrap(runtime_manager : RuntimeManager) if has_scripts? && run_scripts? logd "Found bootstrapping scripts, executing instead of using defaults." begin @@ -183,7 +183,7 @@ module MStrap end else logd "Bootstrapping '#{name}' with runtime defaults." - default_bootstrap + default_bootstrap(runtime_manager) end end @@ -192,12 +192,14 @@ module MStrap # This **does not** run any bootstrapping scripts, and is used mainly for # calling into conventional bootstrapping within a project's # `script/bootstrap` or `script/setup` from `mstrap project`. - protected def default_bootstrap + # + # TODO: Move this somewhere more appropriate + protected def default_bootstrap(runtime_manager : RuntimeManager) runtime_impls = if runtimes.empty? - MStrap::Runtime.all + runtime_manager.runtimes else - MStrap::Runtime.all.select do |runtime| + runtime_manager.runtimes.select do |runtime| runtimes.includes?(runtime.language_name) end end diff --git a/src/mstrap/runtime.cr b/src/mstrap/runtime.cr index 69e58a3..596286b 100644 --- a/src/mstrap/runtime.cr +++ b/src/mstrap/runtime.cr @@ -3,29 +3,14 @@ module MStrap abstract class Runtime include DSL - @version_env_var : String? + getter :runtime_manager - # Execute a command using a specific language runtime version - def asdf_exec(command : String, args : Array(String), runtime_version : String? = nil) - if runtime_version - env = {version_env_var => runtime_version} - cmd env, command, args, quiet: true - else - cmd command, args, quiet: true - end + def initialize(@runtime_manager : RuntimeManager) end - def asdf_install_plugin - log "--> Adding #{asdf_plugin_name} to asdf for #{language_name} support: " - unless cmd("asdf plugin-add #{asdf_plugin_name}", quiet: true) - logc "There was an error adding the #{asdf_plugin_name} to asdf. Check #{MStrap::Paths::LOG_FILE} or run again with --debug" - end - success "OK" - end - - # Name of the ASDF plugin. Defaults to language_name - def asdf_plugin_name : String - language_name + # Execute a command using a specific language runtime version + def runtime_exec(command : String, args : Array(String)? = nil, runtime_version : String? = nil) + runtime_manager.runtime_exec(language_name, command, args, runtime_version) end # Bootstrap the current directory for the runtime @@ -36,17 +21,13 @@ module MStrap # # NOTE: This will not traverse parent directories to find versions files. def current_version - [ - version_from_env, - version_from_tool_versions, - version_from_legacy_version_file, - ].find { |version| version } + runtime_manager.current_version(language_name) end # Returns whether the ASDF plugin has been installed for a language runtime # or not - def has_asdf_plugin? - `asdf plugin-list`.chomp.split("\n").includes?(asdf_plugin_name) + def has_runtime_plugin? + runtime_manager.has_plugin?(language_name) end def has_version?(version) @@ -62,13 +43,7 @@ module MStrap # Returns a list of the versions of the language runtime installed # by ASDF. def installed_versions - `asdf list #{asdf_plugin_name} 2>&1` - .chomp - .split("\n") - .map(&.strip.lstrip('*')) - .reject do |version| - version.blank? || version == "No versions installed" - end + runtime_manager.installed_versions(language_name) end # Installs global packages for the runtime with an optional version @@ -82,24 +57,32 @@ module MStrap abstract def language_name : String # Returns the latest version available for the language runtime, according - # to the asdf plugin + # to the runtime manager def latest_version - `asdf latest #{asdf_plugin_name}`.chomp + runtime_manager.latest_version(language_name) end # Returns whether the project uses the runtime abstract def matches? : Bool - # Installs asdf plugin for the language runtime and installs any of the - # language runtime dependencies for the project. + # Installs runtime manager plugin for the language runtime and installs any + # of the language runtime dependencies for the project. def setup - asdf_install_plugin unless has_asdf_plugin? + unless runtime_manager.has_plugin?(language_name) + log "--> Installing #{language_name} plugin to #{runtime_manager.name}: " + unless runtime_manager.install_plugin(language_name) + raise_setup_error!("There was an unexpected error adding the #{language_name} plugin to #{runtime_manager.name}") + end + success "OK" + end with_dir_version(Dir.current) do + current_version = self.current_version + if current_version && current_version != "" && !has_version?(current_version) - log "--> Installing #{language_name} #{current_version} via asdf-#{asdf_plugin_name}: " - unless cmd("asdf install #{asdf_plugin_name} #{current_version}", quiet: true) - logc "There was an error installing the #{language_name} via asdf. Check #{MStrap::Paths::LOG_FILE} or run again with --debug" + log "--> Installing #{language_name} #{current_version} via #{runtime_manager.name}: " + unless runtime_manager.install_version(language_name, current_version) + raise_setup_error!("There was an unexpected error installing #{current_version} via #{runtime_manager.name}") end success "OK" end @@ -112,61 +95,24 @@ module MStrap # # NOTE: This will not traverse parent directories to find versions files. def with_dir_version(dir, &) - env_version = ENV[version_env_var]? + org_version = current_version begin Dir.cd(dir) do - ENV[version_env_var] = current_version + unless runtime_manager.set_version(language_name, current_version) + raise_setup_error!("Unable to switch version to #{current_version}") + end yield end ensure - ENV[version_env_var] = env_version - end - end - - # :nodoc: - def version_env_var - @version_env_var ||= "ASDF_#{asdf_plugin_name.upcase}_VERSION" - end - - # :nodoc: - def version_from_env - ENV[version_env_var]? - end - - # :nodoc: - def version_from_tool_versions - tool_versions_path = File.join(Dir.current, ".tool-versions") - return nil unless File.exists?(tool_versions_path) - - tool_versions = File.read(tool_versions_path).strip - if matches = tool_versions.match(/^#{asdf_plugin_name}\s+([^\s]+)$/m) - matches[1].strip - else - nil + unless runtime_manager.set_version(language_name, org_version) + raise_setup_error!("Unable to set version back to #{current_version}") + end end end - # :nodoc: - def version_from_legacy_version_file - version_path = File.join(Dir.current, ".#{language_name}-version") - return nil unless File.exists?(version_path) - File.read(version_path).strip - end - # :nodoc: protected def raise_setup_error!(message) raise RuntimeSetupError.new(language_name, message) end - - macro finished - # :nodoc: - def self.all - @@runtimes ||= [ - {% for subclass in @type.subclasses %} - {{ subclass.name }}.new, - {% end %} - ] - end - end end end diff --git a/src/mstrap/runtime_manager.cr b/src/mstrap/runtime_manager.cr new file mode 100644 index 0000000..4924673 --- /dev/null +++ b/src/mstrap/runtime_manager.cr @@ -0,0 +1,49 @@ +module MStrap + abstract class RuntimeManager + include DSL + + abstract def name : String + + def self.for(runtime_manager_name : String) + if manager = all[runtime_manager_name]? + manager + else + raise InvalidRuntimeManagerError.new(runtime_manager_name) + end + end + + abstract def current_version(language_name : String) : String? + + def has_plugin?(language_name : String) : Bool + false + end + + abstract def install_plugin(language_name : String) : Bool + abstract def install_version(language_name : String, version : String) : Bool + abstract def installed_versions(language_name : String) : Array(String) + abstract def latest_version(language_name : String) : String + abstract def runtime_exec(language_name : String, command : String, args : Array(String)? = nil, runtime_version : String? = nil) + abstract def set_version(language_name : String, version : String?) : Bool + abstract def set_global_version(language_name : String, version : String) : Bool + + macro finished + # :nodoc: + def self.all + @@runtime_managers ||= { + {% for subclass in @type.subclasses %} + {{subclass.name.stringify.split("::").last.downcase}} => {{ subclass.name }}.new, + {% end %} + } + end + + # :nodoc: + def runtimes + @runtimes ||= [ + {% for subclass in Runtime.subclasses %} + {{ subclass.name }}.new(self), + {% end %} + ] + end + end + end +end diff --git a/src/mstrap/runtime_managers/asdf.cr b/src/mstrap/runtime_managers/asdf.cr new file mode 100644 index 0000000..3675904 --- /dev/null +++ b/src/mstrap/runtime_managers/asdf.cr @@ -0,0 +1,119 @@ +module MStrap + module RuntimeManagers + class ASDF < RuntimeManager + def name : String + "asdf" + end + + def current_version(language_name : String) : String? + [ + version_from_env(language_name), + version_from_tool_versions(language_name), + version_from_legacy_version_file(language_name), + ].find { |version| version } + end + + # Returns whether the ASDF plugin has been installed for a language runtime + # or not + def has_plugin?(language_name : String) : Bool + `asdf plugin-list`.chomp.split("\n").includes?(plugin_name(language_name)) + end + + def install_plugin(language_name : String) : Bool + asdf_plugin_name = plugin_name(language_name) + + if asdf_plugin_name + cmd("asdf plugin-add #{asdf_plugin_name}", quiet: true) + else + logw "Unable to find an ASDF plugin for #{language_name}" + false + end + end + + def install_version(language_name : String, version : String) : Bool + cmd("asdf install #{plugin_name(language_name)} #{version}", quiet: true) + end + + # Returns a list of the versions of the language runtime installed + # by ASDF. + def installed_versions(language_name : String) : Array(String) + `asdf list #{plugin_name(language_name)} 2>&1` + .chomp + .split("\n") + .map(&.strip.lstrip('*')) + .reject do |version| + version.blank? || version == "No versions installed" + end + end + + def latest_version(language_name : String) : String + `asdf latest #{plugin_name(language_name)}`.chomp + end + + # Name of the ASDF plugin for a particular language + def plugin_name(language_name : String) : String? + case language_name + when "go" + "golang" + when "node" + "nodejs" + else + language_name + end + 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) + if runtime_version + version_env_var = version_env_var(language_name) + env = {version_env_var => runtime_version} + cmd env, command, args, quiet: true + else + cmd command, args, quiet: true + end + end + + def set_version(language_name : String, version : String?) : Bool + version_env_var = version_env_var(language_name) + ENV[version_env_var] = version + true + end + + def set_global_version(language_name, version : String) : Bool + cmd "asdf global #{plugin_name(language_name)} #{version}", quiet: true + end + + private def version_env_var(language_name) : String + if asdf_plugin_name = plugin_name(language_name) + "ASDF_#{asdf_plugin_name.upcase}_VERSION" + else + "ASDF_#{language_name.upcase}_VERSION" + end + end + + private def version_from_env(language_name) + env_var_name = version_env_var(language_name) + ENV[env_var_name]? + end + + private def version_from_tool_versions(language_name) + tool_versions_path = File.join(Dir.current, ".tool-versions") + return nil unless File.exists?(tool_versions_path) + + tool_versions = File.read(tool_versions_path).strip + asdf_plugin_name = plugin_name(language_name) + if matches = tool_versions.match(/^#{asdf_plugin_name}\s+([^\s]+)$/m) + matches[1].strip + else + nil + end + end + + private def version_from_legacy_version_file(language_name) + version_path = File.join(Dir.current, ".#{language_name}-version") + return nil unless File.exists?(version_path) + File.read(version_path).strip + end + end + end +end diff --git a/src/mstrap/runtime_managers/mise.cr b/src/mstrap/runtime_managers/mise.cr new file mode 100644 index 0000000..a94f5f2 --- /dev/null +++ b/src/mstrap/runtime_managers/mise.cr @@ -0,0 +1,75 @@ +module MStrap + module RuntimeManagers + class Mise < RuntimeManager + def name : String + "mise" + end + + def current_version(language_name : String) : String? + `mise current #{plugin_name(language_name)}`.chomp + 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 + + if runtime_version + exec_args << "#{plugin_name(language_name)}@#{runtime_version}" + end + + cmd_args = ["exec"] + exec_args + ["--", command] + cmd_args += args if args + + if command && (!args || args.empty?) + cmd "mise #{cmd_args.join(' ')}", quiet: true + else + cmd "mise", 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 + `mise plugins ls --core --user`.chomp.split("\n").includes?(plugin_name(language_name)) + end + + def install_plugin(language_name : String) : Bool + cmd("mise plugins install #{plugin_name(language_name)}", quiet: true) + end + + def install_version(language_name : String, version : String) : Bool + cmd("mise install #{plugin_name(language_name)} #{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) + mise_json_output = `mise ls -i #{plugin_name(language_name)} --json` + mise_installed = JSON.parse(mise_json_output) + + if installed = mise_installed.as_a? + installed.map(&.["version"].as_s) + else + Array(String).new + end + end + + def latest_version(language_name : String) : String + `mise latest #{plugin_name(language_name)}`.chomp + end + + # Name of the mise plugin for a particular language + def plugin_name(language_name : String) : String? + language_name + end + + def set_version(language_name : String, version : String?) : Bool + true + end + + def set_global_version(language_name, version : String) : Bool + cmd "mise use -g #{plugin_name(language_name)}@#{version}", quiet: true + end + end + end +end diff --git a/src/mstrap/runtimes/crystal.cr b/src/mstrap/runtimes/crystal.cr index dc4ca5a..6099610 100644 --- a/src/mstrap/runtimes/crystal.cr +++ b/src/mstrap/runtimes/crystal.cr @@ -1,14 +1,9 @@ module MStrap module Runtimes # Crystal runtime management implmentation. It contains methods for interacting - # with Crystal via ASDF and bootstrapping a Crystal project based on conventions. + # with Crystal via the chosen runtime manager and bootstrapping a Crystal + # project based on conventions. class Crystal < Runtime - class SetupError < RuntimeSetupError - def initialize(message) - super("crystal", message) - end - end - def language_name : String "crystal" end @@ -20,7 +15,7 @@ module MStrap def bootstrap if File.exists?("shard.lock") - cmd "shards check || shards install", quiet: true + runtime_exec "shards check || shards install" end end diff --git a/src/mstrap/runtimes/go.cr b/src/mstrap/runtimes/go.cr index 1b6b6d4..144278a 100644 --- a/src/mstrap/runtimes/go.cr +++ b/src/mstrap/runtimes/go.cr @@ -1,11 +1,11 @@ module MStrap module Runtimes # Go runtime management implmentation. It contains methods for interacting - # with Go via ASDF and bootstrapping a Go project based on conventions. + # with Go via the chosen runtime manager and bootstrapping a Go project + # based on conventions. class Go < Runtime - def asdf_plugin_name : String - "golang" - end + # :nodoc: + GO_INSTALL_MIN_VERSION = SemanticVersion.new(1, 16, 0) def language_name : String "go" @@ -13,13 +13,18 @@ module MStrap def bootstrap if File.exists?("go.mod") - cmd "go mod download", quiet: true + runtime_exec "go mod download" end end def install_packages(packages : Array(Defs::PkgDef), runtime_version : String? = nil) : Bool packages.all? do |pkg| - cmd_args = ["get", "-u"] + cmd_args = + if SemanticVersion.parse(runtime_version) >= GO_INSTALL_MIN_VERSION + ["install"] + else + ["get", "-u"] + end if version = pkg.version cmd_args << "#{pkg.name}@#{version}" @@ -28,7 +33,7 @@ module MStrap end disable_go_modules do - asdf_exec "go", cmd_args, runtime_version: runtime_version + runtime_exec "go", cmd_args, runtime_version: runtime_version end end end diff --git a/src/mstrap/runtimes/node.cr b/src/mstrap/runtimes/node.cr index aa97045..5f0c3db 100644 --- a/src/mstrap/runtimes/node.cr +++ b/src/mstrap/runtimes/node.cr @@ -1,22 +1,18 @@ module MStrap module Runtimes # Node runtime management implmentation. It contains methods for interacting - # with Node via ASDF and bootstrapping a Node project based on conventions. + # with Node via the chosen runtime manager and bootstrapping a Node project + # based on conventions. class Node < Runtime - def asdf_plugin_name - "nodejs" - end - def language_name : String "node" end def bootstrap if File.exists?("yarn.lock") - cmd "brew install yarn", quiet: true - skip_reshim { cmd "yarn install", quiet: true } + cmd "brew install yarn", quiet: true && skip_reshim { runtime_exec "yarn install" } elsif File.exists?("package.json") - skip_reshim { cmd "npm install", quiet: true } + skip_reshim { runtime_exec "npm install" } end end @@ -31,7 +27,7 @@ module MStrap end end - skip_reshim { asdf_exec "npm", cmd_args, runtime_version: runtime_version } + skip_reshim { runtime_exec "npm", cmd_args, runtime_version: runtime_version } end def matches? : Bool @@ -49,7 +45,7 @@ module MStrap yield ensure ENV.delete("ASDF_SKIP_RESHIM") - asdf_exec "asdf", ["reshim", "nodejs"] + runtime_exec "asdf", ["reshim", "nodejs"] if runtime_manager.name == "asdf" end end end diff --git a/src/mstrap/runtimes/php.cr b/src/mstrap/runtimes/php.cr index 9f41a81..3da82bd 100644 --- a/src/mstrap/runtimes/php.cr +++ b/src/mstrap/runtimes/php.cr @@ -1,7 +1,8 @@ module MStrap module Runtimes # PHP runtime management implmentation. It contains methods for interacting - # with PHP via ASDF and bootstrapping a PHP project based on conventions. + # with PHP via the chosen runtime manager and bootstrapping a PHP project + # based on conventions. class Php < Runtime def language_name : String "php" @@ -10,7 +11,7 @@ module MStrap def bootstrap if File.exists?("composer.json") cmd "brew install composer", quiet: true - cmd "composer install", quiet: true + runtime_exec "composer install" end end @@ -25,7 +26,7 @@ module MStrap end end - cmd "composer", cmd_args, quiet: true + runtime_exec "composer", cmd_args, runtime_version: runtime_version end def matches? : Bool diff --git a/src/mstrap/runtimes/python.cr b/src/mstrap/runtimes/python.cr index e1e7170..04456e3 100644 --- a/src/mstrap/runtimes/python.cr +++ b/src/mstrap/runtimes/python.cr @@ -1,7 +1,8 @@ module MStrap module Runtimes # Python runtime management implmentation. It contains methods for interacting - # with Python via ASDF and bootstrapping a Python project based on conventions. + # with Python via the chosen runtime manager and bootstrapping a Python + # project based on conventions. # # TODO: Does not support virtualenv class Python < Runtime @@ -11,7 +12,7 @@ module MStrap def bootstrap if File.exists?("requirements.txt") - cmd "pip install -r requirements.txt", quiet: true + runtime_exec "pip install -r requirements.txt" end end @@ -26,7 +27,7 @@ module MStrap end end - asdf_exec "pip", cmd_args, runtime_version: runtime_version + runtime_exec "pip", cmd_args, runtime_version: runtime_version end def matches? : Bool diff --git a/src/mstrap/runtimes/ruby.cr b/src/mstrap/runtimes/ruby.cr index 9fb4aad..7b8c72e 100644 --- a/src/mstrap/runtimes/ruby.cr +++ b/src/mstrap/runtimes/ruby.cr @@ -1,19 +1,16 @@ module MStrap module Runtimes # Ruby runtime management implmentation. It contains methods for interacting - # with Ruby via ASDF and bootstrapping a Ruby project based on conventions. + # with Ruby via the chosen runtime manager and bootstrapping a Ruby project + # based on conventions. class Ruby < Runtime def language_name : String "ruby" end def bootstrap - if File.exists?("gems.rb") - cmd "gem install bundler", quiet: true - cmd "bundle check || bundle install", quiet: true - elsif File.exists?("Gemfile") - cmd "gem install bundler", quiet: true - cmd "bundle check || bundle install", quiet: true + if File.exists?("gems.rb") || File.exists?("Gemfile") + runtime_exec "bundle check || bundle install" end end @@ -26,7 +23,7 @@ module MStrap cmd_args << version end - asdf_exec "gem", cmd_args, runtime_version: runtime_version + runtime_exec "gem", cmd_args, runtime_version: runtime_version end end diff --git a/src/mstrap/runtimes/rust.cr b/src/mstrap/runtimes/rust.cr index 33577d9..306f515 100644 --- a/src/mstrap/runtimes/rust.cr +++ b/src/mstrap/runtimes/rust.cr @@ -1,20 +1,21 @@ module MStrap module Runtimes # Rust runtime management implmentation. It contains methods for interacting - # with Rust via ASDF and bootstrapping a Rust project based on conventions. + # with Rust via the chosen runtime manager and bootstrapping a Rust project + # based on conventions. class Rust < Runtime def language_name : String "rust" end def current_version - # Falling back to stable is probably fairly safe - super || "stable" + # Falling back to latest is _usually_ safe + super || latest_version end def bootstrap if File.exists?("Cargo.toml") - cmd "cargo fetch", quiet: true + runtime_exec "cargo fetch" end end @@ -27,7 +28,7 @@ module MStrap cmd_args << version end - asdf_exec "cargo", cmd_args, runtime_version: runtime_version + runtime_exec "cargo", cmd_args, runtime_version: runtime_version end end diff --git a/src/mstrap/step.cr b/src/mstrap/step.cr index f79abab..753c15c 100644 --- a/src/mstrap/step.cr +++ b/src/mstrap/step.cr @@ -6,6 +6,7 @@ module MStrap @docker : Docker? = nil # BUG?: Why aren't these inferred correctly? @profile : Defs::ProfileDef + @runtime_manager : RuntimeManager @user : User # Extra arguments passed to the step not processed by the main CLI @@ -20,6 +21,9 @@ module MStrap # Resolved profile for mstrap getter :profile + # Language runtime manager for mstrap + getter :runtime_manager + # User configured for mstrap getter :user @@ -30,6 +34,7 @@ module MStrap @config = config @options = cli_options @profile = config.resolved_profile + @runtime_manager = config.runtime_manager @user = config.user end diff --git a/src/mstrap/steps/init_step.cr b/src/mstrap/steps/init_step.cr index 2f50002..5c06043 100644 --- a/src/mstrap/steps/init_step.cr +++ b/src/mstrap/steps/init_step.cr @@ -69,7 +69,7 @@ module MStrap if !File.exists?(Paths::BREWFILE) || force? logw "No Brewfile found or update requested with --force" log "--> Copying default Brewfile to #{Paths::BREWFILE}: " - brewfile_contents = Templates::Brewfile.new.to_s + brewfile_contents = Templates::Brewfile.new(runtime_manager).to_s File.write(Paths::BREWFILE, brewfile_contents) success "OK" end diff --git a/src/mstrap/steps/projects_step.cr b/src/mstrap/steps/projects_step.cr index 5ea1553..2611fbc 100644 --- a/src/mstrap/steps/projects_step.cr +++ b/src/mstrap/steps/projects_step.cr @@ -38,7 +38,7 @@ module MStrap end logn "--> Bootstrapping: " - project.bootstrap + project.bootstrap(runtime_manager) success "Finished bootstrapping #{project.name}" end diff --git a/src/mstrap/steps/runtimes_step.cr b/src/mstrap/steps/runtimes_step.cr index e2fcee7..a8f53c4 100644 --- a/src/mstrap/steps/runtimes_step.cr +++ b/src/mstrap/steps/runtimes_step.cr @@ -10,8 +10,8 @@ module MStrap end def bootstrap - MStrap::Runtime.all.each do |runtime| - if runtime.has_asdf_plugin? && runtime.has_versions? + runtime_manager.runtimes.each do |runtime| + if runtime.has_runtime_plugin? && runtime.has_versions? logn "==> Setting global #{runtime.language_name} settings" set_default_to_latest(runtime) packages = runtime_packages(runtime) @@ -46,8 +46,10 @@ module MStrap runtime.installed_versions.last end + return unless latest_version + log "--> Setting default #{runtime.language_name} version to #{latest_version}: " - unless cmd "asdf global #{runtime.asdf_plugin_name} #{latest_version}", quiet: true + unless runtime_manager.set_global_version(runtime.language_name, latest_version) logc "Could not set global #{runtime.language_name} version to #{latest_version}" end success "OK" diff --git a/src/mstrap/steps/shell_step.cr b/src/mstrap/steps/shell_step.cr index a11d963..5d4da89 100644 --- a/src/mstrap/steps/shell_step.cr +++ b/src/mstrap/steps/shell_step.cr @@ -26,6 +26,7 @@ module MStrap MSG @login_shell : String? = nil + @shell_name : String? = nil def self.bootstrap(options) new(options).bootstrap @@ -46,7 +47,7 @@ module MStrap def bootstrap Dir.mkdir_p(MStrap::Paths::RC_DIR) - contents = Templates::EnvSh.new.to_s + contents = Templates::EnvSh.new(shell_name, runtime_manager).to_s File.write(env_sh_path, contents, perm: 0o600) exit_if_shell_changed! @@ -92,6 +93,10 @@ module MStrap end end + private def shell_name + @shell_name ||= login_shell.split("/").last + end + private def shell_supported? !shell_file_name.nil? end diff --git a/src/mstrap/templates/Brewfile.cr b/src/mstrap/templates/Brewfile.cr index 37f1e1a..4e21e67 100644 --- a/src/mstrap/templates/Brewfile.cr +++ b/src/mstrap/templates/Brewfile.cr @@ -3,6 +3,11 @@ module MStrap module Templates class Brewfile ECR.def_to_s "#{__DIR__}/Brewfile.ecr" + + getter :runtime_manager + + def initialize(@runtime_manager : RuntimeManager) + end end end end diff --git a/src/mstrap/templates/Brewfile.ecr b/src/mstrap/templates/Brewfile.ecr index 1f11ada..ee883f0 100644 --- a/src/mstrap/templates/Brewfile.ecr +++ b/src/mstrap/templates/Brewfile.ecr @@ -9,7 +9,11 @@ brew 'pkg-config' brew 'zlib' # Language runtime managers +<% if runtime_manager.name == "asdf" %> brew 'asdf' +<% elsif runtime_manager.name == "mise" %> +brew 'mise' +<% end %> if /darwin/ =~ RUBY_PLATFORM brew 'autoconf' diff --git a/src/mstrap/templates/env.sh.ecr b/src/mstrap/templates/env.sh.ecr index 1e8e4c4..89faf3c 100644 --- a/src/mstrap/templates/env.sh.ecr +++ b/src/mstrap/templates/env.sh.ecr @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env <%= shell_name %> export MSTRAP=true export MSTRAP_PROJECT_SOCKETS="<%= MStrap::Paths::PROJECT_SOCKETS %>" @@ -6,9 +6,12 @@ export MSTRAP_SRC_DIR="<%= MStrap::Paths::SRC_DIR %>" export MSTRAP_RC_DIR="<%= MStrap::Paths::RC_DIR %>" <%- if needs_homebrew_shellenv? %> +# Load Homebrew test -d <%= MStrap::Paths::HOMEBREW_PREFIX %> && eval $(<%= MStrap::Paths::HOMEBREW_PREFIX %>/bin/brew shellenv) <% end -%> +<% if runtime_manager.name == "asdf" %> +# Activate asdf for language runtime version management if [ -d "$(brew --prefix asdf)" ]; then source "$(brew --prefix asdf)/libexec/asdf.sh" @@ -16,3 +19,10 @@ if [ -d "$(brew --prefix asdf)" ]; then echo "legacy_version_file = yes\n" > "$HOME/.asdfrc" fi fi +<% elsif runtime_manager.name == "mise" %> +# Activate mise for language runtime version management +if command -v mise 2>&1 > /dev/null; then + export MISE_ASDF_COMPAT=1 + eval "$(mise activate <%= shell_name %>)" +fi +<% end %> diff --git a/src/mstrap/templates/env_sh.cr b/src/mstrap/templates/env_sh.cr index a47fae4..158b16e 100644 --- a/src/mstrap/templates/env_sh.cr +++ b/src/mstrap/templates/env_sh.cr @@ -4,6 +4,12 @@ module MStrap class EnvSh ECR.def_to_s "#{__DIR__}/env.sh.ecr" + getter :shell_name + getter :runtime_manager + + def initialize(@shell_name : String, @runtime_manager : RuntimeManager) + end + def needs_homebrew_shellenv? {{ flag?(:linux) || (flag?(:aarch64) && flag?(:darwin)) }} end