Carlo Cabrera 0404da7ba7
superenv: handle formulae with runtime CPU detection
Some formulae are able to detect the features of the runtime CPU, and
execute code accordingly. This typically entails 1) the detection of
features of the build-time CPU in order to determine the targets that
the compiler can generate code for, and 2) generating code for the
targets that the compiler can support.

Our filtering of optimization flags can cause misdetection of compiler
features, leading to failed builds [1], and miscompilation even when the
build does not fail [2].

Let's try to fix this by allowing formulae to declare
`ENV.runtime_cpu_detection` which skips the filtering of `-march` and
related flags.

I've also skipped the filtering of the optimisation
level, since it seems to me that if upstream maintainers have gone to
the lengths of writing code that detects runtime hardware, they probably
also know better about appropriate `-O` flags to use.

This is a partial list of formulae that should make use of this feature:
1. apache-arrow
2. fftw
3. gromacs
4. open-mpi
5. openblas

Partially resolves Homebrew/homebrew-core#76537.

[1] open-mpi/ompi#8306 and linked issues/PRs
[2] Homebrew/homebrew-core#76537
2021-07-01 16:24:38 +01:00

478 lines
12 KiB
Bash
Executable File

#!/bin/sh
# Make sure this shim uses the same Ruby interpreter that is used by Homebrew.
if [ -z "$HOMEBREW_RUBY_PATH" ]
then
echo "${0##*/}: The build tool has reset ENV; --env=std required." >&2
exit 1
fi
exec "$HOMEBREW_RUBY_PATH" --enable-frozen-string-literal --disable=gems,did_you_mean,rubyopt -x "$0" "$@"
#!/usr/bin/env ruby -W0
require "pathname"
require "set"
require "English"
def mac?
RUBY_PLATFORM[/darwin/]
end
def high_sierra_or_later?
mac? && ENV["HOMEBREW_MACOS_VERSION_NUMERIC"].to_s.to_i >= 101300
end
def linux?
RUBY_PLATFORM[/linux/]
end
class Cmd
CXX_REGEX = /(?:c|g|clang)\+\+/.freeze
attr_reader :config, :prefix, :cellar, :opt, :cachedir, :tmpdir, :sysroot, :deps
attr_reader :archflags, :optflags, :keg_regex, :formula_prefix
def initialize(arg0, args)
@arg0 = arg0
@args = args.freeze
@config = ENV.fetch("HOMEBREW_CCCFG", "")
@prefix = ENV["HOMEBREW_PREFIX"]
@cellar = ENV["HOMEBREW_CELLAR"]
@cachedir = ENV["HOMEBREW_CACHE"]
@opt = ENV["HOMEBREW_OPT"]
@tmpdir = ENV["HOMEBREW_TEMP"]
@sysroot = ENV["HOMEBREW_SDKROOT"]
@archflags = ENV.fetch("HOMEBREW_ARCHFLAGS", "").split
@optflags = ENV.fetch("HOMEBREW_OPTFLAGS", "").split
@deps = Set.new(ENV.fetch("HOMEBREW_DEPENDENCIES", "").split(","))
@formula_prefix = ENV["HOMEBREW_FORMULA_PREFIX"]
# matches opt or cellar prefix and formula name
@keg_regex = %r{(#{Regexp.escape(opt)}|#{Regexp.escape(cellar)})/([\w+-.@]+)}
end
def mode
if @arg0 == "cpp"
:cpp
elsif ["ld", "ld.gold", "gold"].include? @arg0
:ld
elsif @args.include? "-c"
if CXX_REGEX.match?(@arg0)
:cxx
else
:cc
end
elsif @args.include?("-xc++-header") || @args.each_cons(2).include?(["-x", "c++-header"])
:cxx
elsif @args.include? "-E"
:ccE
elsif CXX_REGEX.match?(@arg0)
:cxxld
else
:ccld
end
end
def tool
@tool ||= case @arg0
when "ld" then "ld"
when "gold", "ld.gold" then "ld.gold"
when "cpp" then "cpp"
when /llvm_(clang(\+\+)?)/
"#{ENV["HOMEBREW_PREFIX"]}/opt/llvm/bin/#{Regexp.last_match(1)}"
when /\w\+\+(-\d+(\.\d)?)?$/
case ENV["HOMEBREW_CC"]
when /clang/
"clang++"
when /llvm-gcc/
"llvm-g++-4.2"
when /(g)?cc(-\d+(\.\d)?)?$/
"g++#{Regexp.last_match(2)}"
end
else
# Note that this is a universal fallback, so that we'll always invoke
# HOMEBREW_CC regardless of what name under which the tool was invoked.
if ENV["HOMEBREW_CC"] == "llvm_clang"
"#{ENV["HOMEBREW_PREFIX"]}/opt/llvm/bin/clang"
else
ENV["HOMEBREW_CC"]
end
end
end
def args
if @args.length == 1 && @args[0] == "-v"
# Don't add linker arguments if -v passed as sole option. This stops gcc
# -v with no other arguments from outputting a linker error. Some
# software uses gcc -v (wrongly) to sniff the GCC version.
return @args.dup
end
args = if !refurbish_args? || mode == :ld || configure?
@args.dup
else
refurbished_args
end
if sysroot
if tool == "ld"
args << "-syslibroot" << sysroot
else
args << "-isysroot#{sysroot}" << "--sysroot=#{sysroot}"
end
end
case mode
when :ccld
cflags + args + cppflags + ldflags
when :cxxld
cxxflags + args + cppflags + ldflags
when :cc
cflags + args + cppflags
when :cxx
cxxflags + args + cppflags
when :ccE
args + cppflags
when :cpp
args + cppflags
when :ld
ldflags + args
end
end
def refurbished_args
@lset = Set.new(library_paths + system_library_paths)
@iset = Set.new(isystem_paths + include_paths)
args = []
enum = @args.each
loop do
case arg = enum.next
when "-arch"
if permit_arch_flags?
args << arg << enum.next
else
enum.next
end
when "-m32", "-m64"
args << arg if permit_arch_flags?
when /^-Xarch_/
refurbished = refurbish_arg(enum.next, enum)
unless refurbished.empty?
args << arg
args += refurbished
end
else
args += refurbish_arg(arg, enum)
end
end
args
end
def refurbish_arg(arg, enum)
args = []
case arg
when /^-g\d?$/, /^-gstabs\d+/, "-gstabs+", /^-ggdb\d?/,
/^-mtune=.+/, /^-mcpu=.+/, /^-O[0-9zs]?$/,
"-fast", "-no-cpp-precomp", "-pedantic",
"-pedantic-errors", "-Wno-long-double",
"-Wno-unused-but-set-variable"
when /^-march=.+/
args << arg if runtime_cpu_detection?
when "-fopenmp", "-lgomp", "-mno-fused-madd", "-fforce-addr", "-fno-defer-pop",
"-mno-dynamic-no-pic", "-fearly-inlining", /^-f(?:no-)?inline-functions-called-once/,
/^-finline-limit/, /^-f(?:no-)?check-new/, "-fno-delete-null-pointer-checks",
"-fcaller-saves", "-fthread-jumps", "-fno-reorder-blocks", "-fcse-skip-blocks",
"-frerun-cse-after-loop", "-frerun-loop-opt", "-fcse-follow-jumps",
"-fno-regmove", "-fno-for-scope", "-fno-tree-pre", "-fno-tree-dominator-opts",
"-fuse-linker-plugin", "-frounding-math"
# clang doesn't support these flags
args << arg unless tool =~ /^clang/
when "-Xpreprocessor", "-Xclang"
# used for -Xpreprocessor -fopenmp
args << arg << enum.next
when /-mmacosx-version-min=(\d+)\.(\d+)/
if high_sierra_or_later? && Regexp.last_match(1) == "10" && Regexp.last_match(2).to_i < 9
arg = "-mmacosx-version-min=10.9"
end
args << arg
when "--fast-math"
arg = "-ffast-math" if /^clang/.match?(tool)
args << arg
when "-Wno-deprecated-register"
# older gccs don't support these flags
args << arg unless tool =~ /^g..-4.[02]/
when /^-Wl,-z,defs/
# -Wl,-undefined,error is already the default
when /^-W[alp],/, /^-Wno-/, "-Werror=implicit-function-declaration"
args << arg
when /^-W.*/
# prune warnings
when "-macosx_version_min", "-dylib_install_name"
args << "-Wl,#{arg},#{enum.next}"
when "-multiply_definedsuppress"
args << "-Wl,-multiply_defined,suppress"
when "-undefineddynamic_lookup"
args << "-Wl,-undefined,dynamic_lookup"
when /^-isysroot=/, /^--sysroot=/
if mac?
sdk = arg.split("=")[1..-1].join("=")
# We set the sysroot for macOS SDKs
args << arg unless sdk.downcase.include? "osx"
else
args << arg
end
when /^-isysroot(.+)?/, /^--sysroot(.+)?/
# Support both "-isysrootfoo" and "-isysroot foo" (two arguments)
sdk = chuzzle(Regexp.last_match(1))
if mac?
sdk ||= enum.next
# We set the sysroot for macOS SDKs
args << "-isysroot#{sdk}" unless sdk.downcase.include? "osx"
else
args << arg
args << enum.next unless sdk
end
when "-dylib"
args << "-Wl,#{arg}"
when /^-I(.+)?/
# Support both "-Ifoo" (one argument) and "-I foo" (two arguments)
val = chuzzle(Regexp.last_match(1)) || enum.next
path = canonical_path(val)
args << "-I#{val}" if keep?(path) && @iset.add?(path)
when /^-L(.+)?/
val = chuzzle(Regexp.last_match(1)) || enum.next
path = canonical_path(val)
args << "-L#{val}" if keep?(path) && @lset.add?(path)
else
args << arg
end
args
end
def keep?(path)
# Allow references to self
if formula_prefix && path.start_with?("#{formula_prefix}/")
true
# first two paths: reject references to Cellar or opt paths
# for unspecified dependencies
elsif path.start_with?(cellar) || path.start_with?(opt)
dep = path[keg_regex, 2]
dep && @deps.include?(dep)
elsif path.start_with?(prefix, cachedir, tmpdir)
true
elsif path.start_with?("/opt/local", "/opt/boxen/homebrew", "/opt/X11", "/sw", "/usr/X11")
# ignore MacPorts, Boxen's Homebrew, X11, fink
false
elsif prefix != "/usr/local" && path.start_with?("/usr/local")
# ignore things in /usr/local if Homebrew is in another prefix;
# on macOS, this probably means that the user is on ARM and has an Intel
# Homebrew in /usr/local
false
elsif mac?
true
else
false
end
end
def cflags
args = []
return args if !refurbish_args? && !configure?
args << "-pipe"
args << "-w" unless configure?
args << "-#{ENV["HOMEBREW_OPTIMIZATION_LEVEL"]}"
args.concat(optflags) unless runtime_cpu_detection?
args.concat(archflags)
args << "-std=#{@arg0}" if /c[89]9/.match?(@arg0)
args
end
def cxxflags
args = cflags
args << "-std=c++11" if cxx11?
args << "-stdlib=libc++" if libcxx?
args << "-stdlib=libstdc++" if libstdcxx?
args
end
def cppflags
path_flags("-isystem", isystem_paths) + path_flags("-I", include_paths)
end
def ldflags_mac(args)
case mode
when :ld
args << "-headerpad_max_install_names"
args << "-no_weak_imports" if no_weak_imports?
when :ccld, :cxxld
args << "-Wl,-headerpad_max_install_names"
args << "-Wl,-no_weak_imports" if no_weak_imports?
end
args
end
def ldflags_linux(args)
unless mode == :ld
wl = "-Wl,"
args << "-B#{@opt}/glibc/lib"
end
args += rpath_flags("#{wl}-rpath=", rpath_paths)
args += ["#{wl}--dynamic-linker=#{dynamic_linker_path}"] if dynamic_linker_path
args
end
def ldflags
args = path_flags("-L", library_paths)
if mac?
ldflags_mac(args)
elsif linux?
ldflags_linux(args)
else
args
end
end
def isystem_paths
path_split("HOMEBREW_ISYSTEM_PATHS")
end
def include_paths
path_split("HOMEBREW_INCLUDE_PATHS")
end
def library_paths
path_split("HOMEBREW_LIBRARY_PATHS")
end
def rpath_paths
path_split("HOMEBREW_RPATH_PATHS")
end
def dynamic_linker_path
chuzzle(ENV["HOMEBREW_DYNAMIC_LINKER"])
end
def system_library_paths
paths = ["#{sysroot}/usr/lib"]
paths << "/usr/local/lib" if !sysroot && !ENV["SDKROOT"]
paths
end
def configure?
# configure scripts generated with autoconf 2.61 or later export as_nl
ENV.key? "as_nl"
end
def refurbish_args?
config.include?("O")
end
def cxx11?
config.include?("x")
end
def libcxx?
config.include?("g")
end
def libstdcxx?
config.include?("h")
end
def permit_arch_flags?
config.include?("K")
end
def runtime_cpu_detection?
config.include?("d")
end
def no_weak_imports?
config.include?("w")
end
def canonical_path(path)
path = Pathname.new(path)
path = path.realpath if path.exist?
path.to_s
end
def path_flags(prefix, paths)
paths = paths.uniq.select { |path| File.directory?(path) }
paths.map! { |path| prefix + path }
end
# Unlike {path_flags}, do not prune non-existent directories.
# `formula.lib` for example does not yet exist, but should not be pruned.
def rpath_flags(prefix, paths)
paths.uniq.map { |path| prefix + path }
end
def path_split(key)
ENV.fetch(key, "").split(File::PATH_SEPARATOR)
end
def chuzzle(val)
return val if val.nil?
val = val.chomp
return val unless val.empty?
end
end
def log(basename, argv, tool, args)
return unless ENV.key?("HOMEBREW_CC_LOG_PATH")
adds = args - argv
dels = argv - args
s = +""
s << "#{basename} called with: #{argv.join(" ")}\n"
s << "superenv removed: #{dels.join(" ")}\n" unless dels.empty?
s << "superenv added: #{adds.join(" ")}\n" unless adds.empty?
s << "superenv executed: #{tool} #{args.join(" ")}\n\n"
File.open("#{ENV["HOMEBREW_CC_LOG_PATH"]}.cc", "a+") { |f| f.write(s) }
end
def remove_superbin_from_path(paths)
superbin = Pathname.new(__FILE__).dirname.realpath
paths.reject do |x|
path = Pathname.new(x)
path.directory? && path.realpath == superbin
end
end
if __FILE__ == $PROGRAM_NAME
##################################################################### sanity
if (cc = ENV["HOMEBREW_CC"]).nil? || cc.empty? || cc == "cc"
# those values are not allowed
ENV["HOMEBREW_CC"] = "clang"
end
####################################################################### main
dirname, basename = File.split($PROGRAM_NAME)
cmd = Cmd.new(basename, ARGV)
tool = cmd.tool
args = cmd.args
log(basename, ARGV, tool, args)
args << { close_others: false }
if mac?
exec "#{dirname}/xcrun", tool, *args
else
paths = ENV["PATH"].split(":")
paths = remove_superbin_from_path(paths)
paths.unshift "#{ENV["HOMEBREW_PREFIX"]}/bin"
ENV["PATH"] = paths.join(":")
exec tool, *args
end
end