Allow installing casks with brew install.

This commit is contained in:
Markus Reiter 2020-09-25 21:13:26 +02:00
parent 9fbe13c17d
commit f54b458cda
8 changed files with 288 additions and 120 deletions

View File

@ -1,5 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cask/config"
require "search" require "search"
module Cask module Cask

View File

@ -26,20 +26,41 @@ module Cask
end end
def run def run
require "cask/installer" self.class.install_casks(
*casks,
options = {
binaries: args.binaries?, binaries: args.binaries?,
verbose: args.verbose?, verbose: args.verbose?,
force: args.force?, force: args.force?,
skip_cask_deps: args.skip_cask_deps?, skip_cask_deps: args.skip_cask_deps?,
require_sha: args.require_sha?, require_sha: args.require_sha?,
quarantine: args.quarantine?, quarantine: args.quarantine?,
)
end
def self.install_casks(
*casks,
verbose: nil,
force: nil,
binaries: nil,
skip_cask_deps: nil,
require_sha: nil,
quarantine: nil
)
odie "Installing casks is supported only on macOS" unless OS.mac?
options = {
verbose: verbose,
force: force,
binaries: binaries,
skip_cask_deps: skip_cask_deps,
require_sha: require_sha,
quarantine: quarantine,
}.compact }.compact
options[:quarantine] = true if options[:quarantine].nil? options[:quarantine] = true if options[:quarantine].nil?
odie "Installing casks is supported only on macOS" unless OS.mac? require "cask/installer"
casks.each do |cask| casks.each do |cask|
Installer.new(cask, **options).install Installer.new(cask, **options).install
rescue CaskAlreadyInstalledError => e rescue CaskAlreadyInstalledError => e

View File

