Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Starting with a caveat: we will never have a "proper" lockfile in Homebrew/homebrew-bundle that installs all your dependencies at exactly the versions you want because Homebrew doesn't work that way (and never will because e.g. pinning OpenSSL at the same version forever is a bad idea). I was somewhat inspired by a conversation with `@jnewland` to give this a go. What I'm thinking at a high-level this lockfile does is say "on this given configuration Homebrew/homebrew-bundle installed these versions of packages successfully". If a run is successful we'll dump various data about what succeeded into a lockfile. Within teams people may vary on the OS versions and even OS (macOS vs. Linux) they use so these will be (eventually) used as separate namespaces for system information so the lockfile can store every high-level configuration in which things worked. `brew bundle` will output a `Brewfile.lock.json` in the same directory as the `Brewfile` if all dependencies are installed successfully. This contains dependency and system status information which can be useful in debugging `brew bundle` failures and replicating a "last known good build" state. You can opt-out of this behaviour by setting the `HOMEBREW_BUNDLE_NO_LOCK` environment variable or passing the `--no-lock option`. You may wish to check this file into the same version control system as your `Brewfile` (or ensure your version control system ignores it if you'd prefer to rely on debugging information from a local machine). For example, for the following `Brewfile`: ```ruby brew "ack" cask "fork" mas "StopTheMadness", id: 1376402589 tap "homebrew/homebrew-bundle" ``` The `Brewfile.lock.json` would be: ```json { "entries": { "brew": { "ack": { "version": "3.2.0", "bottle": false }, "readline": { "version": "8.0.1", "bottle": { "cellar": ":any", "prefix": "/usr/local", "files": { "catalina": { "url": "https://homebrew.bintray.com/bottles/readline-8.0.1.catalina.bottle.tar.gz", "sha256": "ab3c966f4cae7d0f3ecc5688bb989820c3261f5ed547a08c84186ba7f53bdd9c" }, "mojave": { "url": "https://homebrew.bintray.com/bottles/readline-8.0.1.mojave.bottle.tar.gz", "sha256": "3c754391e9d243835811d128771ca0f1a565024100fd2c2871534353d46aaf0e" }, "high_sierra": { "url": "https://homebrew.bintray.com/bottles/readline-8.0.1.high_sierra.bottle.tar.gz", "sha256": "ae341a036139a92a47396aabc773ffcf40a17fc388aaadf0147f688c72ece987" }, "sierra": { "url": "https://homebrew.bintray.com/bottles/readline-8.0.1.sierra.bottle.tar.gz", "sha256": "f234d1ff8148bf08b0ac31e661f2e96b5c6e64df26a45d2392056c9077f964af" } } } } }, "cask": { "fork": { "version": "1.0.72.2" } }, "mas": { "StopTheMadness": { "id": "1376402589", "version": "8.6" } }, "tap": { "homebrew/bundle": { "revision": "045be0715c7cd41f0e914c9faf9dd42e7e1fae59" } } }, "system": { "macos": { "catalina": { "HOMEBREW_VERSION": "2.1.16-39-g5595dea", "HOMEBREW_PREFIX": "/usr/local", "Homebrew/homebrew-core": "a426105d937988c2d7dbe6b4e6fc1589dfb29e10", "CLT": "1100.0.33.8", "Xcode": "11.2", "macOS": "10.15.1" } } } } ```
- Loading branch information
1 parent
799c5cc
commit 8d23c65
Showing
14 changed files
with
300 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,4 @@ | ||
*.gem | ||
*.rbc | ||
.bundle | ||
.config | ||
.yardoc | ||
Brewfile | ||
Brewfile.lock.json | ||
Gemfile.lock | ||
InstalledFiles | ||
_yardoc | ||
bin/ | ||
coverage | ||
doc/ | ||
lib/bundler/man | ||
pkg | ||
rdoc | ||
spec/reports | ||
test/tmp | ||
test/version_tmp | ||
tmp | ||
.ruby-gemset | ||
.ruby-version | ||
.envrc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,142 @@ | ||
# frozen_string_literal: true | ||
|
||
require "tap" | ||
require "os" | ||
require "development_tools" | ||
|
||
module Bundle | ||
module Locker | ||
module_function | ||
|
||
def lockfile | ||
Brewfile.path.dirname/"Brewfile.lock.json" | ||
end | ||
|
||
def write_lockfile? | ||
!ARGV.include?("--no-lock") && ENV["HOMEBREW_BUNDLE_NO_LOCK"].nil? | ||
end | ||
|
||
def lock(entries) | ||
return false unless write_lockfile? | ||
|
||
lock = JSON.parse(lockfile.read) if lockfile.exist? | ||
lock ||= {} | ||
lock["entries"] ||= {} | ||
lock["system"] ||= {} | ||
|
||
entries.each do |entry| | ||
next if Bundle::Skipper.skip?(entry, silent: true) | ||
|
||
entry_type_key = entry.type.to_s | ||
options = entry.options | ||
lock["entries"][entry_type_key] ||= {} | ||
lock["entries"][entry_type_key][entry.name] = case entry.type | ||
when :brew | ||
brew_list_info[entry.name] | ||
when :cask | ||
options.delete(:args) if options[:args].blank? | ||
{ version: cask_list[entry.name] } | ||
when :mas | ||
options.delete(:id) | ||
mas_list[entry.name] | ||
when :tap | ||
options.delete(:clone_target) if options[:clone_target].blank? | ||
options.delete(:pin) if options[:pin] == false | ||
{ revision: Tap.fetch(entry.name).git_head } | ||
end | ||
|
||
if options.present? | ||
lock["entries"][entry_type_key][entry.name]["options"] = | ||
options.deep_stringify_keys | ||
end | ||
end | ||
|
||
if OS.mac? | ||
lock["system"]["macos"] ||= {} | ||
version, hash = system_macos | ||
lock["system"]["macos"][version] = hash | ||
elsif OS.linux? | ||
lock["system"]["linux"] ||= {} | ||
version, hash = system_linux | ||
lock["system"]["linux"][version] = hash | ||
end | ||
|
||
json = JSON.pretty_generate(lock) | ||
lockfile.unlink if lockfile.exist? | ||
lockfile.write(json) | ||
|
||
true | ||
end | ||
|
||
def brew_list_info | ||
@brew_list_info ||= begin | ||
name_bottles = JSON.parse(`brew info --json=v1 --installed`) | ||
.inject({}) do |name_bottles, f| | ||
bottle = f["bottle"]["stable"] | ||
bottle&.delete("rebuild") | ||
bottle&.delete("root_url") | ||
bottle ||= false | ||
name_bottles[f["name"]] = bottle | ||
name_bottles | ||
end | ||
`brew list --versions`.lines | ||
.inject({}) do |name_versions_bottles, line| | ||
name, version, = line.split | ||
name_versions_bottles[name] = { | ||
version: version, | ||
bottle: name_bottles[name], | ||
} | ||
name_versions_bottles | ||
end | ||
end | ||
end | ||
|
||
def cask_list | ||
@cask_list ||= begin | ||
`brew cask list --versions`.lines | ||
.inject({}) do |name_versions, line| | ||
name, version, = line.split | ||
name_versions[name] = version | ||
name_versions | ||
end | ||
end | ||
end | ||
|
||
def mas_list | ||
@mas_list ||= begin | ||
`mas list`.lines | ||
.inject({}) do |name_id_versions, line| | ||
line = line.split | ||
id = line.shift | ||
version = line.pop.delete("()") | ||
name = line.join(" ") | ||
name_id_versions[name] = { | ||
id: id, | ||
version: version, | ||
} | ||
name_id_versions | ||
end | ||
end | ||
end | ||
|
||
def system_macos | ||
[MacOS.version.to_sym.to_s, { | ||
"HOMEBREW_VERSION" => HOMEBREW_VERSION, | ||
"HOMEBREW_PREFIX" => HOMEBREW_PREFIX.to_s, | ||
"Homebrew/homebrew-core" => CoreTap.instance.git_head, | ||
"CLT" => MacOS::CLT.version.to_s, | ||
"Xcode" => MacOS::Xcode.version.to_s, | ||
"macOS" => MacOS.full_version.to_s, | ||
}] | ||
end | ||
|
||
def system_linux | ||
[OS::Linux.os_version, { | ||
"HOMEBREW_VERSION" => HOMEBREW_VERSION, | ||
"HOMEBREW_PREFIX" => HOMEBREW_PREFIX.to_s, | ||
"Homebrew/linuxbrew-core" => CoreTap.instance.git_head, | ||
"GCC" => DevelopmentTools.non_apple_gcc_version("gcc"), | ||
}] | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
# frozen_string_literal: true | ||
|
||
require "spec_helper" | ||
|
||
describe Bundle::Locker do | ||
subject(:locker) { described_class } | ||
|
||
context ".lockfile" do | ||
it "returns a Pathname" do | ||
allow(Bundle::Brewfile).to receive(:path).and_return(Pathname("Brewfile")) | ||
expect(locker.lockfile.class).to be Pathname | ||
end | ||
end | ||
|
||
context ".write_lockfile?" do | ||
it "returns false if --no-lock is passed" do | ||
allow(ARGV).to receive(:include?).with("--no-lock").and_return(true) | ||
expect(locker.write_lockfile?).to be false | ||
end | ||
|
||
it "returns false if HOMEBREW_BUNDLE_NO_LOCK is set" do | ||
ENV["HOMEBREW_BUNDLE_NO_LOCK"] = "1" | ||
expect(locker.write_lockfile?).to be false | ||
end | ||
|
||
it "returns true without --no-lock or HOMEBREW_BUNDLE_NO_LOCK" do | ||
ENV["HOMEBREW_BUNDLE_NO_LOCK"] = nil | ||
expect(locker.write_lockfile?).to be true | ||
end | ||
end | ||
|
||
context ".lock" do | ||
context "writes Brewfile.lock.json" do | ||
let(:lockfile) { Pathname("Brewfile.json.lock") } | ||
let(:brew_options) { { restart_service: true } } | ||
let(:entries) do | ||
[ | ||
Bundle::Dsl::Entry.new(:brew, "mysql", brew_options), | ||
Bundle::Dsl::Entry.new(:cask, "adoptopenjdk8"), | ||
Bundle::Dsl::Entry.new(:mas, "Xcode", id: 497799835), | ||
Bundle::Dsl::Entry.new(:tap, "homebrew/homebrew-cask-versions"), | ||
] | ||
end | ||
|
||
before do | ||
allow(locker).to receive(:lockfile).and_return(lockfile) | ||
allow(brew_options).to receive(:deep_stringify_keys) | ||
.and_return( { "restart_service" => true } ) | ||
allow(locker).to receive(:`).with("brew info --json=v1 --installed").and_return <<~EOS | ||
[ | ||
{ | ||
"name":"mysql", | ||
"bottle":{ | ||
"stable":{} | ||
} | ||
} | ||
] | ||
EOS | ||
allow(locker).to receive(:`).with("brew list --versions").and_return("mysql 8.0.18") | ||
end | ||
|
||
context "on macOS" do | ||
before do | ||
allow(OS).to receive(:mac?).and_return(true) | ||
|
||
allow(locker).to receive(:`).with("brew cask list --versions").and_return("adoptopenjdk8 8,232:b09") | ||
allow(locker).to receive(:`).with("mas list").and_return("497799835 Xcode (11.2)") | ||
end | ||
|
||
it do | ||
expect(lockfile).to receive(:write) | ||
expect(locker.lock(entries)).to be true | ||
end | ||
end | ||
|
||
context "on Linux" do | ||
before do | ||
allow(OS).to receive(:mac?).and_return(false) | ||
allow(OS).to receive(:linux?).and_return(true) | ||
end | ||
|
||
it do | ||
expect(lockfile).to receive(:write) | ||
expect(locker.lock(entries)).to be true | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,7 +15,7 @@ def linux? | |
if macos? | ||
minimum_coverage 100 | ||
else | ||
minimum_coverage 98 | ||
minimum_coverage 97 | ||
end | ||
end | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# frozen_string_literal: true | ||
|
||
class DevelopmentTools | ||
def self.non_apple_gcc_version(*) | ||
"9.2.0" | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.