From 42fc1bebdfcae748085165114900e51551b2287d Mon Sep 17 00:00:00 2001 From: Max Fierke Date: Sun, 28 Apr 2024 14:41:31 -0500 Subject: [PATCH] Add Platform#has_command? and Platform#run_command For has_command?, we can cache output & use Process.find_executable instead of calling a bunch of shell subprocesses for a bit of speed up --- src/mstrap/dsl/system.cr | 85 +++----------------- src/mstrap/platform.cr | 102 +++++++++++++++++++++++- src/mstrap/supports/mise_installer.cr | 4 +- src/mstrap/supports/mkcert.cr | 2 +- src/mstrap/supports/rustup_installer.cr | 2 +- 5 files changed, 115 insertions(+), 80 deletions(-) diff --git a/src/mstrap/dsl/system.cr b/src/mstrap/dsl/system.cr index 6066175..6469ec7 100644 --- a/src/mstrap/dsl/system.cr +++ b/src/mstrap/dsl/system.cr @@ -1,83 +1,24 @@ module MStrap module DSL module System - # Executes a given command and waits for it to complete, returning whether - # the exit status indicated success. - # - # By default the process is configured with input, output, and error of - # the `mstrap` process. - # - # * _env_: optionally specifies the environment for the command - # * _command_: specifies the command to run. Arguments are allowed here, if - # _args_ are omitted and will be evaluated by the system shell. - # * _args_: optionally specifies arguments for the command. These will not - # be processed by the shell. - # * _shell_: specifies whether to run the command through the system shell - # * _input_: specifies where to direct STDIN on the spawned process - # * _output_: specifies where to direct STDOUT on the spawned process - # * _error_: specifies where to direct STDERR on the spawned process - # * _quiet_: If passed as `true`, it does no logging. If `mstrap` is - # running in debug mode, process output is always logged. - # * _sudo_: specifies whether to run the command with superuser privileges + # See `MStrap::Platform#has_command?` + def has_command?(command_name : String, **kwargs) : Bool + MStrap::Platform.has_command?(command_name, **kwargs) + end + + # See `MStrap::Platform#run_command` def cmd( env : Hash?, command : String, args : Array(String)?, - shell = true, - input = Process::Redirect::Inherit, - output = Process::Redirect::Inherit, - error = Process::Redirect::Inherit, - quiet = false, - sudo = false + **kwargs ) - if sudo - if args - args.unshift(command) - command = "sudo" - else - command = "sudo #{command}" - end - end - - logd "+ #{env ? env : ""} #{command} #{args ? args.join(" ") : ""}" - - named = { - shell: shell, - env: env, - input: input, - output: output, - error: error, - } - - if MStrap.debug? - named = named.merge({ - input: Process::Redirect::Inherit, - output: Process::Redirect::Inherit, - error: Process::Redirect::Inherit, - }) - elsif quiet - named = named.merge({ - input: Process::Redirect::Close, - output: Process::Redirect::Close, - error: Process::Redirect::Close, - }) - end - - child = Process.new(command, args, **named) - - at_exit { - # Cleanup this process when we exit, if it's still running. (e.g. receiving SIGINT) - unless child.terminated? - # Reap the whole process group, otherwise nested processes may live - # to print output another day - pgid = Process.pgid(child.pid) - Process.signal(Signal::TERM, -pgid) - child.wait - end - } - - status = child.wait - status.success? + MStrap::Platform.run_command( + env, + command, + args, + **kwargs + ) end # :nodoc: diff --git a/src/mstrap/platform.cr b/src/mstrap/platform.cr index 93325ba..33cc099 100644 --- a/src/mstrap/platform.cr +++ b/src/mstrap/platform.cr @@ -8,14 +8,108 @@ module MStrap {{ raise "Unsupported platform" }} {% end %} + @@found_commands = Hash(String, String).new + # Indicates whether the host platform has Git installed def self.has_git? - ENV["MSTRAP_IGNORE_GIT"]? != "true" && (`command -v git` && $?.success?) + ENV["MSTRAP_IGNORE_GIT"]? != "true" && has_command?("git") + end + + # Indicates whether the host platform has a given command available + # + # Lookups are cached by default, but cached info can be skipped by passing + # `skip_cache: true` + def self.has_command?(command_name : String, skip_cache : Bool = false) : Bool + if (cmd_path = @@found_commands[command_name]?) && !skip_cache + true + elsif cmd_path = Process.find_executable(command_name) + @@found_commands[command_name] = cmd_path + true + else + @@found_commands.delete(command_name) + false + end end - # Indicates whether the host platform has GPG installed - def self.has_gpg? - !!(`command -v gpg` && $?.success?) + # Executes a given command and waits for it to complete, returning whether + # the exit status indicated success. + # + # By default the process is configured with input, output, and error of + # the `mstrap` process. + # + # * _env_: optionally specifies the environment for the command + # * _command_: specifies the command to run. Arguments are allowed here, if + # _args_ are omitted and will be evaluated by the system shell. + # * _args_: optionally specifies arguments for the command. These will not + # be processed by the shell. + # * _shell_: specifies whether to run the command through the system shell + # * _input_: specifies where to direct STDIN on the spawned process + # * _output_: specifies where to direct STDOUT on the spawned process + # * _error_: specifies where to direct STDERR on the spawned process + # * _quiet_: If passed as `true`, it does no logging. If `mstrap` is + # running in debug mode, process output is always logged. + # * _sudo_: specifies whether to run the command with superuser privileges + def self.run_command( + env : Hash?, + command : String, + args : Array(String)?, + shell = true, + input = Process::Redirect::Inherit, + output = Process::Redirect::Inherit, + error = Process::Redirect::Inherit, + quiet = false, + sudo = false + ) + if sudo + if args + args.unshift(command) + command = "sudo" + else + command = "sudo #{command}" + end + end + + Log.debug { "+ #{env ? env : ""} #{command} #{args ? args.join(" ") : ""}" } + + named = { + shell: shell, + env: env, + input: input, + output: output, + error: error, + } + + if MStrap.debug? + named = named.merge({ + input: Process::Redirect::Inherit, + output: Process::Redirect::Inherit, + error: Process::Redirect::Inherit, + }) + elsif quiet + named = named.merge({ + input: Process::Redirect::Close, + output: Process::Redirect::Close, + error: Process::Redirect::Close, + }) + end + + child = Process.new(command, args, **named) + + # TODO: Refactor this into something less hacky + # (e.g. push to a Deque used by a Process.on_terminate handler or something) + at_exit { + # Cleanup this process when we exit, if it's still running. (e.g. receiving SIGINT) + unless child.terminated? + # Reap the whole process group, otherwise nested processes may live + # to print output another day + pgid = Process.pgid(child.pid) + Process.signal(Signal::TERM, -pgid) + child.wait + end + } + + status = child.wait + status.success? end # Installs a list of packages using the platform's package manager diff --git a/src/mstrap/supports/mise_installer.cr b/src/mstrap/supports/mise_installer.cr index 770c051..601fd9e 100644 --- a/src/mstrap/supports/mise_installer.cr +++ b/src/mstrap/supports/mise_installer.cr @@ -52,7 +52,7 @@ module MStrap end def installed? - File.exists?(MISE_INSTALL_PATH) && (`command -v mise` && $?.success?) + File.exists?(MISE_INSTALL_PATH) && has_command?("mise") end private def fetch_installer! @@ -82,7 +82,7 @@ module MStrap end private def verify_installer? - @verify_installer ||= MStrap::Platform.has_gpg? + has_command?("gpg") end end end diff --git a/src/mstrap/supports/mkcert.cr b/src/mstrap/supports/mkcert.cr index fcb0db5..69b9c3e 100644 --- a/src/mstrap/supports/mkcert.cr +++ b/src/mstrap/supports/mkcert.cr @@ -5,7 +5,7 @@ module MStrap # Returns whether mkcert is installed def installed? - cmd("command -v mkcert", quiet: true) + has_command?("mkcert") end # Runs mkcert install process to add CARoot, etc. diff --git a/src/mstrap/supports/rustup_installer.cr b/src/mstrap/supports/rustup_installer.cr index 478d365..ad3f7c6 100644 --- a/src/mstrap/supports/rustup_installer.cr +++ b/src/mstrap/supports/rustup_installer.cr @@ -36,7 +36,7 @@ module MStrap end def installed? - `command -v rustup` && $?.success? + has_command?("rustup") end private def fetch_installer!