From 5f4cdd2a51af351a7c7ead2d1bf2b2c22a623398 Mon Sep 17 00:00:00 2001 From: Max Fierke Date: Mon, 12 Feb 2024 13:56:44 -0600 Subject: [PATCH 1/5] Infer runtime & runtime manager names from their classes by default --- src/mstrap/runtime.cr | 4 +++- src/mstrap/runtime_manager.cr | 4 +++- src/mstrap/runtime_managers/asdf.cr | 4 ---- src/mstrap/runtime_managers/mise.cr | 4 ---- src/mstrap/runtimes/crystal.cr | 6 +----- src/mstrap/runtimes/go.cr | 4 ---- src/mstrap/runtimes/node.cr | 4 ---- src/mstrap/runtimes/php.cr | 4 ---- src/mstrap/runtimes/python.cr | 4 ---- src/mstrap/runtimes/ruby.cr | 6 +----- src/mstrap/runtimes/rust.cr | 4 ---- 11 files changed, 8 insertions(+), 40 deletions(-) diff --git a/src/mstrap/runtime.cr b/src/mstrap/runtime.cr index 596286b..1bbfd6f 100644 --- a/src/mstrap/runtime.cr +++ b/src/mstrap/runtime.cr @@ -54,7 +54,9 @@ module MStrap abstract def install_packages(packages : Array(Defs::PkgDef), runtime_version : String? = nil) : Bool # Name of the language as a string. Always lowercase. - abstract def language_name : String + def language_name : String + {{ @type.name.stringify.split("::").last.downcase }} + end # Returns the latest version available for the language runtime, according # to the runtime manager diff --git a/src/mstrap/runtime_manager.cr b/src/mstrap/runtime_manager.cr index 8414e67..2a3dba4 100644 --- a/src/mstrap/runtime_manager.cr +++ b/src/mstrap/runtime_manager.cr @@ -2,7 +2,9 @@ module MStrap abstract class RuntimeManager include DSL - abstract def name : String + def name : String + {{ @type.name.stringify.split("::").last.downcase }} + end def self.for(runtime_manager_name : String) if manager = all[runtime_manager_name]? diff --git a/src/mstrap/runtime_managers/asdf.cr b/src/mstrap/runtime_managers/asdf.cr index aa6eb31..1999e9d 100644 --- a/src/mstrap/runtime_managers/asdf.cr +++ b/src/mstrap/runtime_managers/asdf.cr @@ -1,10 +1,6 @@ module MStrap module RuntimeManagers class ASDF < RuntimeManager - def name : String - "asdf" - end - def current_version(language_name : String) : String? [ version_from_env(language_name), diff --git a/src/mstrap/runtime_managers/mise.cr b/src/mstrap/runtime_managers/mise.cr index e8c75eb..e979303 100644 --- a/src/mstrap/runtime_managers/mise.cr +++ b/src/mstrap/runtime_managers/mise.cr @@ -1,10 +1,6 @@ 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 diff --git a/src/mstrap/runtimes/crystal.cr b/src/mstrap/runtimes/crystal.cr index 6099610..a71c5b0 100644 --- a/src/mstrap/runtimes/crystal.cr +++ b/src/mstrap/runtimes/crystal.cr @@ -4,10 +4,6 @@ module MStrap # with Crystal via the chosen runtime manager and bootstrapping a Crystal # project based on conventions. class Crystal < Runtime - def language_name : String - "crystal" - end - def current_version # Falling back to the latest is usually fairly safe super || latest_version @@ -15,7 +11,7 @@ module MStrap def bootstrap if File.exists?("shard.lock") - runtime_exec "shards check || shards install" + runtime_exec "shards install" unless runtime_exec "shards check" end end diff --git a/src/mstrap/runtimes/go.cr b/src/mstrap/runtimes/go.cr index 144278a..6707cb8 100644 --- a/src/mstrap/runtimes/go.cr +++ b/src/mstrap/runtimes/go.cr @@ -7,10 +7,6 @@ module MStrap # :nodoc: GO_INSTALL_MIN_VERSION = SemanticVersion.new(1, 16, 0) - def language_name : String - "go" - end - def bootstrap if File.exists?("go.mod") runtime_exec "go mod download" diff --git a/src/mstrap/runtimes/node.cr b/src/mstrap/runtimes/node.cr index 5f0c3db..789000a 100644 --- a/src/mstrap/runtimes/node.cr +++ b/src/mstrap/runtimes/node.cr @@ -4,10 +4,6 @@ module MStrap # with Node via the chosen runtime manager and bootstrapping a Node project # based on conventions. class Node < Runtime - def language_name : String - "node" - end - def bootstrap if File.exists?("yarn.lock") cmd "brew install yarn", quiet: true && skip_reshim { runtime_exec "yarn install" } diff --git a/src/mstrap/runtimes/php.cr b/src/mstrap/runtimes/php.cr index 3da82bd..0a28162 100644 --- a/src/mstrap/runtimes/php.cr +++ b/src/mstrap/runtimes/php.cr @@ -4,10 +4,6 @@ module MStrap # with PHP via the chosen runtime manager and bootstrapping a PHP project # based on conventions. class Php < Runtime - def language_name : String - "php" - end - def bootstrap if File.exists?("composer.json") cmd "brew install composer", quiet: true diff --git a/src/mstrap/runtimes/python.cr b/src/mstrap/runtimes/python.cr index 04456e3..0e5504e 100644 --- a/src/mstrap/runtimes/python.cr +++ b/src/mstrap/runtimes/python.cr @@ -6,10 +6,6 @@ module MStrap # # TODO: Does not support virtualenv class Python < Runtime - def language_name : String - "python" - end - def bootstrap if File.exists?("requirements.txt") runtime_exec "pip install -r requirements.txt" diff --git a/src/mstrap/runtimes/ruby.cr b/src/mstrap/runtimes/ruby.cr index 7b8c72e..bf1e8ca 100644 --- a/src/mstrap/runtimes/ruby.cr +++ b/src/mstrap/runtimes/ruby.cr @@ -4,13 +4,9 @@ module MStrap # 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") || File.exists?("Gemfile") - runtime_exec "bundle check || bundle install" + runtime_exec "bundle install" unless runtime_exec "bundle check" end end diff --git a/src/mstrap/runtimes/rust.cr b/src/mstrap/runtimes/rust.cr index 306f515..3c3e93a 100644 --- a/src/mstrap/runtimes/rust.cr +++ b/src/mstrap/runtimes/rust.cr @@ -4,10 +4,6 @@ module MStrap # 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 latest is _usually_ safe super || latest_version From 7cb86b6fe72c38fde3bcf82a2238a6aca1b48ee3 Mon Sep 17 00:00:00 2001 From: Max Fierke Date: Mon, 12 Feb 2024 16:19:17 -0600 Subject: [PATCH 2/5] Extract class on Project#bootstrap --- src/mstrap.cr | 3 +- src/mstrap/bootstrapper.cr | 28 ++++++++ .../bootstrappers/default_bootstrapper.cr | 35 ++++++++++ .../bootstrappers/script_bootstrapper.cr | 38 +++++++++++ src/mstrap/bootstrappers/web_bootstrapper.cr | 33 +++++++++ src/mstrap/cli.cr | 2 +- src/mstrap/project.cr | 68 ------------------- src/mstrap/steps/projects_step.cr | 4 +- src/mstrap/web_bootstrapper.cr | 31 --------- 9 files changed, 139 insertions(+), 103 deletions(-) create mode 100644 src/mstrap/bootstrapper.cr create mode 100644 src/mstrap/bootstrappers/default_bootstrapper.cr create mode 100644 src/mstrap/bootstrappers/script_bootstrapper.cr create mode 100644 src/mstrap/bootstrappers/web_bootstrapper.cr delete mode 100644 src/mstrap/web_bootstrapper.cr diff --git a/src/mstrap.cr b/src/mstrap.cr index 2b256e8..19dab49 100644 --- a/src/mstrap.cr +++ b/src/mstrap.cr @@ -25,7 +25,8 @@ require "./mstrap/profile_fetcher" require "./mstrap/user" require "./mstrap/configuration" require "./mstrap/supports/**" -require "./mstrap/web_bootstrapper" +require "./mstrap/bootstrapper" +require "./mstrap/bootstrappers/**" require "./mstrap/runtime_manager" require "./mstrap/runtime_managers/**" require "./mstrap/runtime" diff --git a/src/mstrap/bootstrapper.cr b/src/mstrap/bootstrapper.cr new file mode 100644 index 0000000..89dc76e --- /dev/null +++ b/src/mstrap/bootstrapper.cr @@ -0,0 +1,28 @@ +module MStrap + abstract class Bootstrapper + include DSL + + def initialize(@config : Configuration) + end + + def self.for(config : Configuration, project : Project) : Array(Bootstrapper) + bootstrappers = Array(Bootstrapper).new + + if project.run_scripts? && Bootstrappers::ScriptBootstrapper.has_scripts?(project) + bootstrappers << Bootstrappers::ScriptBootstrapper.new(config) + else + bootstrappers << Bootstrappers::DefaultBootstrapper.new(config) + + if project.web? + bootstrappers << Bootstrappers::WebBootstrapper.new(config) + end + end + + bootstrappers + end + + abstract def bootstrap(project : Project) : Bool + + protected getter :config + end +end diff --git a/src/mstrap/bootstrappers/default_bootstrapper.cr b/src/mstrap/bootstrappers/default_bootstrapper.cr new file mode 100644 index 0000000..62e0e80 --- /dev/null +++ b/src/mstrap/bootstrappers/default_bootstrapper.cr @@ -0,0 +1,35 @@ +module MStrap + module Bootstrappers + class DefaultBootstrapper < Bootstrapper + # Conventional bootstrapping from mstrap. This will auto-detect the runtimes + # used by the project and run the standard bootstrapping for each runtime. + # 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`. + def bootstrap(project : Project) : Bool + logd "Bootstrapping '#{project.name}' with runtime defaults." + + runtime_impls(project).each do |runtime| + Dir.cd(project.path) do + if runtime.matches? + logd "Detected #{runtime.language_name}. Installing #{runtime.language_name}, project #{runtime.language_name} packages, and other relevant dependencies" + runtime.setup + end + end + end + + true + end + + def runtime_impls(project) + if project.runtimes.empty? + config.runtime_manager.runtimes + else + config.runtime_manager.runtimes.select do |runtime| + project.runtimes.includes?(runtime.language_name) + end + end + end + end + end +end diff --git a/src/mstrap/bootstrappers/script_bootstrapper.cr b/src/mstrap/bootstrappers/script_bootstrapper.cr new file mode 100644 index 0000000..b999a57 --- /dev/null +++ b/src/mstrap/bootstrappers/script_bootstrapper.cr @@ -0,0 +1,38 @@ +module MStrap + module Bootstrappers + class ScriptBootstrapper < Bootstrapper + # :nodoc: + BOOTSTRAP_SCRIPT = File.join("script", "bootstrap") + + # :nodoc: + SETUP_SCRIPT = File.join("script", "setup") + + # Executes `script/bootstrap` and `script/setup` (if either exists and are + # configured to run) + def bootstrap(project : Project) : Bool + logd "Found bootstrapping scripts, executing instead of using defaults." + + begin + ENV["__MSTRAP_EXEC_SCRIPTS"] = "true" + + Dir.cd(project.path) do + cmd BOOTSTRAP_SCRIPT if File.exists?(BOOTSTRAP_SCRIPT) + cmd SETUP_SCRIPT if File.exists?(SETUP_SCRIPT) + end + ensure + ENV.delete("__MSTRAP_EXEC_SCRIPTS") + end + + true + end + + # Whether project has any bootstrapping/setup scripts a-la + # [`scripts-to-rule-them-all`](https://github.com/github/scripts-to-rule-them-all) + def self.has_scripts?(project) + [BOOTSTRAP_SCRIPT, SETUP_SCRIPT].any? do |script_path| + File.exists?(File.join(project.path, script_path)) + end + end + end + end +end diff --git a/src/mstrap/bootstrappers/web_bootstrapper.cr b/src/mstrap/bootstrappers/web_bootstrapper.cr new file mode 100644 index 0000000..77e703b --- /dev/null +++ b/src/mstrap/bootstrappers/web_bootstrapper.cr @@ -0,0 +1,33 @@ +module MStrap + module Bootstrappers + # The `WebBootstrapper` is responsible for bootstrapping web-based projects. + # Currently, this is just setting up an NGINX configuration for the project. + class WebBootstrapper < Bootstrapper + include DSL + + def initialize(@config : Configuration) + super + @mkcert = Mkcert.new + end + + # Executes the bootstrapper + def bootstrap(project : Project) : Bool + logd "'#{project.name}' is a web project. Running web bootstrapper." + + if mkcert.installed? + Dir.cd(Paths::PROJECT_CERTS) do + mkcert.install! + mkcert.install_cert!(project.hostname) + end + else + logw "mkcert not found. Skipping cert setup." + end + + Templates::NginxConf.new(project).write_to_config! + true + end + + private getter :mkcert + end + end +end diff --git a/src/mstrap/cli.cr b/src/mstrap/cli.cr index 7430e13..96fb574 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(config.runtime_manager) + Bootstrapper.for(config, project).each { |bs| bs.bootstrap(project) } end end diff --git a/src/mstrap/project.cr b/src/mstrap/project.cr index 5f11b66..218387d 100644 --- a/src/mstrap/project.cr +++ b/src/mstrap/project.cr @@ -8,12 +8,6 @@ module MStrap # :nodoc: SCP_REPO_REGEX = /\A(.+@)?[\w\d\.\-_]+:/ - # :nodoc: - BOOTSTRAP_SCRIPT = File.join("script", "bootstrap") - - # :nodoc: - SETUP_SCRIPT = File.join("script", "setup") - @cname : String @hostname : String @name : String @@ -117,14 +111,6 @@ module MStrap end end - # Whether project has any bootstrapping/setup scripts a-la - # [`scripts-to-rule-them-all`](https://github.com/github/scripts-to-rule-them-all) - def has_scripts? - [BOOTSTRAP_SCRIPT, SETUP_SCRIPT].any? do |script_path| - File.exists?(File.join(path, script_path)) - end - end - # Clones the project from Git def clone success = cmd("git", "clone", git_uri, path, quiet: true) @@ -165,60 +151,6 @@ module MStrap end end - # 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(runtime_manager : RuntimeManager) - if has_scripts? && run_scripts? - logd "Found bootstrapping scripts, executing instead of using defaults." - begin - ENV["__MSTRAP_EXEC_SCRIPTS"] = "true" - - Dir.cd(path) do - cmd BOOTSTRAP_SCRIPT if File.exists?(BOOTSTRAP_SCRIPT) - cmd SETUP_SCRIPT if File.exists?(SETUP_SCRIPT) - end - ensure - ENV.delete("__MSTRAP_EXEC_SCRIPTS") - end - else - logd "Bootstrapping '#{name}' with runtime defaults." - default_bootstrap(runtime_manager) - end - end - - # Conventional bootstrapping from mstrap. This will auto-detect the runtimes - # used by the project and run the standard bootstrapping for each runtime. - # 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`. - # - # TODO: Move this somewhere more appropriate - protected def default_bootstrap(runtime_manager : RuntimeManager) - runtime_impls = - if runtimes.empty? - runtime_manager.runtimes - else - runtime_manager.runtimes.select do |runtime| - runtimes.includes?(runtime.language_name) - end - end - - runtime_impls.each do |runtime| - Dir.cd(path) do - if runtime.matches? - logd "Detected #{runtime.language_name}. Installing #{runtime.language_name}, project #{runtime.language_name} packages, and other relevant dependencies" - runtime.setup - end - end - end - - if web? - logd "'#{name}' is a web project. Running web bootstrapper." - WebBootstrapper.new(self).bootstrap - end - end - private def current_branch `git rev-parse --abbrev-ref HEAD`.chomp end diff --git a/src/mstrap/steps/projects_step.cr b/src/mstrap/steps/projects_step.cr index 2611fbc..f2fa04e 100644 --- a/src/mstrap/steps/projects_step.cr +++ b/src/mstrap/steps/projects_step.cr @@ -4,7 +4,7 @@ module MStrap # updating, and bootstrapping all configured projects. class ProjectsStep < Step @has_web_projects = false - @projects : Array(Project) | Nil + @projects : Array(Project)? def self.description "Bootstraps configured projects" @@ -38,7 +38,7 @@ module MStrap end logn "--> Bootstrapping: " - project.bootstrap(runtime_manager) + Bootstrapper.for(config, project).each { |bs| bs.bootstrap(project) } success "Finished bootstrapping #{project.name}" end diff --git a/src/mstrap/web_bootstrapper.cr b/src/mstrap/web_bootstrapper.cr deleted file mode 100644 index 8351f7f..0000000 --- a/src/mstrap/web_bootstrapper.cr +++ /dev/null @@ -1,31 +0,0 @@ -module MStrap - # The `WebBootstrapper` is responsible for bootstrapping web-based projects. - # Currently, this is just setting up an NGINX configuration for the project. - class WebBootstrapper - include DSL - - # Project to run bootstrapper on - getter :project - - def initialize(project : Project) - @project = project - @mkcert = Mkcert.new - end - - # Executes the bootstrapper - def bootstrap - if mkcert.installed? - Dir.cd(Paths::PROJECT_CERTS) do - mkcert.install! - mkcert.install_cert!(project.hostname) - end - else - logw "mkcert not found. Skipping cert setup." - end - - Templates::NginxConf.new(project).write_to_config! - end - - private getter :mkcert - end -end From 4b5610fcac7ebedb4a860fe1e2b85b6b83fa3c97 Mon Sep 17 00:00:00 2001 From: Max Fierke Date: Mon, 12 Feb 2024 18:19:34 -0600 Subject: [PATCH 3/5] Implement partial support for multiple runtime managers --- .../bootstrappers/default_bootstrapper.cr | 6 +-- src/mstrap/configuration.cr | 22 +++++++-- src/mstrap/defs/runtime_config_def.cr | 15 ++++++ src/mstrap/defs/runtimes_config_def.cr | 5 +- src/mstrap/errors.cr | 6 +++ src/mstrap/runtime_manager.cr | 47 ++++++++++++++++--- src/mstrap/runtime_managers/asdf.cr | 4 ++ src/mstrap/runtime_managers/mise.cr | 4 ++ src/mstrap/step.cr | 14 ++++-- src/mstrap/steps/debug_step.cr | 10 +++- src/mstrap/steps/dependencies_step.cr | 2 +- src/mstrap/steps/init_step.cr | 2 +- src/mstrap/steps/runtimes_step.cr | 4 +- src/mstrap/steps/shell_step.cr | 2 +- src/mstrap/templates/Brewfile.cr | 4 +- src/mstrap/templates/Brewfile.ecr | 2 +- src/mstrap/templates/env.sh.ecr | 2 + src/mstrap/templates/env_sh.cr | 4 +- 18 files changed, 126 insertions(+), 29 deletions(-) create mode 100644 src/mstrap/defs/runtime_config_def.cr diff --git a/src/mstrap/bootstrappers/default_bootstrapper.cr b/src/mstrap/bootstrappers/default_bootstrapper.cr index 62e0e80..099e2fc 100644 --- a/src/mstrap/bootstrappers/default_bootstrapper.cr +++ b/src/mstrap/bootstrappers/default_bootstrapper.cr @@ -9,7 +9,7 @@ module MStrap def bootstrap(project : Project) : Bool logd "Bootstrapping '#{project.name}' with runtime defaults." - runtime_impls(project).each do |runtime| + runtime_impls(project).each_value do |runtime| Dir.cd(project.path) do if runtime.matches? logd "Detected #{runtime.language_name}. Installing #{runtime.language_name}, project #{runtime.language_name} packages, and other relevant dependencies" @@ -23,9 +23,9 @@ module MStrap def runtime_impls(project) if project.runtimes.empty? - config.runtime_manager.runtimes + config.runtimes else - config.runtime_manager.runtimes.select do |runtime| + config.runtimes.select do |_, runtime| project.runtimes.includes?(runtime.language_name) end end diff --git a/src/mstrap/configuration.cr b/src/mstrap/configuration.cr index b5e276b..d7ce9ac 100644 --- a/src/mstrap/configuration.cr +++ b/src/mstrap/configuration.cr @@ -3,11 +3,13 @@ module MStrap include DSL @config_def : Defs::ConfigDef + @default_runtime_manager : RuntimeManager @loaded_profile_configs : Array(Defs::ProfileConfigDef) @loaded_profiles : Array(Defs::ProfileDef) @known_profile_configs : Array(Defs::ProfileConfigDef) @resolved_profile : Defs::ProfileDef - @runtime_manager : RuntimeManager + @runtime_managers : Array(RuntimeManager) + @runtimes : Hash(String, Runtime) @user : User DEFAULT_PROFILE_CONFIG_DEF = Defs::DefaultProfileConfigDef.new @@ -28,8 +30,18 @@ module MStrap # profiles with the default profiles. getter :resolved_profile - # Returns the runtime manager specified by the configuration - getter :runtime_manager + # Returns the default runtime manager specified by the configuration + getter :default_runtime_manager + + # Returns the runtime managers specified by the configuration + getter :runtime_managers + + # Returns the language runtimes with their resolved runtime manager + # + # Raises UnsupportedLanguageRuntimeManagerError if the configuration of a + # language runtime to a runtime manager is invalid + # Raises InvalidRuntimeManagerError if an invalid runtime manager is provided + getter :runtimes # Returns the mstrap user getter :user @@ -40,11 +52,13 @@ module MStrap ) @config_def = config @config_path = config_path + @default_runtime_manager = RuntimeManager.for(config.runtimes.default_manager) @loaded_profile_configs = [] of Defs::ProfileConfigDef @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) + @runtime_managers = RuntimeManager.resolve_managers(config) + @runtimes = RuntimeManager.resolve_runtimes(config) @user = User.new(user: config.user) end diff --git a/src/mstrap/defs/runtime_config_def.cr b/src/mstrap/defs/runtime_config_def.cr new file mode 100644 index 0000000..c390302 --- /dev/null +++ b/src/mstrap/defs/runtime_config_def.cr @@ -0,0 +1,15 @@ +module MStrap + module Defs + class RuntimeConfigDef + include HCL::Serializable + + @[HCL::Label] + property name : String + + @[HCL::Attribute] + property manager : String? = nil + + def_equals_and_hash @name, @manager + end + end +end diff --git a/src/mstrap/defs/runtimes_config_def.cr b/src/mstrap/defs/runtimes_config_def.cr index 0df0c6e..6be0a2b 100644 --- a/src/mstrap/defs/runtimes_config_def.cr +++ b/src/mstrap/defs/runtimes_config_def.cr @@ -6,7 +6,10 @@ module MStrap @[HCL::Attribute] property default_manager = "asdf" - def_equals_and_hash @default_manager + @[HCL::Block(key: "runtime")] + property runtimes = [] of ::MStrap::Defs::RuntimeConfigDef + + def_equals_and_hash @default_manager, @runtimes def initialize end diff --git a/src/mstrap/errors.cr b/src/mstrap/errors.cr index 97c463a..e20493c 100644 --- a/src/mstrap/errors.cr +++ b/src/mstrap/errors.cr @@ -38,6 +38,12 @@ module MStrap end end + class UnsupportedLanguageRuntimeManagerError < MStrapError + def initialize(manager_name, language_name) + super("#{manager_name} does not support the language provided: #{language_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/runtime_manager.cr b/src/mstrap/runtime_manager.cr index 2a3dba4..5f1a9ee 100644 --- a/src/mstrap/runtime_manager.cr +++ b/src/mstrap/runtime_manager.cr @@ -2,6 +2,8 @@ module MStrap abstract class RuntimeManager include DSL + @runtimes : Array(Runtime)? + def name : String {{ @type.name.stringify.split("::").last.downcase }} end @@ -28,6 +30,7 @@ module MStrap abstract def set_version(language_name : String, version : String?) : Bool abstract def set_global_version(language_name : String, version : String) : Bool abstract def shell_activation(shell_name : String) : String + abstract def supported_languages : Array(String) macro finished # :nodoc: @@ -40,12 +43,44 @@ module MStrap end # :nodoc: - def runtimes - @runtimes ||= [ - {% for subclass in Runtime.subclasses %} - {{ subclass.name }}.new(self), - {% end %} - ] + def self.resolve_managers(config_def : Defs::ConfigDef) : Array(RuntimeManager) + default_runtime_manager = self.for(config_def.runtimes.default_manager) + managers = [default_runtime_manager] + + config_def.runtimes.runtimes.map(&.manager).uniq!.each do |manager_name| + next if !manager_name + managers << RuntimeManager.for(manager_name) + end + + managers + end + + # :nodoc: + def self.resolve_runtimes(config_def : Defs::ConfigDef) : Hash(String, Runtime) + impls = Hash(String, Runtime).new + default_manager = {{ @type }}.all[config_def.runtimes.default_manager] + + {% for subclass, index in Runtime.subclasses %} + {% language_name = subclass.name.stringify.split("::").last.downcase %} + + %runtime_def{index} = config_def.runtimes.runtimes.find { |r| r.name == {{ language_name }} } + + if %runtime_def{index} && (runtime_manager_name = %runtime_def{index}.manager) + runtime_manager = self.for(runtime_manager_name) + + if !runtime_manager.supported_languages.includes?({{ language_name }}) + raise UnsupportedLanguageRuntimeManagerError.new(runtime_manager.name, {{ language_name }}) + end + + impls[{{language_name}}] = {{ subclass.name }}.new(runtime_manager) + elsif default_manager.supported_languages.includes?({{ language_name }}) + impls[{{language_name}}] = {{ subclass.name }}.new(default_manager) + else + raise UnsupportedLanguageRuntimeManagerError.new(default_manager.name, {{ language_name }}) + end + {% end %} + + impls end end end diff --git a/src/mstrap/runtime_managers/asdf.cr b/src/mstrap/runtime_managers/asdf.cr index 1999e9d..5099807 100644 --- a/src/mstrap/runtime_managers/asdf.cr +++ b/src/mstrap/runtime_managers/asdf.cr @@ -92,6 +92,10 @@ module MStrap SHELL end + def supported_languages : Array(String) + %w(crystal go node php python ruby rust) + end + private def version_env_var(language_name) : String if asdf_plugin_name = plugin_name(language_name) "ASDF_#{asdf_plugin_name.upcase}_VERSION" diff --git a/src/mstrap/runtime_managers/mise.cr b/src/mstrap/runtime_managers/mise.cr index e979303..954c690 100644 --- a/src/mstrap/runtime_managers/mise.cr +++ b/src/mstrap/runtime_managers/mise.cr @@ -77,6 +77,10 @@ module MStrap fi SHELL end + + def supported_languages : Array(String) + %w(crystal go node php python ruby rust) + end end end end diff --git a/src/mstrap/step.cr b/src/mstrap/step.cr index 753c15c..f091f9e 100644 --- a/src/mstrap/step.cr +++ b/src/mstrap/step.cr @@ -6,7 +6,8 @@ module MStrap @docker : Docker? = nil # BUG?: Why aren't these inferred correctly? @profile : Defs::ProfileDef - @runtime_manager : RuntimeManager + @runtime_managers : Array(RuntimeManager) + @runtimes : Hash(String, Runtime) @user : User # Extra arguments passed to the step not processed by the main CLI @@ -21,8 +22,11 @@ module MStrap # Resolved profile for mstrap getter :profile - # Language runtime manager for mstrap - getter :runtime_manager + # Language runtime managers in use + getter :runtime_managers + + # Language runtimes + getter :runtimes # User configured for mstrap getter :user @@ -33,8 +37,10 @@ module MStrap @args = args @config = config @options = cli_options + @profile = config.resolved_profile - @runtime_manager = config.runtime_manager + @runtime_managers = config.runtime_managers + @runtimes = config.runtimes @user = config.user end diff --git a/src/mstrap/steps/debug_step.cr b/src/mstrap/steps/debug_step.cr index 91fc95f..7a56dcd 100644 --- a/src/mstrap/steps/debug_step.cr +++ b/src/mstrap/steps/debug_step.cr @@ -20,7 +20,15 @@ module MStrap puts "Loaded Config:" puts " #{options.config_path}" puts "Default runtime manager:" - puts " #{runtime_manager.name}" + puts " #{config.default_runtime_manager.name}" + puts "Resolved runtime managers:" + config.runtime_managers.each do |runtime_manager| + puts " #{runtime_manager.name}" + end + puts "Resolved runtimes:" + config.runtimes.each do |runtime_name, runtime| + puts " #{runtime_name} (via #{runtime.runtime_manager.name})" + end puts "Known Profiles:" config.known_profile_configs.each do |profile| puts " #{profile.name}" diff --git a/src/mstrap/steps/dependencies_step.cr b/src/mstrap/steps/dependencies_step.cr index 7993345..47def93 100644 --- a/src/mstrap/steps/dependencies_step.cr +++ b/src/mstrap/steps/dependencies_step.cr @@ -16,7 +16,7 @@ module MStrap end def bootstrap - install_mise if runtime_manager.name == "mise" + install_mise if config.default_runtime_manager.name == "mise" set_strap_env! strap_sh load_profile! diff --git a/src/mstrap/steps/init_step.cr b/src/mstrap/steps/init_step.cr index afa697f..c5310a2 100644 --- a/src/mstrap/steps/init_step.cr +++ b/src/mstrap/steps/init_step.cr @@ -40,7 +40,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(runtime_manager).to_s + brewfile_contents = Templates::Brewfile.new(config.default_runtime_manager).to_s File.write(Paths::BREWFILE, brewfile_contents) success "OK" end diff --git a/src/mstrap/steps/runtimes_step.cr b/src/mstrap/steps/runtimes_step.cr index a8f53c4..b147d3c 100644 --- a/src/mstrap/steps/runtimes_step.cr +++ b/src/mstrap/steps/runtimes_step.cr @@ -10,7 +10,7 @@ module MStrap end def bootstrap - runtime_manager.runtimes.each do |runtime| + runtimes.each_value do |runtime| if runtime.has_runtime_plugin? && runtime.has_versions? logn "==> Setting global #{runtime.language_name} settings" set_default_to_latest(runtime) @@ -49,7 +49,7 @@ module MStrap return unless latest_version log "--> Setting default #{runtime.language_name} version to #{latest_version}: " - unless runtime_manager.set_global_version(runtime.language_name, latest_version) + unless runtime.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 5d4da89..76940f8 100644 --- a/src/mstrap/steps/shell_step.cr +++ b/src/mstrap/steps/shell_step.cr @@ -47,7 +47,7 @@ module MStrap def bootstrap Dir.mkdir_p(MStrap::Paths::RC_DIR) - contents = Templates::EnvSh.new(shell_name, runtime_manager).to_s + contents = Templates::EnvSh.new(shell_name, runtime_managers).to_s File.write(env_sh_path, contents, perm: 0o600) exit_if_shell_changed! diff --git a/src/mstrap/templates/Brewfile.cr b/src/mstrap/templates/Brewfile.cr index 4e21e67..e67f245 100644 --- a/src/mstrap/templates/Brewfile.cr +++ b/src/mstrap/templates/Brewfile.cr @@ -4,9 +4,9 @@ module MStrap class Brewfile ECR.def_to_s "#{__DIR__}/Brewfile.ecr" - getter :runtime_manager + getter :default_runtime_manager - def initialize(@runtime_manager : RuntimeManager) + def initialize(@default_runtime_manager : RuntimeManager) end end end diff --git a/src/mstrap/templates/Brewfile.ecr b/src/mstrap/templates/Brewfile.ecr index bd52e8d..d6d8037 100644 --- a/src/mstrap/templates/Brewfile.ecr +++ b/src/mstrap/templates/Brewfile.ecr @@ -9,7 +9,7 @@ brew 'pkg-config' brew 'zlib' # Language runtime managers -<% if runtime_manager.name == "asdf" %> +<% if default_runtime_manager.name == "asdf" %> brew 'asdf' <% end %> diff --git a/src/mstrap/templates/env.sh.ecr b/src/mstrap/templates/env.sh.ecr index 2235c06..d890396 100644 --- a/src/mstrap/templates/env.sh.ecr +++ b/src/mstrap/templates/env.sh.ecr @@ -10,4 +10,6 @@ export MSTRAP_RC_DIR="<%= MStrap::Paths::RC_DIR %>" test -d <%= MStrap::Paths::HOMEBREW_PREFIX %> && eval $(<%= MStrap::Paths::HOMEBREW_PREFIX %>/bin/brew shellenv) <% end -%> +<%- runtime_managers.each do |runtime_manager| %> <%= runtime_manager.shell_activation(shell_name) %> +<% end -%> diff --git a/src/mstrap/templates/env_sh.cr b/src/mstrap/templates/env_sh.cr index 158b16e..f74fcf6 100644 --- a/src/mstrap/templates/env_sh.cr +++ b/src/mstrap/templates/env_sh.cr @@ -5,9 +5,9 @@ module MStrap ECR.def_to_s "#{__DIR__}/env.sh.ecr" getter :shell_name - getter :runtime_manager + getter :runtime_managers - def initialize(@shell_name : String, @runtime_manager : RuntimeManager) + def initialize(@shell_name : String, @runtime_managers : Array(RuntimeManager)) end def needs_homebrew_shellenv? From 82df3467a6d3be76b5a40a6e1a4166e0e886ca04 Mon Sep 17 00:00:00 2001 From: Max Fierke Date: Mon, 12 Feb 2024 22:53:13 -0600 Subject: [PATCH 4/5] 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 From f359d18fdead067a817b5d86e1708907054aa701 Mon Sep 17 00:00:00 2001 From: Max Fierke Date: Mon, 12 Feb 2024 23:06:04 -0600 Subject: [PATCH 5/5] Satisfy the ameba --- src/mstrap/cli.cr | 2 +- src/mstrap/steps/projects_step.cr | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mstrap/cli.cr b/src/mstrap/cli.cr index 96fb574..31aacdc 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) - Bootstrapper.for(config, project).each { |bs| bs.bootstrap(project) } + Bootstrapper.for(config, project).each(&.bootstrap(project)) end end diff --git a/src/mstrap/steps/projects_step.cr b/src/mstrap/steps/projects_step.cr index f2fa04e..259080e 100644 --- a/src/mstrap/steps/projects_step.cr +++ b/src/mstrap/steps/projects_step.cr @@ -38,7 +38,7 @@ module MStrap end logn "--> Bootstrapping: " - Bootstrapper.for(config, project).each { |bs| bs.bootstrap(project) } + Bootstrapper.for(config, project).each(&.bootstrap(project)) success "Finished bootstrapping #{project.name}" end