8000 BottleLoader: Use the formula stored in the bottle by sjackman · Pull Request #3176 · Homebrew/brew · GitHub
[go: up one dir, main page]

Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BottleLoader: Use the formula stored in the bottle #3176

Merged
merged 6 commits into from
Sep 29, 2017

Conversation

sjackman
Copy link
Contributor
@sjackman sjackman commented Sep 19, 2017
  • Have you followed the guidelines in our Contributing document?
  • Have you checked to ensure there aren't other open Pull Requests for the same change?
  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes? Here's an example.
  • Have you successfully run brew tests with your changes locally?

When installing a bottle from the local file system or on http, use the formula stored in the bottle.

bottle_version = Utils::Bottles.resolve_version @bottle_filename
system "tar", "-xf", @bottle_filename, "-C", HOMEBREW_CACHE_FORMULA,
"--strip-components", "3", "#{name}/#{bottle_version}/.brew/#{name}.rb",
err: :close
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be good to avoid creating/deleting/writing files in the formulary loader. How about using something like tar -O to read it from stdout?

Copy link 8000
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good suggestion, Mike. Done.

@sjackman sjackman force-pushed the bottle-formula branch 2 times, most recently from 3883eea to 1320d39 Compare September 22, 2017 22:43
bottle_version = Utils::Bottles.resolve_version @bottle_filename
path = "#{name}/#{bottle_version}/.brew/#{name}.rb"
contents = Utils.popen_read "tar", "-xO", "-f", @bottle_filename,
"--strip-components", "3", path, err: :close
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't want err: :close to be going throughout the codebase when they are no-ops for most users and when, in this case, the error should never be hit. I'm tempted to say rather than falling back this should raise an exception instead. Thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I'll change it.

formula = if $CHILD_STATUS.success?
Formulary.from_contents name, HOMEBREW_CELLAR/path, contents, spec
else
super
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this stays as-is: worth printing a Warning message in this case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a warning.

@sjackman sjackman force-pushed the bottle-formula branch 3 times, most recently from 4b546eb to 2654b9f Compare September 24, 2017 15:37
formula = if $CHILD_STATUS.success?
Formulary.from_contents name, HOMEBREW_CELLAR/path, contents, spec
else
opoo "This bottle does not contain a formula: #{path}"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should probably just fail rather than have inconsistent behaviour. If you're loading bottles more than a year old you're going to have other problems.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Done.

@sjackman sjackman force-pushed the bottle-formula branch 2 times, most recently from 6e0516c to 622831b Compare September 25, 2017 06:36
@@ -122,14 +122,14 @@ def initialize(bottle_name)
super name, Formulary.path(full_name)
end

def get_formula(spec, alias_path: nil)
formula = super
def get_formula(spec, alias_path: nil) # rubocop:disable Lint/UnusedMethodArgument
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is # rubocop:disable Lint/UnusedMethodArgument the best way to avoid this brew style error?

W:125: 27: Unused method argument - alias_path.

See rubocop/rubocop#3130

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would you prefer…

raise "Unexpected non-nil parameter alias_path" unless alias_path.nil?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think def get_formula(spec, **) should work.

Copy link
Contributor Author
@sjackman sjackman Sep 25, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that works, but it does alter the behavior of the method for style, in that get_formula(spec, monkey: :banana) no longer fails. rubocop/rubocop#3130 (comment)
My minor style preference is raise … unless nil?. I'm fine with ** though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@sjackman
Copy link
Contributor Author

I have a failed test to address. I'll get back to you with that later today.

  1) Formulary::factory returns a Formula when given a bottle
     Failure/Error: formula = subject.factory(bottle)
     
     BottleFormulaUnavailableError:
       This bottle does not contain the formula file:
         testball_bottle/0.1/.brew/testball_bottle.rb

Bottle version mismatch
Bottle: #{bottle_file} (#{bottle_version})
Formula: #{formula.full_name} (#{formula_version})
This bottle does not contain the formula file:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about outputting the bottle file and expected formula path within it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

