Skip to content

Commit

Permalink
Add support for TeX Live package manager (tlmgr) packages
Browse files Browse the repository at this point in the history
Allow tlmgr packages to be installed, dumped, checked, updated and cleaned up.
  • Loading branch information
Okeanos committed Mar 28, 2024
1 parent 6b81dab commit 832d50b
Show file tree
Hide file tree
Showing 21 changed files with 415 additions and 23 deletions.
8 changes: 7 additions & 1 deletion 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

Expand All @@ -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.
Expand Down Expand Up @@ -64,6 +66,10 @@ whalebrew "whalebrew/wget"

# 'vscode --install-extension'
vscode "GitHub.codespaces"

# 'sudo tlmgr install'
tlmgr "bibtex"
tlmgr "bibtex", args: [ ]
```

## Versions and lockfiles
Expand Down
4 changes: 4 additions & 0 deletions cmd/bundle.rb
Expand Up @@ -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 " \
Expand Down Expand Up @@ -137,6 +139,7 @@ def bundle
mas: args.mas?,
whalebrew: args.whalebrew?,
vscode: args.vscode?,
tlmgr: args.tlmgr?,
)
when "cleanup"
Bundle::Commands::Cleanup.run(
Expand Down Expand Up @@ -169,6 +172,7 @@ def bundle
mas: args.mas?,
whalebrew: args.whalebrew?,
vscode: args.vscode?,
tlmgr: args.tlmgr?,
brews: args.brews?,
)
else
Expand Down
3 changes: 3 additions & 0 deletions lib/bundle.rb
Expand Up @@ -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"
4 changes: 4 additions & 0 deletions lib/bundle/bundle.rb
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions lib/bundle/checker.rb
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
26 changes: 26 additions & 0 deletions lib/bundle/commands/cleanup.rb
Expand Up @@ -15,6 +15,7 @@ def reset!
Bundle::BrewDumper.reset!
Bundle::TapDumper.reset!
Bundle::VscodeExtensionDumper.reset!
Bundle::TlmgrPackageDumper.reset!
Bundle::BrewServices.reset!
end

Expand All @@ -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"] : []
Expand All @@ -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
Expand All @@ -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`:"
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions lib/bundle/commands/list.rb
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions lib/bundle/dsl.rb
Expand Up @@ -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)
Expand Down
9 changes: 5 additions & 4 deletions lib/bundle/dumper.rb
Expand Up @@ -15,26 +15,27 @@ 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
content << CaskDumper.dump(describe:) if casks || all
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

Expand Down
2 changes: 2 additions & 0 deletions lib/bundle/installer.rb
Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions lib/bundle/lister.rb
Expand Up @@ -5,25 +5,26 @@ 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
end
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
Expand Down
18 changes: 18 additions & 0 deletions 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
36 changes: 36 additions & 0 deletions 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

0 comments on commit 832d50b

Please sign in to comment.