@ -24,9 +24,9 @@ module Cask
def self.reinstall_casks( def self.reinstall_casks(
*casks, *casks,
verbose: false, verbose: nil,
force: false, force: nil,
skip_cask_deps: false, skip_cask_deps: nil,
binaries: nil, binaries: nil,
require_sha: nil, require_sha: nil,
quarantine: nil quarantine: nil

View File

@ -21,34 +21,47 @@ module Homebrew
end end
def to_formulae def to_formulae
@to_formulae ||= (downcased_unique_named - homebrew_tap_cask_names).map do |name| @to_formulae ||= to_formulae_and_casks.select { |o| o.is_a?(Formula) }.freeze
Formulary.factory(name, spec, force_bottle: @force_bottle, flags: @flags)
end.uniq(&:name).freeze
end end
def to_formulae_and_casks def to_formulae_and_casks(only: nil)
@to_formulae_and_casks ||= begin @to_formulae_and_casks ||= {}
formulae_and_casks = [] @to_formulae_and_casks[only] ||= begin
to_objects(only: only).reject { |o| o.is_a?(Tap) }.freeze
end
end
downcased_unique_named.each do |name| def load_formula_or_cask(name, only: nil)
formulae_and_casks << Formulary.factory(name, spec) if only != :cask
warn_if_cask_conflicts(name, "formula")
rescue FormulaUnavailableError
begin begin
formulae_and_casks << Cask::CaskLoader.load(name) formula = Formulary.factory(name, spec, force_bottle: @force_bottle, flags: @flags)
rescue Cask::CaskUnavailableError warn_if_cask_conflicts(name, "formula") unless only == :formula
raise "No available formula or cask with the name \"#{name}\"" return formula
rescue FormulaUnavailableError => e
raise e if only == :formula
end end
end end
formulae_and_casks.freeze if only != :formula
begin
return Cask::CaskLoader.load(name)
rescue Cask::CaskUnavailableError
raise e if only == :cask
end end
end end
raise FormulaOrCaskUnavailableError, name
end
private :load_formula_or_cask
def resolve_formula(name)
Formulary.resolve(name, spec: spec(nil), force_bottle: @force_bottle, flags: @flags)
end
private :resolve_formula
def to_resolved_formulae def to_resolved_formulae
@to_resolved_formulae ||= (downcased_unique_named - homebrew_tap_cask_names).map do |name| @to_resolved_formulae ||= (downcased_unique_named - homebrew_tap_cask_names).map do |name|
Formulary.resolve(name, spec: spec(nil), force_bottle: @force_bottle, flags: @flags) resolve_formula(name)
end.uniq(&:name).freeze end.uniq(&:name).freeze
end end
@ -58,7 +71,7 @@ module Homebrew
casks = [] casks = []
downcased_unique_named.each do |name| downcased_unique_named.each do |name|
resolved_formulae << Formulary.resolve(name, spec: spec(nil), force_bottle: @force_bottle, flags: @flags) resolved_formulae << resolve_formula(name)
warn_if_cask_conflicts(name, "formula") warn_if_cask_conflicts(name, "formula")
rescue FormulaUnavailableError rescue FormulaUnavailableError
@ -73,6 +86,18 @@ module Homebrew
end end
end end
# Convert named arguments to `Tap`, `Formula` or `Cask` objects.
# If both a formula and cask exist with the same name, returns the
# formula and prints a warning unless `only` is specified.
def to_objects(only: nil)
@to_objects ||= {}
@to_objects[only] ||= downcased_unique_named.flat_map do |name|
next Tap.fetch(name) if only == :tap || (only.nil? && name.count("/") == 1 && !name.start_with?("./", "/"))
load_formula_or_cask(name, only: only)
end.uniq.freeze
end
def to_formulae_paths def to_formulae_paths
to_paths(only: :formulae) to_paths(only: :formulae)
end end

View File

@ -1,5 +1,6 @@
# frozen_string_literal: true # frozen_string_literal: true
require "cask/config"
require "missing_formula" require "missing_formula"
require "formula_installer" require "formula_installer"
require "development_tools" require "development_tools"
@ -15,6 +16,111 @@ module Homebrew
module_function module_function
def install_args def install_args
cask_only_options = {
["--cask", "--casks"] => {
description: "Treat all named arguments as casks.",
},
["--[no-]binaries"] => {
description: "Disable/enable linking of helper executables to `#{Cask::Config.global.binarydir}`. " \
"Default: enabled",
env: :cask_opts_binaries,
},
["--require-sha"] => {
description: "Require all casks to have a checksum.",
env: :cask_opts_require_sha,
},
["--[no-]quarantine"] => {
description: "Disable/enable quarantining of downloads. Default: enabled",
env: :cask_opts_quarantine,
},
["--skip-cask-deps"] => {
description: "Skip installing cask dependencies.",
},
}.freeze
formula_only_options = {
["--formula", "--formulae"] => {
description: "Treat all named arguments as formulae.",
},
["--env="] => {
description: "If `std` is passed, use the standard build environment instead of superenv. "\
"If `super` is passed, use superenv even if the formula specifies the "\
"standard build environment.",
},
["--ignore-dependencies"] => {
description: "An unsupported Homebrew development flag to skip installing any dependencies of "\
"any kind. If the dependencies are not already present, the formula will have issues. "\
"If you're not developing Homebrew, consider adjusting your PATH rather than "\
"using this flag.",
},
["--only-dependencies"] => {
description: "Install the dependencies with specified options but do not install the "\
"formula itself.",
},
["--cc="] => {
description: "Attempt to compile using the specified <compiler>, which should be the "\
"name of the compiler's executable, e.g. `gcc-7` for GCC 7. "\
"In order to use LLVM's clang, specify `llvm_clang`. To use the "\
"Apple-provided clang, specify `clang`. This option will only accept "\
"compilers that are provided by Homebrew or bundled with macOS. "\
"Please do not file issues if you encounter errors while using this option.",
},
["-s", "--build-from-source"] => {
description: "Compile <formula> from source even if a bottle is provided. "\
"Dependencies will still be installed from bottles if they are available.",
},
["--force-bottle"] => {
description: "Install from a bottle if it exists for the current or newest version of "\
"macOS, even if it would not normally be used for installation.",
},
["--include-test"] => {
description: "Install testing dependencies required to run `brew test` <formula>.",
},
["--HEAD"] => {
description: "If <formula> defines it, install the HEAD version, aka. master, trunk, unstable.",
},
["--fetch-HEAD"] => {
description: "Fetch the upstream repository to detect if the HEAD installation of the "\
"formula is outdated. Otherwise, the repository's HEAD will only be checked for "\
"updates when a new stable or development version has been released.",
},
["--keep-tmp"] => {
description: "Retain the temporary files created during installation.",
},
["--build-bottle"] => {
description: "Prepare the formula for eventual bottling during installation, skipping any "\
"post-install steps.",
},
["--bottle-arch="] => {
depends_on: "--build-bottle",
description: "Optimise bottles for the specified architecture rather than the oldest "\
"architecture supported by the version of macOS the bottles are built on.",
},
["--display-times"] => {
env: :display_install_times,
description: "Print install times for each formula at the end of the run.",
},
["-i", "--interactive"] => {
description: "Download and patch <formula>, then open a shell. This allows the user to "\
"run `./configure --help` and otherwise determine how to turn the software "\
"package into a Homebrew package.",
},
["-g", "--git"] => {
description: "Create a Git repository, useful for creating patches to the software.",
},
}.freeze
Homebrew::CLI::Parser.new do Homebrew::CLI::Parser.new do
usage_banner <<~EOS usage_banner <<~EOS
`install` [<options>] <formula> `install` [<options>] <formula>
@ -27,65 +133,30 @@ module Homebrew
switch "-d", "--debug", switch "-d", "--debug",
description: "If brewing fails, open an interactive debugging session with access to IRB "\ description: "If brewing fails, open an interactive debugging session with access to IRB "\
"or a shell inside the temporary build directory." "or a shell inside the temporary build directory."
flag "--env=",
description: "If `std` is passed, use the standard build environment instead of superenv. "\
"If `super` is passed, use superenv even if the formula specifies the "\
"standard build environment."
switch "--ignore-dependencies",
description: "An unsupported Homebrew development flag to skip installing any dependencies of "\
"any kind. If the dependencies are not already present, the formula will have issues. "\
"If you're not developing Homebrew, consider adjusting your PATH rather than "\
"using this flag."
switch "--only-dependencies",
description: "Install the dependencies with specified options but do not install the "\
"formula itself."
flag "--cc=",
description: "Attempt to compile using the specified <compiler>, which should be the "\
"name of the compiler's executable, e.g. `gcc-7` for GCC 7. "\
"In order to use LLVM's clang, specify `llvm_clang`. To use the "\
"Apple-provided clang, specify `clang`. This option will only accept "\
"compilers that are provided by Homebrew or bundled with macOS. "\
"Please do not file issues if you encounter errors while using this option."
switch "-s", "--build-from-source",
description: "Compile <formula> from source even if a bottle is provided. "\
"Dependencies will still be installed from bottles if they are available."
switch "--force-bottle",
description: "Install from a bottle if it exists for the current or newest version of "\
"macOS, even if it would not normally be used for installation."
switch "--include-test",
description: "Install testing dependencies required to run `brew test` <formula>."
switch "--HEAD",
description: "If <formula> defines it, install the HEAD version, aka. master, trunk, unstable."
switch "--fetch-HEAD",
description: "Fetch the upstream repository to detect if the HEAD installation of the "\
"formula is outdated. Otherwise, the repository's HEAD will only be checked for "\
"updates when a new stable or development version has been released."
switch "--keep-tmp",
description: "Retain the temporary files created during installation."
switch "--build-bottle",
description: "Prepare the formula for eventual bottling during installation, skipping any "\
"post-install steps."
flag "--bottle-arch=",
depends_on: "--build-bottle",
description: "Optimise bottles for the specified architecture rather than the oldest "\
"architecture supported by the version of macOS the bottles are built on."
switch "-f", "--force", switch "-f", "--force",
description: "Install without checking for previously installed keg-only or "\ description: "Install without checking for previously installed keg-only or "\
"non-migrated versions." "non-migrated versions."
switch "-v", "--verbose", switch "-v", "--verbose",
description: "Print the verification and postinstall steps." description: "Print the verification and postinstall steps."
switch "--display-times",
env: :display_install_times,
description: "Print install times for each formula at the end of the run."
switch "-i", "--interactive",
description: "Download and patch <formula>, then open a shell. This allows the user to "\
"run `./configure --help` and otherwise determine how to turn the software "\
"package into a Homebrew package."
switch "-g", "--git",
description: "Create a Git repository, useful for creating patches to the software."
conflicts "--ignore-dependencies", "--only-dependencies" conflicts "--ignore-dependencies", "--only-dependencies"
conflicts "--devel", "--HEAD"
conflicts "--build-from-source", "--build-bottle", "--force-bottle" conflicts "--build-from-source", "--build-bottle", "--force-bottle"
formula_only_options.each do |flags, **options|
if flags.last.end_with?("=")
flag(*flags, **options)
else
switch(*flags, **options)
end
conflicts "--cask", flags.last
end
cask_only_options.each do |flags, **options|
switch(*flags, **options)
conflicts "--formula", flags.last
end
formula_options formula_options
min_named :formula min_named :formula
end end
@ -94,13 +165,15 @@ module Homebrew
def install def install
args = install_args.parse args = install_args.parse
args.named.each do |name| only = :formula if args.formula?
next if File.exist?(name) only = :cask if args.cask?
next if name !~ HOMEBREW_TAP_FORMULA_REGEX && name !~ HOMEBREW_CASK_TAP_CASK_REGEX
tap = Tap.fetch(Regexp.last_match(1), Regexp.last_match(2)) objects = args.named.to_objects(only: only)
tap.install unless tap.installed?
end taps, formulae_or_casks = objects.partition { |o| o.is_a?(Tap) }
taps = (taps + formulae_or_casks.map(&:tap).compact).uniq.sort_by(&:name)
taps.reject(&:installed?).each(&:install)
if args.ignore_dependencies? if args.ignore_dependencies?
opoo <<~EOS opoo <<~EOS
@ -111,25 +184,29 @@ module Homebrew
EOS EOS
end end
formulae = [] formulae, casks = formulae_or_casks.partition { |formula_or_cask| formula_or_cask.is_a?(Formula) }
unless args.named.homebrew_tap_cask_names.empty? if casks.any?
cask_args = [] require "cask/cmd/install"
cask_args << "--force" if args.force?
cask_args << "--debug" if args.debug?
cask_args << "--verbose" if args.verbose?
args.named.homebrew_tap_cask_names.each do |c| Cask::Cmd::Install.install_casks(
ohai "brew cask install #{c} #{cask_args.join " "}" *casks,
system("#{HOMEBREW_PREFIX}/bin/brew", "cask", "install", c, *cask_args) binaries: args.binaries?,
end verbose: args.verbose?,
force: args.force?,
skip_cask_deps: args.skip_cask_deps?,
require_sha: args.require_sha?,
quarantine: args.quarantine?,
)
end end
# if the user's flags will prevent bottle only-installations when no # if the user's flags will prevent bottle only-installations when no
# developer tools are available, we need to stop them early on # developer tools are available, we need to stop them early on
FormulaInstaller.prevent_build_flags(args) FormulaInstaller.prevent_build_flags(args)
args.named.to_formulae.each do |f| installed_formulae = []
formulae.each do |f|
# head-only without --HEAD is an error # head-only without --HEAD is an error
if !args.HEAD? && f.stable.nil? if !args.HEAD? && f.stable.nil?
raise <<~EOS raise <<~EOS
@ -160,7 +237,7 @@ module Homebrew
To upgrade to #{f.version}, run `brew upgrade #{f.full_name}` To upgrade to #{f.version}, run `brew upgrade #{f.full_name}`
EOS EOS
elsif args.only_dependencies? elsif args.only_dependencies?
formulae << f installed_formulae << f
else else
opoo <<~EOS opoo <<~EOS
#{f.full_name} #{f.pkg_version} is already installed and up-to-date #{f.full_name} #{f.pkg_version} is already installed and up-to-date
@ -193,7 +270,7 @@ module Homebrew
EOS EOS
elsif args.only_dependencies? elsif args.only_dependencies?
msg = nil msg = nil
formulae << f installed_formulae << f
else else
msg = <<~EOS msg = <<~EOS
#{msg} and up-to-date #{msg} and up-to-date
@ -221,7 +298,7 @@ module Homebrew
else else
# If none of the above is true and the formula is linked, then # If none of the above is true and the formula is linked, then
# FormulaInstaller will handle this case. # FormulaInstaller will handle this case.
formulae << f installed_formulae << f
end end
# Even if we don't install this formula mark it as no longer just # Even if we don't install this formula mark it as no longer just
@ -236,11 +313,11 @@ module Homebrew
end end
end end
return if formulae.empty? return if installed_formulae.empty?
Install.perform_preinstall_checks(cc: args.cc) Install.perform_preinstall_checks(cc: args.cc)
formulae.each do |f| installed_formulae.each do |f|
Migrator.migrate_if_needed(f, force: args.force?) Migrator.migrate_if_needed(f, force: args.force?)
install_formula(f, args: args) install_formula(f, args: args)
Cleanup.install_formula_clean!(f) Cleanup.install_formula_clean!(f)
@ -256,7 +333,7 @@ module Homebrew
# formula was found, but there's a problem with its implementation). # formula was found, but there's a problem with its implementation).
$stderr.puts e.backtrace if Homebrew::EnvConfig.developer? $stderr.puts e.backtrace if Homebrew::EnvConfig.developer?
ofail e.message ofail e.message
rescue FormulaUnavailableError => e rescue FormulaOrCaskUnavailableError => e
if e.name == "updog" if e.name == "updog"
ofail "What's updog?" ofail "What's updog?"
return return

View File

@ -68,17 +68,25 @@ class MethodDeprecatedError < StandardError
attr_accessor :issues_url attr_accessor :issues_url
end end
# Raised when a formula is not available. # Raised when neither a formula nor a cask with the given name is available.
class FormulaUnavailableError < RuntimeError class FormulaOrCaskUnavailableError < RuntimeError
attr_reader :name attr_reader :name
attr_accessor :dependent
def initialize(name) def initialize(name)
super super()
@name = name @name = name
end end
def to_s
"No available formula or cask with the name \"#{name}\"."
end
end
# Raised when a formula is not available.
class FormulaUnavailableError < FormulaOrCaskUnavailableError
attr_accessor :dependent
def dependent_s def dependent_s
"(dependency of #{dependent})" if dependent && dependent != name "(dependency of #{dependent})" if dependent && dependent != name
end end

View File

@ -313,6 +313,12 @@ installed formulae or, every 30 days, for all formulae.
* `-d`, `--debug`: * `-d`, `--debug`:
If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory. If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory.
* `-f`, `--force`:
Install without checking for previously installed keg-only or non-migrated versions.
* `-v`, `--verbose`:
Print the verification and postinstall steps.
* `--formula`:
Treat all named arguments as formulae.
* `--env`: * `--env`:
If `std` is passed, use the standard build environment instead of superenv. If `super` is passed, use superenv even if the formula specifies the standard build environment. If `std` is passed, use the standard build environment instead of superenv. If `super` is passed, use superenv even if the formula specifies the standard build environment.
* `--ignore-dependencies`: * `--ignore-dependencies`:
@ -337,16 +343,22 @@ installed formulae or, every 30 days, for all formulae.
Prepare the formula for eventual bottling during installation, skipping any post-install steps. Prepare the formula for eventual bottling during installation, skipping any post-install steps.
* `--bottle-arch`: * `--bottle-arch`:
Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on. Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on.
* `-f`, `--force`:
Install without checking for previously installed keg-only or non-migrated versions.
* `-v`, `--verbose`:
Print the verification and postinstall steps.
* `--display-times`: * `--display-times`:
Print install times for each formula at the end of the run. Print install times for each formula at the end of the run.
* `-i`, `--interactive`: * `-i`, `--interactive`:
Download and patch *`formula`*, then open a shell. This allows the user to run `./configure --help` and otherwise determine how to turn the software package into a Homebrew package. Download and patch *`formula`*, then open a shell. This allows the user to run `./configure --help` and otherwise determine how to turn the software package into a Homebrew package.
* `-g`, `--git`: * `-g`, `--git`:
Create a Git repository, useful for creating patches to the software. Create a Git repository, useful for creating patches to the software.
* `--cask`:
Treat all named arguments as casks.
* `--[no-]binaries`:
Disable/enable linking of helper executables to `/usr/local/bin`. Default: enabled
* `--require-sha`:
Require all casks to have a checksum.
* `--[no-]quarantine`:
Disable/enable quarantining of downloads. Default: enabled
* `--skip-cask-deps`:
Skip installing cask dependencies.
### `leaves` ### `leaves`

View File

@ -456,6 +456,18 @@ Unless \fBHOMEBREW_NO_INSTALL_CLEANUP\fR is set, \fBbrew cleanup\fR will then be
If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory\. If brewing fails, open an interactive debugging session with access to IRB or a shell inside the temporary build directory\.
. .
.TP .TP
\fB\-f\fR, \fB\-\-force\fR
Install without checking for previously installed keg\-only or non\-migrated versions\.
.
.TP
\fB\-v\fR, \fB\-\-verbose\fR
Print the verification and postinstall steps\.
.
.TP
\fB\-\-formula\fR
Treat all named arguments as formulae\.
.
.TP
\fB\-\-env\fR \fB\-\-env\fR
If \fBstd\fR is passed, use the standard build environment instead of superenv\. If \fBsuper\fR is passed, use superenv even if the formula specifies the standard build environment\. If \fBstd\fR is passed, use the standard build environment instead of superenv\. If \fBsuper\fR is passed, use superenv even if the formula specifies the standard build environment\.
. .
@ -504,14 +516,6 @@ Prepare the formula for eventual bottling during installation, skipping any post
Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on\. Optimise bottles for the specified architecture rather than the oldest architecture supported by the version of macOS the bottles are built on\.
. .
.TP .TP
\fB\-f\fR, \fB\-\-force\fR
Install without checking for previously installed keg\-only or non\-migrated versions\.
.
.TP
\fB\-v\fR, \fB\-\-verbose\fR
Print the verification and postinstall steps\.
.
.TP
\fB\-\-display\-times\fR \fB\-\-display\-times\fR
Print install times for each formula at the end of the run\. Print install times for each formula at the end of the run\.
. .
@ -523,6 +527,26 @@ Download and patch \fIformula\fR, then open a shell\. This allows the user to ru
\fB\-g\fR, \fB\-\-git\fR \fB\-g\fR, \fB\-\-git\fR
Create a Git repository, useful for creating patches to the software\. Create a Git repository, useful for creating patches to the software\.
. .
.TP
\fB\-\-cask\fR
Treat all named arguments as casks\.
.
.TP
\fB\-\-[no\-]binaries\fR
Disable/enable linking of helper executables to \fB/usr/local/bin\fR\. Default: enabled
.
.TP
\fB\-\-require\-sha\fR
Require all casks to have a checksum\.
.
.TP
\fB\-\-[no\-]quarantine\fR
Disable/enable quarantining of downloads\. Default: enabled
.
.TP
\fB\-\-skip\-cask\-deps\fR
Skip installing cask dependencies\.
.
.SS "\fBleaves\fR" .SS "\fBleaves\fR"
List installed formulae that are not dependencies of another installed formula\. List installed formulae that are not dependencies of another installed formula\.
. .