diff --git a/README.md b/README.md index 3355afc28..46a23c87f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Homebrew Bundle -Bundler for non-Ruby dependencies from Homebrew, Homebrew Cask, Mac App Store, Whalebrew and Visual Studio Code. +Bundler for non-Ruby dependencies from Homebrew, Homebrew Cask, Mac App Store, Whalebrew, Visual Studio Code, and TeX Live package manager. ## Requirements @@ -14,6 +14,8 @@ Bundler for non-Ruby dependencies from Homebrew, Homebrew Cask, Mac App Store, W [Visual Studio Code](https://code.visualstudio.com/) is optional and used for installing Visual Studio Code extensions. +[TeX Live package manager](https://tug.org/texlive/tlmgr.html) is optional and used for installing TeX Live packages. It is provided via [basictex](https://formulae.brew.sh/cask/basictex) for example but must be provided by you as a user. Homebrew Bundle will make no attempt at installing the TeX Live package manager for you. + ## Installation `brew bundle` is automatically installed when first run. @@ -64,6 +66,10 @@ whalebrew "whalebrew/wget" # 'vscode --install-extension' vscode "GitHub.codespaces" + +# 'sudo tlmgr install' +tlmgr "bibtex" +tlmgr "bibtex", args: [ ] ``` ## Versions and lockfiles diff --git a/cmd/bundle.rb b/cmd/bundle.rb index dd49dee2b..2bc911482 100755 --- a/cmd/bundle.rb +++ b/cmd/bundle.rb @@ -79,6 +79,8 @@ def bundle_args description: "`list` Whalebrew dependencies." switch "--vscode", description: "`list` VSCode extensions." + switch "--tlmgr", + description: "`list` TeX Live packages." switch "--describe", env: :bundle_dump_describe, description: "`dump` adds a description comment above each line, unless the " \ @@ -137,6 +139,7 @@ def bundle mas: args.mas?, whalebrew: args.whalebrew?, vscode: args.vscode?, + tlmgr: args.tlmgr?, ) when "cleanup" Bundle::Commands::Cleanup.run( @@ -169,6 +172,7 @@ def bundle mas: args.mas?, whalebrew: args.whalebrew?, vscode: args.vscode?, + tlmgr: args.tlmgr?, brews: args.brews?, ) else diff --git a/lib/bundle.rb b/lib/bundle.rb index 17f249819..b6648ee8a 100644 --- a/lib/bundle.rb +++ b/lib/bundle.rb @@ -36,3 +36,6 @@ require "bundle/vscode_extension_checker" require "bundle/vscode_extension_dumper" require "bundle/vscode_extension_installer" +require "bundle/tlmgr_package_checker" +require "bundle/tlmgr_package_dumper" +require "bundle/tlmgr_package_installer" diff --git a/lib/bundle/bundle.rb b/lib/bundle/bundle.rb index ffb0b6f90..e0c15dd3c 100644 --- a/lib/bundle/bundle.rb +++ b/lib/bundle/bundle.rb @@ -29,6 +29,10 @@ def vscode_installed? @vscode_installed ||= which("code").present? end + def tlmgr_installed? + @tlmgr_installed ||= which("tlmgr").present? + end + def whalebrew_installed? @whalebrew_installed ||= which_formula("whalebrew") end diff --git a/lib/bundle/checker.rb b/lib/bundle/checker.rb index 44859940f..701b375ea 100644 --- a/lib/bundle/checker.rb +++ b/lib/bundle/checker.rb @@ -61,6 +61,7 @@ def find_actionable(entries, exit_on_first_error: false, no_upgrade: false, verb taps_to_tap: "Taps", casks_to_install: "Casks", extensions_to_install: "VSCode Extensions", + packages_to_install: "tlmgr Packages", apps_to_install: "Apps", formulae_to_install: "Formulae", formulae_to_start: "Services", @@ -122,6 +123,13 @@ def extensions_to_install(exit_on_first_error: false, no_upgrade: false, verbose ) end + def packages_to_install(exit_on_first_error: false, no_upgrade: false, verbose: false) + Bundle::Checker::TlmgrPackageChecker.new.find_actionable( + @dsl.entries, + exit_on_first_error:, no_upgrade:, verbose:, + ) + end + def formulae_to_start(exit_on_first_error: false, no_upgrade: false, verbose: false) Bundle::Checker::BrewServiceChecker.new.find_actionable( @dsl.entries, @@ -134,6 +142,7 @@ def reset! Bundle::CaskDumper.reset! Bundle::BrewDumper.reset! Bundle::MacAppStoreDumper.reset! + Bundle::TlmgrPackageDumper.reset! Bundle::TapDumper.reset! Bundle::BrewServices.reset! end diff --git a/lib/bundle/commands/cleanup.rb b/lib/bundle/commands/cleanup.rb index 2f2974247..cfa921176 100644 --- a/lib/bundle/commands/cleanup.rb +++ b/lib/bundle/commands/cleanup.rb @@ -15,6 +15,7 @@ def reset! Bundle::BrewDumper.reset! Bundle::TapDumper.reset! Bundle::VscodeExtensionDumper.reset! + Bundle::TlmgrPackageDumper.reset! Bundle::BrewServices.reset! end @@ -23,6 +24,7 @@ def run(global: false, file: nil, force: false, zap: false) formulae = formulae_to_uninstall(global:, file:) taps = taps_to_untap(global:, file:) vscode_extensions = vscode_extensions_to_uninstall(global:, file:) + tlmgr_packages = tlmgr_packages_to_uninstall(global:, file:) if force if casks.any? args = zap ? ["--zap"] : [] @@ -41,6 +43,12 @@ def run(global: false, file: nil, force: false, zap: false) Kernel.system "code", "--uninstall-extension", extension end + # TODO: See how we can get around the sudo requirement because of the way e.g. basictex installs itself + # See also https://tug.org/texlive/doc/tlmgr.html#USER-MODE + tlmgr_packages.each do |package| + Kernel.system "sudo", "tlmgr", "remove", package + end + cleanup = system_output_no_stderr(HOMEBREW_BREW_FILE, "cleanup") puts cleanup unless cleanup.empty? else @@ -64,6 +72,11 @@ def run(global: false, file: nil, force: false, zap: false) puts Formatter.columns vscode_extensions end + if tlmgr_packages.any? + puts "Would uninstall TeX Live packages:" + puts Formatter.columns tlmgr_packages + end + cleanup = system_output_no_stderr(HOMEBREW_BREW_FILE, "cleanup", "--dry-run") unless cleanup.empty? puts "Would `brew cleanup`:" @@ -154,6 +167,19 @@ def vscode_extensions_to_uninstall(global: false, file: nil) current_extensions - kept_extensions end + def tlmgr_packages_to_uninstall(global: false, file: nil) + @dsl ||= Bundle::Dsl.new(Brewfile.read(global:, file:)) + kept_packages = @dsl.entries.select { |e| e.type == :tlmgr }.map { |x| x.name.downcase } + + # To provide a graceful migration from `Brewfile`s that don't yet or + # don't want to use `tlmgr`: don't remove any extensions if we don't + # find any in the `Brewfile`. + return [].freeze if kept_packages.empty? + + current_extensions = Bundle::TlmgrPackageDumper.packages + current_extensions - kept_packages + end + def system_output_no_stderr(cmd, *args) IO.popen([cmd, *args], err: :close).read end diff --git a/lib/bundle/commands/list.rb b/lib/bundle/commands/list.rb index d24c3144e..e044bf1ca 100644 --- a/lib/bundle/commands/list.rb +++ b/lib/bundle/commands/list.rb @@ -6,11 +6,11 @@ module List module_function def run(global: false, file: nil, all: false, casks: false, taps: false, mas: false, whalebrew: false, - vscode: false, brews: false) + vscode: false, tlmgr: false, brews: false) parsed_entries = Bundle::Dsl.new(Brewfile.read(global:, file:)).entries Bundle::Lister.list( parsed_entries, - all:, casks:, taps:, mas:, whalebrew:, vscode:, brews:, + all:, casks:, taps:, mas:, whalebrew:, vscode:, tlmgr:, brews:, ) end end diff --git a/lib/bundle/dsl.rb b/lib/bundle/dsl.rb index 9cc190ab1..e409c42cd 100644 --- a/lib/bundle/dsl.rb +++ b/lib/bundle/dsl.rb @@ -80,6 +80,13 @@ def vscode(name) @entries << Entry.new(:vscode, name) end + def tlmgr(name, options = {}) + raise "name(#{name.inspect}) should be a String object" unless name.is_a? String + raise "options(#{options.inspect}) should be a Hash object" unless options.is_a? Hash + + @entries << Entry.new(:tlmgr, name, options) + end + def tap(name, clone_target = nil, options = {}) raise "name(#{name.inspect}) should be a String object" unless name.is_a? String if clone_target && !clone_target.is_a?(String) diff --git a/lib/bundle/dumper.rb b/lib/bundle/dumper.rb index da6cebc6e..8f13a66ec 100644 --- a/lib/bundle/dumper.rb +++ b/lib/bundle/dumper.rb @@ -15,8 +15,8 @@ def can_write_to_brewfile?(brewfile_path, force: false) def build_brewfile(describe: false, no_restart: false, all: false, taps: false, brews: false, casks: false, - mas: false, whalebrew: false, vscode: false) - all ||= !(taps || brews || casks || mas || whalebrew || vscode) + mas: false, whalebrew: false, vscode: false, tlmgr: false) + all ||= !(taps || brews || casks || mas || whalebrew || vscode || tlmgr) content = [] content << TapDumper.dump if taps || all content << BrewDumper.dump(describe:, no_restart:) if brews || all @@ -24,17 +24,18 @@ def build_brewfile(describe: false, no_restart: false, content << MacAppStoreDumper.dump if mas || all content << WhalebrewDumper.dump if whalebrew || all content << VscodeExtensionDumper.dump if vscode || all + content << TlmgrPackageDumper.dump if tlmgr || all "#{content.reject(&:empty?).join("\n")}\n" end def dump_brewfile(global: false, file: nil, describe: false, force: false, no_restart: false, all: false, taps: false, brews: false, casks: false, - mas: false, whalebrew: false, vscode: false) + mas: false, whalebrew: false, vscode: false, tlmgr: false) path = brewfile_path(global:, file:) can_write_to_brewfile?(path, force:) content = build_brewfile(describe:, no_restart:, all:, taps:, brews:, casks:, - mas:, whalebrew:, vscode:) + mas:, whalebrew:, vscode:, tlmgr:) write_file path, content end diff --git a/lib/bundle/installer.rb b/lib/bundle/installer.rb index d50f1adb2..38da94e64 100644 --- a/lib/bundle/installer.rb +++ b/lib/bundle/installer.rb @@ -30,6 +30,8 @@ def install(entries, global: false, file: nil, no_lock: false, no_upgrade: false Bundle::WhalebrewInstaller when :vscode Bundle::VscodeExtensionInstaller + when :tlmgr + Bundle::TlmgrPackageInstaller when :tap verb = "Tapping" options = entry.options diff --git a/lib/bundle/lister.rb b/lib/bundle/lister.rb index 2769f777e..d6f7d1d02 100644 --- a/lib/bundle/lister.rb +++ b/lib/bundle/lister.rb @@ -5,9 +5,9 @@ module Lister module_function def list(entries, all: false, casks: false, taps: false, mas: false, whalebrew: false, - vscode: false, brews: false) + vscode: false, tlmgr: false, brews: false) entries.each do |entry| - if show?(entry.type, all:, casks:, taps:, mas:, whalebrew:, vscode:, + if show?(entry.type, all:, casks:, taps:, mas:, whalebrew:, vscode:, tlmgr:, brews:) puts entry.name end @@ -15,15 +15,16 @@ def list(entries, all: false, casks: false, taps: false, mas: false, whalebrew: end def self.show?(type, all: false, casks: false, taps: false, mas: false, whalebrew: false, - vscode: false, brews: false) + vscode: false, tlmgr: false, brews: false) return true if all return true if casks && type == :cask return true if taps && type == :tap return true if mas && type == :mas return true if whalebrew && type == :whalebrew return true if vscode && type == :vscode + return true if tlmgr && type == :tlmgr return true if brews && type == :brew - return true if type == :brew && !casks && !taps && !mas && !whalebrew && !vscode + return true if type == :brew && !casks && !taps && !mas && !whalebrew && !vscode && !tlmgr false end diff --git a/lib/bundle/tlmgr_package_checker.rb b/lib/bundle/tlmgr_package_checker.rb new file mode 100644 index 000000000..bc0725937 --- /dev/null +++ b/lib/bundle/tlmgr_package_checker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Bundle + module Checker + class TlmgrPackageChecker < Bundle::Checker::Base + PACKAGE_TYPE = :tlmgr + PACKAGE_TYPE_NAME = "TeX Live Package" + + def failure_reason(package, no_upgrade:) + "#{PACKAGE_TYPE_NAME} #{package} needs to be installed." + end + + def installed_and_up_to_date?(package, no_upgrade: false) + Bundle::TlmgrPackageInstaller.package_installed?(package) + end + end + end +end diff --git a/lib/bundle/tlmgr_package_dumper.rb b/lib/bundle/tlmgr_package_dumper.rb new file mode 100644 index 000000000..38e4d8cb4 --- /dev/null +++ b/lib/bundle/tlmgr_package_dumper.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Bundle + module TlmgrPackageDumper + module_function + + def reset! + @packages = nil + @outdated_packages = nil + end + + def packages + @packages ||= if Bundle.tlmgr_installed? + `tlmgr info --only-installed 2>/dev/null`.split("\n").map { |l| l.match(/i ([^:]+).*/)[1] }.map(&:downcase) + else + [] + end + end + + # TODO: See how we can get around the sudo requirement because of the way e.g. basictex installs itself + # See also https://tug.org/texlive/doc/tlmgr.html#USER-MODE + def outdated_packages + @outdated_packages ||= if Bundle.tlmgr_installed? + `sudo tlmgr update --all --dry-run 2>/dev/null`.split("\n") + .map { |l| l.match(/^update:\s+([^ ]+).*/)[1] } + .reject(&:empty?).map(&:downcase) + else + [] + end + end + + def dump + packages.map { |name| "tlmgr \"#{name}\"" }.join("\n") + end + end +end diff --git a/lib/bundle/tlmgr_package_installer.rb b/lib/bundle/tlmgr_package_installer.rb new file mode 100644 index 000000000..4297cd2cc --- /dev/null +++ b/lib/bundle/tlmgr_package_installer.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +module Bundle + class TlmgrPackageInstaller + def self.reset! + @installed_packages = nil + @outdated_packages = nil + end + + def self.preinstall(name, no_upgrade: false, verbose: false, **options) + new(name, options).preinstall(no_upgrade:, verbose:) + end + + def self.install(name, preinstall: true, no_upgrade: false, verbose: false, force: false, **options) + new(name, options).install(preinstall:, no_upgrade:, verbose:) + end + + def initialize(name, options = {}) + @name = name + @args = options.fetch(:args, []).map { |arg| "--#{arg}"} + @changed = nil + end + + def preinstall(no_upgrade: false, verbose: false) + raise "Unable to install #{name} TeX Live package. tlmgr is not installed. Provide it by installing one of 'basictex', 'mactex', or 'mactex-no-gui'" unless Bundle.tlmgr_installed? + + if installed? && (no_upgrade || !upgradable?) + puts "Skipping install of #{name} TeX Live package. It is already installed." if verbose + @changed = nil + return false + end + + true + end + + def install(preinstall: true, no_upgrade: false, verbose: false, force: false) + if preinstall + install_change_state!(no_upgrade:, verbose:, force:) + else + true + end + end + + def changed? + @changed.present? + end + + def self.package_installed_and_up_to_date?(package, no_upgrade: false) + return false unless package_installed?(package) + return true if no_upgrade + + !package_upgradable?(package) + end + + def self.package_in_array?(package, array) + return true if array.include?(package) + + resolved_name = Bundle::TlmgrPackageDumper.packages[package] + return false unless resolved_name + return true if array.include?(resolved_name) + + false + end + + def self.package_installed?(package) + package_in_array?(package, installed_packages) + end + + def self.package_upgradable?(package) + package_in_array?(package, upgradable_packages) && Formula[package].outdated? + end + + def self.installed_packages + @installed_packages ||= packages + end + + def self.upgradable_packages + outdated_packages + end + + def self.outdated_packages + @outdated_packages ||= Bundle::TlmgrPackageDumper.outdated_packages + end + + def self.packages + Bundle::TlmgrPackageDumper.packages + end + + def installed? + TlmgrPackageInstaller.package_installed?(@name) + end + + def upgradable? + TlmgrPackageInstaller.package_upgradable?(@name) + end + + def install!(verbose:, force:) + install_args = @args.dup + with_args = " with #{install_args.join(" ")}" if install_args.present? + puts "Installing #{name} TeX Live package. It is not currently installed." if verbose + # TODO: See how we can get around the sudo requirement because of the way e.g. basictex installs itself + # See also https://tug.org/texlive/doc/tlmgr.html#USER-MODE + unless Bundle.system("sudo", "tlmgr", "install", *install_args, @name, verbose: verbose) + @changed = nil + return false + end + + TlmgrPackageInstaller.installed_packages << @name + @changed = true + true + end + + def upgrade!(verbose:, force:) + puts "Upgrading #{@name} TeX Live package. It is installed but not up-to-date." if verbose + # TODO: See how we can get around the sudo requirement because of the way e.g. basictex installs itself + # See also https://tug.org/texlive/doc/tlmgr.html#USER-MODE + unless Bundle.system("sudo", "tlmgr", "update", @name, verbose:) + @changed = nil + return false + end + @changed = true + true + end + + def package_installed?(name) + installed_packages.include? name.downcase + end + + def installed_packages + @installed_packages ||= Bundle::TlmgrPackageDumper.extensions + end + end +end diff --git a/spec/bundle/commands/check_command_spec.rb b/spec/bundle/commands/check_command_spec.rb index 121041534..b8d780c1e 100644 --- a/spec/bundle/commands/check_command_spec.rb +++ b/spec/bundle/commands/check_command_spec.rb @@ -155,7 +155,7 @@ end end - context "when extension not installed" do + context "when VSCode extension not installed" do let(:expected_output) do <<~MSG brew bundle can't satisfy your Brewfile's dependencies. @@ -176,6 +176,27 @@ end end + context "when TeX Live package not installed" do + let(:expected_output) do + <<~MSG + brew bundle can't satisfy your Brewfile's dependencies. + → TeX Live Package foo needs to be installed. + Satisfy missing dependencies with `brew bundle install`. + MSG + end + let(:verbose) { true } + + before do + Bundle::Checker.reset! + allow(Bundle::Checker::TlmgrPackageChecker).to receive(:installed_and_up_to_date?).and_return(false) + end + + it "raises an error that doesn't mention upgrade" do + allow_any_instance_of(Pathname).to receive(:read).and_return("tlmgr 'foo'") + expect { do_check }.to raise_error(SystemExit).and output(expected_output).to_stdout + end + end + context "when there are taps to install" do before do allow_any_instance_of(Pathname).to receive(:read).and_return("") @@ -215,6 +236,23 @@ end end + context "when there are TeX Live packages to install" do + before do + allow_any_instance_of(Pathname).to receive(:read).and_return("") + allow(Bundle::Checker).to receive(:packages_to_install).and_return(["asdf"]) + end + + it "does not check for formulae" do + expect(Bundle::Checker).not_to receive(:formulae_to_install) + expect { do_check }.to raise_error(SystemExit) + end + + it "does not check for apps" do + expect(Bundle::Checker).not_to receive(:apps_to_install) + expect { do_check }.to raise_error(SystemExit) + end + end + context "when there are formulae to install" do before do allow_any_instance_of(Pathname).to receive(:read).and_return("") @@ -261,6 +299,14 @@ receive(:exit_early_check).once.and_call_original expect { do_check }.to raise_error(SystemExit) end + + it "stops checking after the first TeX Live package" do + allow_any_instance_of(Pathname).to receive(:read).and_return("tlmgr 'abc'\ntlmgr 'def'") + + expect_any_instance_of(Bundle::Checker::TlmgrPackageChecker).to \ + receive(:exit_early_check).once.and_call_original + expect { do_check }.to raise_error(SystemExit) + end end context "when a new checker fails to implement installed_and_up_to_date" do diff --git a/spec/bundle/commands/cleanup_command_spec.rb b/spec/bundle/commands/cleanup_command_spec.rb index f5ab585ac..9062c0a95 100644 --- a/spec/bundle/commands/cleanup_command_spec.rb +++ b/spec/bundle/commands/cleanup_command_spec.rb @@ -22,6 +22,7 @@ brew 'hasbuilddependency2' mas 'appstoreapp1', id: 1 vscode 'VsCodeExtension1' + tlmgr 'TeX-Package-1' EOS end @@ -81,6 +82,16 @@ allow(Bundle::VscodeExtensionDumper).to receive(:extensions).and_return(%w[z vscodeextension1]) expect(described_class.vscode_extensions_to_uninstall).to eql(%w[z]) end + + it "computes which TeX Live packages to uninstall" do + allow(Bundle::TlmgrPackageDumper).to receive(:packages).and_return(%w[z]) + expect(described_class.tlmgr_packages_to_uninstall).to eql(%w[z]) + end + + it "computes which TeX Live packages to uninstall irrespective of case of the extension name" do + allow(Bundle::TlmgrPackageDumper).to receive(:packages).and_return(%w[z tex-package-1]) + expect(described_class.tlmgr_packages_to_uninstall).to eql(%w[z]) + end end context "when there are no formulae to uninstall and no taps to untap" do @@ -89,7 +100,8 @@ allow(described_class).to receive_messages(casks_to_uninstall: [], formulae_to_uninstall: [], taps_to_untap: [], - vscode_extensions_to_uninstall: []) + vscode_extensions_to_uninstall: [], + tlmgr_packages_to_uninstall: [],) end it "does nothing" do @@ -105,7 +117,8 @@ allow(described_class).to receive_messages(casks_to_uninstall: %w[a b], formulae_to_uninstall: [], taps_to_untap: [], - vscode_extensions_to_uninstall: []) + vscode_extensions_to_uninstall: [], + tlmgr_packages_to_uninstall: []) end it "uninstalls casks" do @@ -121,7 +134,8 @@ allow(described_class).to receive_messages(casks_to_uninstall: %w[a b], formulae_to_uninstall: [], taps_to_untap: [], - vscode_extensions_to_uninstall: []) + vscode_extensions_to_uninstall: [], + tlmgr_packages_to_uninstall: []) end it "uninstalls casks" do @@ -137,7 +151,8 @@ allow(described_class).to receive_messages(casks_to_uninstall: [], formulae_to_uninstall: %w[a b], taps_to_untap: [], - vscode_extensions_to_uninstall: []) + vscode_extensions_to_uninstall: [], + tlmgr_packages_to_uninstall: []) end it "uninstalls formulae" do @@ -153,7 +168,8 @@ allow(described_class).to receive_messages(casks_to_uninstall: [], formulae_to_uninstall: [], taps_to_untap: %w[a b], - vscode_extensions_to_uninstall: []) + vscode_extensions_to_uninstall: [], + tlmgr_packages_to_uninstall: []) end it "untaps taps" do @@ -169,7 +185,8 @@ allow(described_class).to receive_messages(casks_to_uninstall: [], formulae_to_uninstall: [], taps_to_untap: [], - vscode_extensions_to_uninstall: %w[GitHub.codespaces]) + vscode_extensions_to_uninstall: %w[GitHub.codespaces], + tlmgr_packages_to_uninstall: []) end it "uninstalls extensions" do @@ -179,22 +196,40 @@ end end + context "when there are TeX Live packages to uninstall" do + before do + described_class.reset! + allow(described_class).to receive_messages(casks_to_uninstall: [], + formulae_to_uninstall: [], + taps_to_untap: [], + vscode_extensions_to_uninstall: [], + tlmgr_packages_to_uninstall: %w[bibtex]) + end + + it "uninstalls packages" do + expect(Kernel).to receive(:system).with("sudo", "tlmgr", "remove", "bibtex") + expect(described_class).to receive(:system_output_no_stderr).and_return("") + described_class.run(force: true) + end + end + context "when there are casks and formulae to uninstall and taps to untap but without passing `--force`" do before do described_class.reset! allow(described_class).to receive_messages(casks_to_uninstall: %w[a b], formulae_to_uninstall: %w[a b], taps_to_untap: %w[a b], - vscode_extensions_to_uninstall: %w[a b]) + vscode_extensions_to_uninstall: %w[a b], + tlmgr_packages_to_uninstall: %w[a b]) end it "lists casks, formulae and taps" do - expect(Formatter).to receive(:columns).with(%w[a b]).exactly(4).times + expect(Formatter).to receive(:columns).with(%w[a b]).exactly(5).times expect(Kernel).not_to receive(:system) expect(described_class).to receive(:system_output_no_stderr).and_return("") expect do described_class.run - end.to output(/Would uninstall formulae:.*Would untap:.*Would uninstall VSCode extensions:/m).to_stdout + end.to output(/Would uninstall formulae:.*Would untap:.*Would uninstall VSCode extensions:.*Would uninstall TeX Live packages:/m).to_stdout end end @@ -204,7 +239,8 @@ allow(described_class).to receive_messages(casks_to_uninstall: [], formulae_to_uninstall: [], taps_to_untap: [], - vscode_extensions_to_uninstall: []) + vscode_extensions_to_uninstall: [], + tlmgr_packages_to_uninstall: []) end def sane? diff --git a/spec/bundle/commands/dump_command_spec.rb b/spec/bundle/commands/dump_command_spec.rb index b0adb99c3..61c9983de 100644 --- a/spec/bundle/commands/dump_command_spec.rb +++ b/spec/bundle/commands/dump_command_spec.rb @@ -20,6 +20,7 @@ expect(Bundle::BrewDumper).not_to receive(:dump) expect(Bundle::CaskDumper).not_to receive(:dump) expect(Bundle::WhalebrewDumper).not_to receive(:dump) + expect(Bundle::TlmgrPackageDumper).not_to receive(:dump) expect do described_class.run end.to raise_error(RuntimeError) diff --git a/spec/bundle/commands/install_command_spec.rb b/spec/bundle/commands/install_command_spec.rb index 41f5227bc..a5353b168 100644 --- a/spec/bundle/commands/install_command_spec.rb +++ b/spec/bundle/commands/install_command_spec.rb @@ -24,6 +24,7 @@ mas '1Password', id: 443987910 whalebrew 'whalebrew/wget' vscode 'GitHub.codespaces' + tlmgr 'bibtex' EOS end @@ -31,6 +32,7 @@ allow(Bundle::TapInstaller).to receive(:preinstall).and_return(false) allow(Bundle::WhalebrewInstaller).to receive(:preinstall).and_return(false) allow(Bundle::VscodeExtensionInstaller).to receive(:preinstall).and_return(false) + allow(Bundle::TlmgrPackageInstaller).to receive_messages(preinstall: true, install: true) allow(Bundle::BrewInstaller).to receive_messages(preinstall: true, install: true) allow(Bundle::CaskInstaller).to receive_messages(preinstall: true, install: true) allow(Bundle::MacAppStoreInstaller).to receive_messages(preinstall: true, install: true) @@ -54,6 +56,7 @@ allow(Bundle::TapInstaller).to receive_messages(preinstall: true, install: false) allow(Bundle::WhalebrewInstaller).to receive_messages(preinstall: true, install: false) allow(Bundle::VscodeExtensionInstaller).to receive_messages(preinstall: true, install: false) + allow(Bundle::TlmgrPackageInstaller).to receive_messages(preinstall: true, install: true) allow(Bundle::Locker).to receive(:lockfile).and_return(Pathname(__dir__)) allow_any_instance_of(Pathname).to receive(:read).and_return(brewfile_contents) @@ -67,6 +70,7 @@ allow(Bundle::MacAppStoreInstaller).to receive_messages(preinstall: true, install: true) allow(Bundle::WhalebrewInstaller).to receive_messages(preinstall: true, install: true) allow(Bundle::VscodeExtensionInstaller).to receive_messages(preinstall: true, install: true) + allow(Bundle::TlmgrPackageInstaller).to receive_messages(preinstall: true, install: true) allow_any_instance_of(Pathname).to receive(:read).and_return(brewfile_contents) expect(Bundle).not_to receive(:system) diff --git a/spec/bundle/dsl_spec.rb b/spec/bundle/dsl_spec.rb index 4188c79e0..8dd231074 100644 --- a/spec/bundle/dsl_spec.rb +++ b/spec/bundle/dsl_spec.rb @@ -20,6 +20,7 @@ mas '1Password', id: 443987910 whalebrew 'whalebrew/wget' vscode 'GitHub.codespaces' + tlmgr 'bibtex', args: ['with-doc'] EOS end @@ -52,6 +53,8 @@ expect(dsl.entries[9].options).to eql(id: 443_987_910) expect(dsl.entries[10].name).to eql("whalebrew/wget") expect(dsl.entries[11].name).to eql("GitHub.codespaces") + expect(dsl.entries[12].name).to eql("bibtex") + expect(dsl.entries[12].options).to eql(args: ["with-docs"]) end end diff --git a/spec/bundle/dumper_spec.rb b/spec/bundle/dumper_spec.rb index d7e9e9280..8d702d7a8 100644 --- a/spec/bundle/dumper_spec.rb +++ b/spec/bundle/dumper_spec.rb @@ -10,13 +10,14 @@ ENV["HOMEBREW_BUNDLE_FILE"] = "" allow(Bundle).to receive_messages(cask_installed?: true, mas_installed?: false, whalebrew_installed?: false, - vscode_installed?: false) + vscode_installed?: false, tlmgr_installed?: false) Bundle::BrewDumper.reset! Bundle::TapDumper.reset! Bundle::CaskDumper.reset! Bundle::MacAppStoreDumper.reset! Bundle::WhalebrewDumper.reset! Bundle::VscodeExtensionDumper.reset! + Bundle::TlmgrPackageDumper.reset! Bundle::BrewServices.reset! chrome = instance_double(Cask::Cask, diff --git a/spec/bundle/tlmgr_package_installer_spec.rb b/spec/bundle/tlmgr_package_installer_spec.rb new file mode 100644 index 000000000..172ffb9c2 --- /dev/null +++ b/spec/bundle/tlmgr_package_installer_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Bundle::TlmgrPackageInstaller do + context "when Tex Live package manager is not installed" do + before do + described_class.reset! + allow(Bundle).to receive_messages(tlmgr_installed?: false) + end + + it "raises an error" do + expect { Bundle }.to raise_error(RuntimeError).and_return(false) + expect { described_class.preinstall("foo") }.to raise_error(RuntimeError) + end + end + + context "when TeX Live package manager is installed" do + before do + allow(Bundle).to receive(:mas_installed?).and_return(true) + end + + context "when package is installed" do + before do + allow(described_class).to receive(:installed_packages).and_return(["foo"]) + end + + it "skips" do + expect(Bundle).not_to receive(:system) + expect(described_class.preinstall("foo")).to be(false) + end + + it "skips ignoring case" do + expect(Bundle).not_to receive(:system) + expect(described_class.preinstall("Foo")).to be(false) + end + end + + context "when package is not installed" do + before do + allow(described_class).to receive(:installed_extensions).and_return([]) + end + + it "installs extension" do + expect(Bundle).to receive(:system).with("sudo", "tlmgr", "install", "foo", verbose: false).and_return(true) + expect(described_class.preinstall("foo")).to be(true) + expect(described_class.install("foo")).to be(true) + end + end + end +end