def get_formula(spec, alias_path: nil)
formula = super
def get_formula(spec, alias_path: nil) # rubocop:disable Lint/UnusedMethodArgument
bottle_version = Utils::Bottles.resolve_version @bottle_filename
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this fail?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the bottle does not contain a file matching the regex .+/.+/INSTALL_RECEIPT.json, then yes, it could fail. It currently throws an exception:

Error: undefined method `split' for nil:NilClass
/Users/sjackman/.homebrew/Library/Homebrew/utils/bottles.rb:40:in `resolve_formula_names'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added

--- a/Library/Homebrew/utils/bottles.rb
+++ b/Library/Homebrew/utils/bottles.rb
@@ -29,9 +29,11 @@ module Utils
       end
 
       def receipt_path(bottle_file)
-        Utils.popen_read("tar", "-tzf", bottle_file).lines.map(&:chomp).find do |line|
+        path = Utils.popen_read("tar", "-tzf", bottle_file).lines.map(&:chomp).find do |line|
           line =~ %r{.+/.+/INSTALL_RECEIPT.json}
         end
+        raise "This bottle does not contain the file INSTALL_RECEIPT.json: #{bottle_file}" if path.nil?
+        path
       end
 
       def resolve_formula_names(bottle_file)

contents = Utils.popen_read "tar", "-xO", "-f", @bottle_filename, "--strip-components=3", formula_path
raise BottleFormulaUnavailableError, formula_path unless $CHILD_STATUS.success?
formula = Formulary.from_contents name, HOMEBREW_CELLAR/formula_path, contents, spec
formula.bottle_specification.sha256 @bottle_filename.sha256 => Utils::Bottles.tag
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens without this, out of interest? I'm 👍 on it, just curious.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the formula stored in the bottle does not have a bottle block, the bottle method returns nil, and causes the error. In principle that error would have also been possible before if the formula in the repo did not contain a bottle block, which is uncommon.

==> Pouring the cached bottle
Error: undefined method `compatible_cellar?' for nil:NilClass
/Users/sjackman/.homebrew/Library/Homebrew/formula_installer.rb:104:in `pour_bottle?'

I'll add a commit to address this issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -101,7 +101,7 @@ def pour_bottle?(install_bottle_options = { warn: false })
return false
end

unless bottle.compatible_cellar?
if bottle && !bottle.compatible_cellar?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unless bottle&.compatible_cellar?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't know about the &. operator! Fantastic!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be enforced by brew style from Homebrew/homebrew-portable-ruby#50. Wasn't supported in Ruby 2.0.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formula.bottle is nil when bottle.compatible_cellar? is false.
Using formula.bottle_specification.compatible_cellar? rather
than formula.bottle.compatible_cellar? avoids this problem

line =~ %r{.+/.+/INSTALL_RECEIPT.json}
end
raise "This bottle does not contain the file INSTALL_RECEIPT.json: #{bottle_file}" if path.nil?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unless path

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@sjackman sjackman force-pushed the bottle-formula branch 2 times, most recently from 9d1f214 to aa98aca Compare September 25, 2017 23:16
Copy link
Member
@MikeMcQuaid MikeMcQuaid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Let me know when you've tested this thoroughly locally and will merge. Thanks!

@sjackman
10000 Copy link
Contributor Author
sjackman commented Sep 26, 2017

I found one odd use case: brew install -s local_bottle.tar.gz failed when copying the formula to .brew/NAME.rb, because the formula is inside the bottle and not stored anywhere on disk. I've fixed this issue with the most recent commit, 46fa99c3d68c0e9fc37203bdaa37f6a45bb7ef30.

@sjackman
Copy link
Contributor Author

Do you think it would be useful to add a contents method to the Formula class, that returns the Ruby code of the formula?

@MikeMcQuaid
Copy link
Member

@sjackman If there's 3 or more locations it would be used: yep, otherwise: nope.

@ilovezfs
Copy link
Contributor

Masking the formula that is in the git repository in favor of one buried in a binary and, in particular, allowing that to override the checksum, seems like a security risk to me.

@MikeMcQuaid
Copy link
Member

Masking the formula that is in the git repository in favor of one buried in a binary and, in particular, allowing that to override the checksum, seems like a security risk to me.

