diff --git a/Library/Homebrew/cmd/config.rb b/Library/Homebrew/cmd/config.rb index 7627d6e970..74b4f436e2 100644 --- a/Library/Homebrew/cmd/config.rb +++ b/Library/Homebrew/cmd/config.rb @@ -7,23 +7,23 @@ module Homebrew end def llvm - @llvm ||= MacOS.llvm_build_version + @llvm ||= MacOS.llvm_build_version if MacOS.has_apple_developer_tools? end def gcc_42 - @gcc_42 ||= MacOS.gcc_42_build_version + @gcc_42 ||= MacOS.gcc_42_build_version if MacOS.has_apple_developer_tools? end def gcc_40 - @gcc_40 ||= MacOS.gcc_40_build_version + @gcc_40 ||= MacOS.gcc_40_build_version if MacOS.has_apple_developer_tools? end def clang - @clang ||= MacOS.clang_version + @clang ||= MacOS.clang_version if MacOS.has_apple_developer_tools? end def clang_build - @clang_build ||= MacOS.clang_build_version + @clang_build ||= MacOS.clang_build_version if MacOS.has_apple_developer_tools? end def xcode diff --git a/Library/Homebrew/cmd/doctor.rb b/Library/Homebrew/cmd/doctor.rb index 7ceb88b7e5..97e31a8e24 100644 --- a/Library/Homebrew/cmd/doctor.rb +++ b/Library/Homebrew/cmd/doctor.rb @@ -258,6 +258,7 @@ class Checks end end + # TODO: distill down into single method definition a la BuildToolsError if MacOS.version >= "10.9" def check_for_installed_developer_tools unless MacOS::Xcode.installed? || MacOS::CLT.installed? then <<-EOS.undent diff --git a/Library/Homebrew/cmd/install.rb b/Library/Homebrew/cmd/install.rb index 19c826d44c..47735a23c1 100644 --- a/Library/Homebrew/cmd/install.rb +++ b/Library/Homebrew/cmd/install.rb @@ -38,6 +38,10 @@ module Homebrew end end + # if the user's flags will prevent bottle only-installations when no + # developer tools are available, we need to stop them early on + FormulaInstaller.prevent_build_flags unless MacOS.has_apple_developer_tools? + ARGV.formulae.each do |f| # head-only without --HEAD is an error if !ARGV.build_head? && f.stable.nil? && f.devel.nil? @@ -131,10 +135,10 @@ module Homebrew checks = Checks.new %w[ check_for_unsupported_osx + check_for_bad_install_name_tool check_for_installed_developer_tools check_xcode_license_approved check_for_osx_gcc_installer - check_for_bad_install_name_tool ].each do |check| out = checks.send(check) opoo out unless out.nil? @@ -161,7 +165,7 @@ module Homebrew def perform_preinstall_checks check_ppc check_writable_install_location - check_xcode + check_xcode if MacOS.has_apple_developer_tools? check_cellar end diff --git a/Library/Homebrew/cmd/reinstall.rb b/Library/Homebrew/cmd/reinstall.rb index fe5e2e5ec7..435c3155fd 100644 --- a/Library/Homebrew/cmd/reinstall.rb +++ b/Library/Homebrew/cmd/reinstall.rb @@ -2,6 +2,8 @@ require "formula_installer" module Homebrew def reinstall + FormulaInstaller.prevent_build_flags unless MacOS.has_apple_developer_tools? + ARGV.resolved_formulae.each { |f| reinstall_formula(f) } end diff --git a/Library/Homebrew/cmd/upgrade.rb b/Library/Homebrew/cmd/upgrade.rb index 211d081e7b..e1b7dd0dc7 100644 --- a/Library/Homebrew/cmd/upgrade.rb +++ b/Library/Homebrew/cmd/upgrade.rb @@ -3,6 +3,8 @@ require "cmd/outdated" module Homebrew def upgrade + FormulaInstaller.prevent_build_flags unless MacOS.has_apple_developer_tools? + Homebrew.perform_preinstall_checks if ARGV.named.empty? diff --git a/Library/Homebrew/exceptions.rb b/Library/Homebrew/exceptions.rb index bae941eede..65675247c0 100644 --- a/Library/Homebrew/exceptions.rb +++ b/Library/Homebrew/exceptions.rb @@ -257,6 +257,100 @@ class BuildError < RuntimeError end end +# raised by FormulaInstaller.check_dependencies_bottled and +# FormulaInstaller.install if the formula or its dependencies are not bottled +# and are being installed on a system without necessary build tools +class BuildToolsError < RuntimeError + def initialize(formulae) + if formulae.length > 1 + formula_text = "formulae" + package_text = "binary packages" + else + formula_text = "formula" + package_text = "a binary package" + end + + if MacOS.version >= "10.10" + xcode_text = <<-EOS.undent + To continue, you must install Xcode from the App Store, + or the CLT by running: + xcode-select --install + EOS + elsif MacOS.version == "10.9" + xcode_text = <<-EOS.undent + To continue, you must install Xcode from: + https://developer.apple.com/downloads/ + or the CLT by running: + xcode-select --install + EOS + elsif MacOS.version >= "10.7" + xcode_text = <<-EOS.undent + To continue, you must install Xcode or the CLT from: + https://developer.apple.com/downloads/ + EOS + else + xcode_text = <<-EOS.undent + To continue, you must install Xcode from: + https://developer.apple.com/downloads/ + EOS + end + + super <<-EOS.undent + The following #{formula_text}: + #{formulae.join(', ')} + cannot be installed as a #{package_text} and must be built from source. + #{xcode_text} + EOS + end +end + +# raised by Homebrew.install, Homebrew.reinstall, and Homebrew.upgrade +# if the user passes any flags/environment that would case a bottle-only +# installation on a system without build tools to fail +class BuildFlagsError < RuntimeError + def initialize(flags) + if flags.length > 1 + flag_text = "flags" + require_text = "require" + else + flag_text = "flag" + require_text = "requires" + end + + if MacOS.version >= "10.10" + xcode_text = <<-EOS.undent + or install Xcode from the App Store, or the CLT by running: + xcode-select --install + EOS + elsif MacOS.version == "10.9" + xcode_text = <<-EOS.undent + or install Xcode from: + https://developer.apple.com/downloads/ + or the CLT by running: + xcode-select --install + EOS + elsif MacOS.version >= "10.7" + xcode_text = <<-EOS.undent + or install Xcode or the CLT from: + https://developer.apple.com/downloads/ + EOS + else + xcode_text = <<-EOS.undent + or install Xcode from: + https://developer.apple.com/downloads/ + EOS + end + + super <<-EOS.undent + The following #{flag_text}: + #{flags.join(', ')} + #{require_text} building tools, but none are installed. + Either remove the #{flag_text} to attempt bottle installation, + #{xcode_text} + EOS + end +end + # raised by CompilerSelector if the formula fails with all of # the compilers available on the user's system class CompilerSelectionError < RuntimeError diff --git a/Library/Homebrew/extend/ARGV.rb b/Library/Homebrew/extend/ARGV.rb index 9241f02c6f..0f78769c5b 100644 --- a/Library/Homebrew/extend/ARGV.rb +++ b/Library/Homebrew/extend/ARGV.rb @@ -207,6 +207,20 @@ module HomebrewArgvExtension value "env" end + # If the user passes any flags that trigger building over installing from + # a bottle, they are collected here and returned as an Array for checking. + def collect_build_flags + build_flags = [] + + build_flags << '--HEAD' if build_head? + build_flags << '--universal' if build_universal? + build_flags << '--32-bit' if build_32_bit? + build_flags << '--build-bottle' if build_bottle? + build_flags << '--build-from-source' if build_from_source? + + build_flags + end + private def spec(default = :stable) diff --git a/Library/Homebrew/extend/ENV/std.rb b/Library/Homebrew/extend/ENV/std.rb index 6798e6cd33..d05ccf1a38 100644 --- a/Library/Homebrew/extend/ENV/std.rb +++ b/Library/Homebrew/extend/ENV/std.rb @@ -29,7 +29,7 @@ module Stdenv self["PKG_CONFIG_LIBDIR"] = determine_pkg_config_libdir # make any aclocal stuff installed in Homebrew available - self["ACLOCAL_PATH"] = "#{HOMEBREW_PREFIX}/share/aclocal" if MacOS::Xcode.provides_autotools? + self["ACLOCAL_PATH"] = "#{HOMEBREW_PREFIX}/share/aclocal" if MacOS.has_apple_developer_tools? && MacOS::Xcode.provides_autotools? self["MAKEFLAGS"] = "-j#{make_jobs}" diff --git a/Library/Homebrew/extend/ENV/super.rb b/Library/Homebrew/extend/ENV/super.rb index a59b54a1d1..95a8773cdd 100644 --- a/Library/Homebrew/extend/ENV/super.rb +++ b/Library/Homebrew/extend/ENV/super.rb @@ -23,6 +23,8 @@ module Superenv end def self.bin + return unless MacOS.has_apple_developer_tools? + bin = (HOMEBREW_REPOSITORY/"Library/ENV").subdirs.reject { |d| d.basename.to_s > MacOS::Xcode.version }.max bin.realpath unless bin.nil? end diff --git a/Library/Homebrew/formula.rb b/Library/Homebrew/formula.rb index 8fb220ba89..278cf65959 100644 --- a/Library/Homebrew/formula.rb +++ b/Library/Homebrew/formula.rb @@ -741,7 +741,11 @@ class Formula end def file_modified? - return false unless which("git") + git_dir = MacOS.locate("git").dirname.to_s + + # /usr/bin/git is a popup stub when Xcode/CLT aren't installed, so bail out + return false if git_dir == "/usr/bin" && !MacOS.has_apple_developer_tools? + path.parent.cd do diff = Utils.popen_read("git", "diff", "origin/master", "--", "#{path}") !diff.empty? && $?.exitstatus == 0 diff --git a/Library/Homebrew/formula_installer.rb b/Library/Homebrew/formula_installer.rb index 8b0c1eebc4..8c15367a9e 100644 --- a/Library/Homebrew/formula_installer.rb +++ b/Library/Homebrew/formula_installer.rb @@ -13,6 +13,7 @@ require "cmd/postinstall" require "hooks/bottles" require "debrew" require "sandbox" +require "requirements/cctools_requirement" class FormulaInstaller include FormulaCellarChecks @@ -55,6 +56,15 @@ class FormulaInstaller @pour_failed = false end + # When no build tools are available and build flags are passed through ARGV, + # it's necessary to interrupt the user before any sort of installation + # can proceed. Only invoked when the user has no developer tools. + def self.prevent_build_flags + build_flags = ARGV.collect_build_flags + + raise BuildFlagsError.new(build_flags) unless build_flags.empty? + end + def pour_bottle?(install_bottle_options = { :warn=>false }) return true if Homebrew::Hooks::Bottles.formula_has_bottle?(formula) @@ -146,7 +156,15 @@ class FormulaInstaller check_conflicts - compute_and_install_dependencies unless ignore_deps? + if !pour_bottle? && !MacOS.has_apple_developer_tools? + raise BuildToolsError.new([formula]) + end + + if !ignore_deps? + deps = compute_dependencies + check_dependencies_bottled(deps) if pour_bottle? + install_dependencies(deps) + end return if only_deps? @@ -166,6 +184,7 @@ class FormulaInstaller if pour_bottle?(:warn => true) begin + install_relocation_tools unless formula.bottle.skip_relocation? pour rescue => e raise if ARGV.homebrew_developer? @@ -215,18 +234,31 @@ class FormulaInstaller raise FormulaConflictError.new(formula, conflicts) unless conflicts.empty? end - def compute_and_install_dependencies + # Compute and collect the dependencies needed by the formula currently + # being installed. + def compute_dependencies req_map, req_deps = expand_requirements - check_requirements(req_map) - deps = expand_dependencies(req_deps + formula.deps) - if deps.empty? && only_deps? - puts "All dependencies for #{formula.full_name} are satisfied." - else - install_dependencies(deps) + deps + end + + # Check that each dependency in deps has a bottle available, terminating + # abnormally with a BuildToolsError if one or more don't. + # Only invoked when the user has no developer tools. + def check_dependencies_bottled(deps) + unbottled = deps.select do |dep, _| + formula = dep.to_formula + !formula.pour_bottle? && !MacOS.has_apple_developer_tools? end + + raise BuildToolsError.new(unbottled) unless unbottled.empty? + end + + def compute_and_install_dependencies + deps = compute_dependencies + install_dependencies(deps) end def check_requirements(req_map) @@ -317,15 +349,29 @@ class FormulaInstaller end def install_dependencies(deps) - if deps.length > 1 - oh1 "Installing dependencies for #{formula.full_name}: #{Tty.green}#{deps.map(&:first)*", "}#{Tty.reset}" + if deps.empty? && only_deps? + puts "All dependencies for #{formula.full_name} are satisfied." + else + oh1 "Installing dependencies for #{formula.full_name}: #{Tty.green}#{deps.map(&:first)*", "}#{Tty.reset}" unless deps.empty? + deps.each { |dep, options| install_dependency(dep, options) } end - deps.each { |dep, options| install_dependency(dep, options) } - @show_header = true unless deps.empty? end + # Installs the relocation tools (as provided by the cctools formula) as a hard + # dependency for every formula installed from a bottle when the user has no + # developer tools. Invoked unless the formula explicitly sets + # :any_skip_relocation in its bottle DSL. + def install_relocation_tools + cctools = CctoolsRequirement.new + dependency = cctools.to_dependency + formula = dependency.to_formula + return if cctools.satisfied? || @@attempted.include?(formula) + + install_dependency(dependency, inherited_options_for(cctools)) + end + class DependencyInstaller < FormulaInstaller def initialize(*) super @@ -397,7 +443,8 @@ class FormulaInstaller keg = Keg.new(formula.prefix) link(keg) - fix_install_names(keg) + + fix_install_names(keg) unless @poured_bottle && formula.bottle.skip_relocation? if formula.post_install_defined? if build_bottle? @@ -633,8 +680,10 @@ class FormulaInstaller end keg = Keg.new(formula.prefix) - keg.relocate_install_names Keg::PREFIX_PLACEHOLDER, HOMEBREW_PREFIX.to_s, - Keg::CELLAR_PLACEHOLDER, HOMEBREW_CELLAR.to_s, :keg_only => formula.keg_only? + unless formula.bottle.skip_relocation? + keg.relocate_install_names Keg::PREFIX_PLACEHOLDER, HOMEBREW_PREFIX.to_s, + Keg::CELLAR_PLACEHOLDER, HOMEBREW_CELLAR.to_s, :keg_only => formula.keg_only? + end Pathname.glob("#{formula.bottle_prefix}/{etc,var}/**/*") do |path| path.extend(InstallRenamed) diff --git a/Library/Homebrew/keg_relocate.rb b/Library/Homebrew/keg_relocate.rb index eceef192f1..05d19db4d6 100644 --- a/Library/Homebrew/keg_relocate.rb +++ b/Library/Homebrew/keg_relocate.rb @@ -98,8 +98,8 @@ class Keg end def install_name_tool(*args) - tool = MacOS.locate("install_name_tool") - system(tool, *args) || raise(ErrorDuringExecution.new(tool, args)) + tool = MacOS.install_name_tool + system(tool, *args) or raise ErrorDuringExecution.new(tool, args) end # If file is a dylib or bundle itself, look for the dylib named by diff --git a/Library/Homebrew/mach.rb b/Library/Homebrew/mach.rb index c15399cbfc..f7ca428e6e 100644 --- a/Library/Homebrew/mach.rb +++ b/Library/Homebrew/mach.rb @@ -154,9 +154,9 @@ module MachO def parse_otool_L_output ENV["HOMEBREW_MACH_O_FILE"] = path.expand_path.to_s - libs = `#{MacOS.locate("otool")} -L "$HOMEBREW_MACH_O_FILE"`.split("\n") + libs = `#{MacOS.otool} -L "$HOMEBREW_MACH_O_FILE"`.split("\n") unless $?.success? - raise ErrorDuringExecution.new(MacOS.locate("otool"), + raise ErrorDuringExecution.new(MacOS.otool, ["-L", ENV["HOMEBREW_MACH_O_FILE"]]) end diff --git a/Library/Homebrew/os/mac.rb b/Library/Homebrew/os/mac.rb index ffab00b009..c2c909d768 100644 --- a/Library/Homebrew/os/mac.rb +++ b/Library/Homebrew/os/mac.rb @@ -36,6 +36,33 @@ module OS end end + # Locates a (working) copy of install_name_tool, guaranteed to function + # whether the user has developer tools installed or not. + def install_name_tool + if File.executable?(path = "#{HOMEBREW_PREFIX}/opt/cctools/bin/install_name_tool") + Pathname.new(path) + else + locate("install_name_tool") + end + end + + # Locates a (working) copy of otool, guaranteed to function whether the user + # has developer tools installed or not. + def otool + if File.executable?(path = "#{HOMEBREW_PREFIX}/opt/cctools/bin/otool") + Pathname.new(path) + else + locate("otool") + end + end + + # Checks if the user has any developer tools installed, either via Xcode + # or the CLT. Convenient for guarding against formula builds when building + # is impossible. + def has_apple_developer_tools? + Xcode.installed? || CLT.installed? + end + def active_developer_dir @active_developer_dir ||= Utils.popen_read("/usr/bin/xcode-select", "-print-path").strip end diff --git a/Library/Homebrew/os/mac/xcode.rb b/Library/Homebrew/os/mac/xcode.rb index 9b111e4e65..a79814d0d1 100644 --- a/Library/Homebrew/os/mac/xcode.rb +++ b/Library/Homebrew/os/mac/xcode.rb @@ -76,6 +76,8 @@ module OS return "0" unless OS.mac? + return nil if !MacOS::Xcode.installed? && !MacOS::CLT.installed? + %W[#{prefix}/usr/bin/xcodebuild #{which("xcodebuild")}].uniq.each do |path| if File.file? path Utils.popen_read(path, "-version") =~ /Xcode (\d(\.\d)*)/ diff --git a/Library/Homebrew/requirements/cctools_requirement.rb b/Library/Homebrew/requirements/cctools_requirement.rb new file mode 100644 index 0000000000..ab7f8cb59c --- /dev/null +++ b/Library/Homebrew/requirements/cctools_requirement.rb @@ -0,0 +1,13 @@ +# Represents a general requirement for utilities normally installed by Xcode, +# the CLT, or provided by the cctools formula. In particular, this requirement +# allows Homebrew to pull in the cctools formula and use its utilities to +# perform relocation operations on systems that do not have either Xcode or the +# CLT installed (but still want to install bottled formulae). +class CctoolsRequirement < Requirement + fatal true + default_formula 'cctools' + + satisfy(:build_env => false) do + MacOS::Xcode.installed? || MacOS::CLT.installed? || Formula['cctools'].installed? + end +end diff --git a/Library/Homebrew/software_spec.rb b/Library/Homebrew/software_spec.rb index f7e2ff1131..e8148322e3 100644 --- a/Library/Homebrew/software_spec.rb +++ b/Library/Homebrew/software_spec.rb @@ -245,6 +245,11 @@ class Bottle @spec.compatible_cellar? end + # Does the bottle need to be relocated? + def skip_relocation? + @spec.skip_relocation? + end + def stage resource.downloader.stage end @@ -281,7 +286,12 @@ class BottleSpecification end def compatible_cellar? - cellar == :any || cellar == HOMEBREW_CELLAR.to_s + cellar == :any || cellar == :any_skip_relocation || cellar == HOMEBREW_CELLAR.to_s + end + + # Does the Bottle this BottleSpecification belongs to need to be relocated? + def skip_relocation? + cellar == :any_skip_relocation end def tag?(tag) diff --git a/Library/Homebrew/test/bottles/testball_bottle-0.1.mavericks.bottle.tar.gz b/Library/Homebrew/test/bottles/testball_bottle-0.1.mavericks.bottle.tar.gz new file mode 120000 index 0000000000..3e989830ba --- /dev/null +++ b/Library/Homebrew/test/bottles/testball_bottle-0.1.mavericks.bottle.tar.gz @@ -0,0 +1 @@ +testball_bottle-0.1.yosemite.bottle.tar.gz \ No newline at end of file diff --git a/Library/Homebrew/test/bottles/testball_bottle-0.1.mountain_lion.bottle.tar.gz b/Library/Homebrew/test/bottles/testball_bottle-0.1.mountain_lion.bottle.tar.gz new file mode 120000 index 0000000000..3e989830ba --- /dev/null +++ b/Library/Homebrew/test/bottles/testball_bottle-0.1.mountain_lion.bottle.tar.gz @@ -0,0 +1 @@ +testball_bottle-0.1.yosemite.bottle.tar.gz \ No newline at end of file diff --git a/Library/Homebrew/test/bottles/testball_bottle-0.1.yosemite.bottle.tar.gz b/Library/Homebrew/test/bottles/testball_bottle-0.1.yosemite.bottle.tar.gz new file mode 100644 index 0000000000..d88838a94d Binary files /dev/null and b/Library/Homebrew/test/bottles/testball_bottle-0.1.yosemite.bottle.tar.gz differ diff --git a/Library/Homebrew/test/test_formula_installer_bottle.rb b/Library/Homebrew/test/test_formula_installer_bottle.rb new file mode 100644 index 0000000000..4d2d1676f8 --- /dev/null +++ b/Library/Homebrew/test/test_formula_installer_bottle.rb @@ -0,0 +1,78 @@ +require "testing_env" +require "formula" +require "compat/formula_specialties" +require "formula_installer" +require "keg" +require "testball_bottle" +require "testball" + +class InstallBottleTests < Homebrew::TestCase + def temporary_bottle_install(formula) + refute_predicate formula, :installed? + assert_predicate formula, :bottled? + assert_predicate formula, :pour_bottle? + + installer = FormulaInstaller.new(formula) + + shutup { installer.install } + + keg = Keg.new(formula.prefix) + + assert_predicate formula, :installed? + + begin + yield formula + ensure + keg.unlink + keg.uninstall + formula.clear_cache + Dir["#{HOMEBREW_CACHE}/testball_bottle*"].each { |f| File.delete(f) } + # there will be log files when sandbox is enable. + formula.logs.rmtree if formula.logs.directory? + end + + refute_predicate keg, :exist? + refute_predicate formula, :installed? + end + + def test_a_basic_bottle_install + MacOS.stubs(:has_apple_developer_tools?).returns(false) + + temporary_bottle_install(TestballBottle.new) do |f| + # Copied directly from test_formula_installer.rb as we expect + # the same behavior + + # Test that things made it into the Keg + assert_predicate f.bin, :directory? + + assert_predicate f.libexec, :directory? + + refute_predicate f.prefix+"main.c", :exist? + + # Test that things make it into the Cellar + keg = Keg.new f.prefix + keg.link + + bin = HOMEBREW_PREFIX+"bin" + assert_predicate bin, :directory? + end + end + + def test_build_tools_error + MacOS.stubs(:has_apple_developer_tools?).returns(false) + + # Testball doesn't have a bottle block, so use it to test this behavior + formula = Testball.new + + refute_predicate formula, :installed? + refute_predicate formula, :bottled? + + installer = FormulaInstaller.new(formula) + + assert_raises(BuildToolsError) do + installer.install + end + + refute_predicate formula, :installed? + end +end diff --git a/Library/Homebrew/test/testball_bottle.rb b/Library/Homebrew/test/testball_bottle.rb new file mode 100644 index 0000000000..27ffd972aa --- /dev/null +++ b/Library/Homebrew/test/testball_bottle.rb @@ -0,0 +1,17 @@ +class TestballBottle < Formula + def initialize(name = "testball_bottle", path = Pathname.new(__FILE__).expand_path, spec = :stable) + self.class.instance_eval do + stable.url "file://#{File.expand_path("..", __FILE__)}/tarballs/testball-0.1.tbz" + stable.sha256 "1dfb13ce0f6143fe675b525fc9e168adb2215c5d5965c9f57306bb993170914f" + stable.bottle do + cellar :any_skip_relocation + root_url "file://#{File.expand_path("..", __FILE__)}/bottles" + sha256 "9abc8ce779067e26556002c4ca6b9427b9874d25f0cafa7028e05b5c5c410cb4" => :yosemite + sha256 "9abc8ce779067e26556002c4ca6b9427b9874d25f0cafa7028e05b5c5c410cb4" => :mavericks + sha256 "9abc8ce779067e26556002c4ca6b9427b9874d25f0cafa7028e05b5c5c410cb4" => :mountain_lion + end + cxxstdlib_check :skip + end + super + end +end