I think the checksum should be able to be set if it's unset if that's needed for internal code but it shouldn't be able to be overridden, I agree. I wonder if it's worth reading the version from the both formulae and using the one in Git if the version/revision/pkg_version match (as otherwise we are unlikely to have old bottles remain working after e.g. major post_install changes). Alternatively, the best middle ground might be only using the post_install from the bottled formula as that's the only bit that affects bottles at all.

@ilovezfs
Copy link
Contributor

If we patch a critical CVE that would still let someone blithely brew install ./badversion since the versions wouldn't be the same.

@MikeMcQuaid
Copy link
Member

A thought: a warning should be output when loading a formula that exists in the tap from the tab at a newer version. If we’re aware an older version is being installed (which we can be): we should warn.

@sjackman
Copy link
Contributor Author

Sure. I can do that.

@sjackman
Copy link
Contributor Author

My most recent commit displays a warning when downgrading a formula.

@sjackman
Copy link
Contributor Author

I think the checksum should be able to be set if it's unset if that's needed for internal code

Adding the sha256 to the bottle spec is no longer necessary thanks to fixing up the logic in pour_bottle? not to freak out when formula.bottle is nil. I've removed the line that sets the sha256 of the bottle.

@@ -101,6 +100,7 @@ def pour_bottle?(install_bottle_options = { warn: false })
return false
end

bottle = formula.bottle_specification
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed below, out of interest, and bottle_specification rather than bottle?

Copy link
Contributor Author
@sjackman sjackman Sep 28, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formula.bottle returns nil when compatible_cellar? is false.
See https://github.com/Homebrew/brew/blob/master/Library/Homebrew/formula.rb#L330
and https://github.com/Homebrew/brew/blob/master/Library/Homebrew/software_spec.rb#L93

We could use formula.bottled? rather than formula.bottle_specification.compatible_cellar?. Do you have a preference?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

formula.bottled? would be preferable, yeh.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -235,6 +235,10 @@ def install
end
raise CannotInstallFormulaError, message
end
if formula.opt_prefix.directory? &&
formula.pkg_version < (v = Keg.new(formula.opt_prefix.resolved_path).version)
opoo "Downgrading #{formula.name} from #{v} to #{formula.pkg_version}."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, rather than downgrading here would be good to check the tab in the bottle and see if the formula file exists and that one has a newer version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, got it. Sorry I misunderstood. I've updated the warning.

@@ -6,7 +6,7 @@ def initialize(name = "testball_bottle", path = Pathname.new(__FILE__).expand_pa
stable.bottle do
cellar :any_skip_relocation
root_url "file://#{TEST_FIXTURE_DIR}/bottles"
sha256 "9abc8ce779067e26556002c4ca6b9427b9874d25f0cafa7028e05b5c5c410cb4" => Utils::Bottles.tag
sha256 "d48bbbe583dcfbfa608579724fc6f0328b3cd316935c6ea22f134610aaf2952f" => Utils::Bottles.tag
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably: adding the formula?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep

@sjackman sjackman force-pushed the bottle-formula branch 2 times, most recently from 7fc5ca9 to 4d3dbae Compare September 28, 2017 18:48
# Warn if a more recent version of this formula is available in the tap.
begin
if formula.pkg_version < (v = Formulary.factory(formula.full_name).pkg_version)
opoo "#{formula.name} #{v} is available and more recent than version #{formula.pkg_version}."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use formula.full_name here? @ilovezfs, does this look OK?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

formula.bottle is nil when bottle.compatible_cellar? is false.
Use formula.bottle_specification.compatible_cellar? rather
than formula.bottle.compatible_cellar?.
Factor Utils::Bottles.formula_contents out of BottleLoader.
Warn if a more recent version of this formula is available in the tap.
@MikeMcQuaid
Copy link
Member

Thanks again @sjackman!

@MikeMcQuaid MikeMcQuaid merged commit 296a441 into Homebrew:master Sep 29, 2017
@sjackman sjackman deleted the bottle-formula branch September 29, 2017 15:42
@sjackman
Copy link
Contributor Author

Woo hoo! Thanks for merging, Mike!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0