class MiniPortile
Constants
- DEFAULT_TIMEOUT
- KEYRING_NAME
- TAR_EXECUTABLES
- VERSION
Attributes
Public Class Methods
# File lib/mini_portile2/mini_portile.rb, line 54 def self.darwin? target_os =~ /darwin/ end
# File lib/mini_portile2/mini_portile.rb, line 58 def self.freebsd? target_os =~ /freebsd/ end
# File lib/mini_portile2/mini_portile.rb, line 66 def self.linux? target_os =~ /linux/ end
GNU MinGW compiled Ruby?
# File lib/mini_portile2/mini_portile.rb, line 45 def self.mingw? target_os =~ /mingw/ end
MS Visual-C compiled Ruby?
# File lib/mini_portile2/mini_portile.rb, line 50 def self.mswin? target_os =~ /mswin/ end
# File lib/mini_portile2/mini_portile.rb, line 82 def self.native_path(path) path = File.expand_path(path) if File::ALT_SEPARATOR path.tr(File::SEPARATOR, File::ALT_SEPARATOR) else path end end
# File lib/mini_portile2/mini_portile.rb, line 100 def initialize(name, version, **kwargs) @name = name @version = version @target = 'ports' @files = [] @patch_files = [] @log_files = {} @logger = kwargs[:logger] || STDOUT @source_directory = nil @cc_command = kwargs[:cc_command] || kwargs[:gcc_command] @cxx_command = kwargs[:cxx_command] @make_command = kwargs[:make_command] @open_timeout = kwargs[:open_timeout] || DEFAULT_TIMEOUT @read_timeout = kwargs[:read_timeout] || DEFAULT_TIMEOUT @original_host = @host = detect_host end
# File lib/mini_portile2/mini_portile.rb, line 62 def self.openbsd? target_os =~ /openbsd/ end
# File lib/mini_portile2/mini_portile.rb, line 91 def self.posix_path(path) path = File.expand_path(path) if File::ALT_SEPARATOR "/" + path.tr(File::ALT_SEPARATOR, File::SEPARATOR).tr(":", File::SEPARATOR) else path end end
# File lib/mini_portile2/mini_portile.rb, line 70 def self.solaris? target_os =~ /solaris/ end
# File lib/mini_portile2/mini_portile.rb, line 78 def self.target_cpu RbConfig::CONFIG['target_cpu'] end
# File lib/mini_portile2/mini_portile.rb, line 74 def self.target_os RbConfig::CONFIG['target_os'] end
# File lib/mini_portile2/mini_portile.rb, line 40 def self.windows? target_os =~ /mswin|mingw/ end
Public Instance Methods
# File lib/mini_portile2/mini_portile.rb, line 243 def activate vars = { 'PATH' => File.join(port_path, 'bin'), 'CPATH' => include_path, 'LIBRARY_PATH' => lib_path, }.reject { |env, path| !File.directory?(path) } output "Activating #{@name} #{@version} (from #{port_path})..." vars.each do |var, path| full_path = native_path(path) # save current variable value old_value = ENV[var] || '' unless old_value.include?(full_path) ENV[var] = "#{full_path}#{File::PATH_SEPARATOR}#{old_value}" end end # rely on LDFLAGS when cross-compiling if File.exist?(lib_path) && (@host != @original_host) full_path = native_path(lib_path) old_value = ENV.fetch("LDFLAGS", "") unless old_value.include?(full_path) ENV["LDFLAGS"] = "-L#{full_path} #{old_value}".strip end end end
# File lib/mini_portile2/mini_portile.rb, line 144 def apply_patch(patch_file) ( # Not a class variable because closures will capture self. @apply_patch ||= case when which('git') lambda { |file| message "Running git apply with #{file}... " Dir.mktmpdir do |tmp_git_dir| execute('patch', ["git", "--git-dir=#{tmp_git_dir}", "--work-tree=.", "apply", "--whitespace=warn", file], :initial_message => false) end } when which('patch') lambda { |file| message "Running patch with #{file}... " execute('patch', ["patch", "-p1", "-i", file], :initial_message => false) } else raise "Failed to complete patch task; patch(1) or git(1) is required." end ).call(patch_file) end
# File lib/mini_portile2/mini_portile.rb, line 375 def cc_cmd (ENV["CC"] || @cc_command || RbConfig::CONFIG["CC"] || "gcc").dup end
# File lib/mini_portile2/mini_portile.rb, line 193 def compile execute('compile', make_cmd) end
# File lib/mini_portile2/mini_portile.rb, line 178 def configure return if configured? FileUtils.mkdir_p(tmp_path) cache_file = File.join(tmp_path, 'configure.options_cache') File.open(cache_file, "w") { |f| f.write computed_options.to_s } command = Array(File.join((source_directory || "."), "configure")) if RUBY_PLATFORM=~/mingw|mswin/ # Windows doesn't recognize the shebang. command.unshift("sh") end execute('configure', command + computed_options, altlog: "config.log") end
# File lib/mini_portile2/mini_portile.rb, line 174 def configure_options @configure_options ||= configure_defaults end
# File lib/mini_portile2/mini_portile.rb, line 210 def configured? configure = File.join((source_directory || work_path), 'configure') makefile = File.join(work_path, 'Makefile') cache_file = File.join(tmp_path, 'configure.options_cache') stored_options = File.exist?(cache_file) ? File.read(cache_file) : "" current_options = computed_options.to_s (current_options == stored_options) && newer?(makefile, configure) end
# File lib/mini_portile2/mini_portile.rb, line 228 def cook if source_directory prepare_build_directory else download unless downloaded? extract patch end configure unless configured? compile install unless installed? return true end
# File lib/mini_portile2/mini_portile.rb, line 380 def cxx_cmd (ENV["CXX"] || @cxx_command || RbConfig::CONFIG["CXX"] || "g++").dup end
# File lib/mini_portile2/mini_portile.rb, line 130 def download files_hashs.each do |file| download_file(file[:url], file[:local_path]) verify_file(file) end end
# File lib/mini_portile2/mini_portile.rb, line 202 def downloaded? missing = files_hashs.detect do |file| !File.exist?(file[:local_path]) end missing ? false : true end
# File lib/mini_portile2/mini_portile.rb, line 137 def extract files_hashs.each do |file| verify_file(file) extract_file(file[:local_path], tmp_path) end end
# File lib/mini_portile2/mini_portile.rb, line 367 def include_path File.join(path, "include") end
# File lib/mini_portile2/mini_portile.rb, line 197 def install return if installed? execute('install', %Q(#{make_cmd} install)) end
# File lib/mini_portile2/mini_portile.rb, line 221 def installed? makefile = File.join(work_path, 'Makefile') target_dir = Dir.glob("#{port_path}/*").find { |d| File.directory?(d) } newer?(target_dir, makefile) end
# File lib/mini_portile2/mini_portile.rb, line 371 def lib_path File.join(path, "lib") end
# File lib/mini_portile2/mini_portile.rb, line 384 def make_cmd (ENV["MAKE"] || @make_command || ENV["make"] || "make").dup end
pkg: the pkg-config file name (without the .pc extension) dir: inject the directory path for the pkg-config file (probably only useful for tests) static: the name of the static library archive (without the “lib” prefix or the file extension), or nil for dynamic linking
we might be able to be terribly clever and infer the name of the static archive file, but unfortunately projects have so much freedom in what they can report (for name, for libs, etc.) that it feels unreliable to try to do so, so I’m preferring to just have the developer make it explicit.
# File lib/mini_portile2/mini_portile.rb, line 282 def mkmf_config(pkg: nil, dir: nil, static: nil) require "mkmf" if pkg dir ||= File.join(lib_path, "pkgconfig") pcfile = File.join(dir, "#{pkg}.pc") unless File.exist?(pcfile) raise ArgumentError, "pkg-config file '#{pcfile}' does not exist" end output "Configuring MakeMakefile for #{File.basename(pcfile)} (in #{File.dirname(pcfile)})\n" # on macos, pkg-config will not return --cflags without this ENV["PKG_CONFIG_ALLOW_SYSTEM_CFLAGS"] = "t" # append to PKG_CONFIG_PATH as we go, so later pkg-config files can depend on earlier ones ENV["PKG_CONFIG_PATH"] = [ENV["PKG_CONFIG_PATH"], dir].compact.join(File::PATH_SEPARATOR) incflags = minimal_pkg_config(pcfile, "cflags-only-I") cflags = minimal_pkg_config(pcfile, "cflags-only-other") if static ldflags = minimal_pkg_config(pcfile, "libs-only-L", "static") libflags = minimal_pkg_config(pcfile, "libs-only-l", "static") else ldflags = minimal_pkg_config(pcfile, "libs-only-L") libflags = minimal_pkg_config(pcfile, "libs-only-l") end else output "Configuring MakeMakefile for #{@name} #{@version} (from #{path})\n" lib_name = name.sub(/\Alib/, "") # TODO: use delete_prefix when we no longer support ruby 2.4 incflags = Dir.exist?(include_path) ? "-I#{include_path}" : "" cflags = "" ldflags = Dir.exist?(lib_path) ? "-L#{lib_path}" : "" libflags = Dir.exist?(lib_path) ? "-l#{lib_name}" : "" end if static libdir = lib_path if pcfile pcfile_libdir = minimal_pkg_config(pcfile, "variable=libdir").strip libdir = pcfile_libdir unless pcfile_libdir.empty? end # # keep track of the libraries we're statically linking against, and fix up ldflags and # libflags to make sure we link statically against the recipe's libaries. # # this avoids the unintentionally dynamically linking against system libraries, and makes sure # that if multiple pkg-config files reference each other that we are able to intercept flags # from dependent packages that reference the static archive. # $MINI_PORTILE_STATIC_LIBS[static] = libdir static_ldflags = $MINI_PORTILE_STATIC_LIBS.values.map { |v| "-L#{v}" } static_libflags = $MINI_PORTILE_STATIC_LIBS.keys.map { |v| "-l#{v}" } # remove `-L#{libdir}` and `-lfoo`. we don't need them since we link against the static # archive using the full path. ldflags = ldflags.shellsplit.reject { |f| static_ldflags.include?(f) }.shelljoin libflags = libflags.shellsplit.reject { |f| static_libflags.include?(f) }.shelljoin # prepend the full path to the static archive to the linker flags static_archive = File.join(libdir, "lib#{static}.#{$LIBEXT}") libflags = [static_archive, libflags].join(" ").strip end # prefer this package by prepending to search paths and library flags # # convert the ldflags into a list of directories and append to $LIBPATH (instead of just using # $LDFLAGS) to ensure we get the `-Wl,-rpath` linker flag for re-finding shared libraries. $INCFLAGS = [incflags, $INCFLAGS].join(" ").strip libpaths = ldflags.shellsplit.map { |f| f.sub(/\A-L/, "") } $LIBPATH = libpaths | $LIBPATH $libs = [libflags, $libs].join(" ").strip # prefer this package's compiler flags by appending them to the command line $CFLAGS = [$CFLAGS, cflags].join(" ").strip $CXXFLAGS = [$CXXFLAGS, cflags].join(" ").strip end
# File lib/mini_portile2/mini_portile.rb, line 167 def patch @patch_files.each do |full_path| next unless File.exist?(full_path) apply_patch(full_path) end end
# File lib/mini_portile2/mini_portile.rb, line 363 def path File.expand_path(port_path) end
# File lib/mini_portile2/mini_portile.rb, line 123 def prepare_build_directory raise "source_directory is not set" if source_directory.nil? output "Building #{@name} from source at '#{source_directory}'" FileUtils.mkdir_p(File.join(tmp_path, [name, version].join("-"))) FileUtils.rm_rf(port_path) # make sure we always re-install end
# File lib/mini_portile2/mini_portile.rb, line 119 def source_directory=(path) @source_directory = posix_path(path) end
Private Instance Methods
# File lib/mini_portile2/mini_portile.rb, line 406 def archives_path "#{@target}/archives" end
# File lib/mini_portile2/mini_portile.rb, line 426 def computed_options [ configure_options, # customized or default options configure_prefix, # installation target ].flatten end
# File lib/mini_portile2/mini_portile.rb, line 414 def configure_defaults [ "--host=#{@host}", # build for specific target (host) "--enable-static", # build static library "--disable-shared" # disable generation of shared object ] end
# File lib/mini_portile2/mini_portile.rb, line 422 def configure_prefix "--prefix=#{File.expand_path(port_path)}" end
# File lib/mini_portile2/mini_portile.rb, line 528 def detect_host return @detect_host if defined?(@detect_host) begin ENV["LC_ALL"], old_lc_all = "C", ENV["LC_ALL"] output = `#{gcc_cmd} -v 2>&1` if m = output.match(/^Target\: (.*)$/) @detect_host = m[1] else @detect_host = nil end @detect_host ensure ENV["LC_ALL"] = old_lc_all end end
Slighly modified from RubyInstaller uri_ext, Rubinius configure and adaptations of Wayne’s RailsInstaller
# File lib/mini_portile2/mini_portile.rb, line 650 def download_file(url, full_path, count = 3) return if File.exist?(full_path) uri = URI.parse(url) case uri.scheme.downcase when /ftp/ download_file_ftp(uri, full_path) when /http|https/ download_file_http(url, full_path, count) when /file/ download_file_file(uri, full_path) else raise ArgumentError.new("Unsupported protocol for #{url}") end rescue Exception => e File.unlink full_path if File.exist?(full_path) raise e end
# File lib/mini_portile2/mini_portile.rb, line 723 def download_file_file(uri, full_path) FileUtils.mkdir_p File.dirname(full_path) FileUtils.cp uri.path, full_path end
# File lib/mini_portile2/mini_portile.rb, line 728 def download_file_ftp(uri, full_path) require "net/ftp" filename = File.basename(uri.path) with_tempfile(filename, full_path) do |temp_file| total = 0 params = { :content_length_proc => lambda{|length| total = length }, :progress_proc => lambda{|bytes| new_progress = (bytes * 100) / total message "\rDownloading %s (%3d%%) " % [filename, new_progress] }, :open_timeout => @open_timeout, :read_timeout => @read_timeout, } if ENV["ftp_proxy"] _, userinfo, _p_host, _p_port = URI.split(ENV['ftp_proxy']) if userinfo proxy_user, proxy_pass = userinfo.split(/:/).map{|s| CGI.unescape(s) } params[:proxy_http_basic_authentication] = [ENV['ftp_proxy'], proxy_user, proxy_pass] end end OpenURI.open_uri(uri, 'rb', params) do |io| temp_file << io.read end output end rescue LoadError raise LoadError, "Ruby #{RUBY_VERSION} does not provide the net-ftp gem, please add it as a dependency if you need to use FTP" rescue Net::FTPError return false end
# File lib/mini_portile2/mini_portile.rb, line 669 def download_file_http(url, full_path, count = 3) filename = File.basename(full_path) with_tempfile(filename, full_path) do |temp_file| total = 0 params = { "Accept-Encoding" => 'identity', :content_length_proc => lambda{|length| total = length }, :progress_proc => lambda{|bytes| if total new_progress = (bytes * 100) / total message "\rDownloading %s (%3d%%) " % [filename, new_progress] else # Content-Length is unavailable because Transfer-Encoding is chunked message "\rDownloading %s " % [filename] end }, :open_timeout => @open_timeout, :read_timeout => @read_timeout, } proxy_uri = URI.parse(url).scheme.downcase == 'https' ? ENV["https_proxy"] : ENV["http_proxy"] if proxy_uri _, userinfo, _p_host, _p_port = URI.split(proxy_uri) if userinfo proxy_user, proxy_pass = userinfo.split(/:/).map{|s| CGI.unescape(s) } params[:proxy_http_basic_authentication] = [proxy_uri, proxy_user, proxy_pass] end end begin OpenURI.open_uri(url, 'rb', params) do |io| temp_file << io.read end output rescue OpenURI::HTTPRedirect => redirect raise "Too many redirections for the original URL, halting." if count <= 0 count = count - 1 return download_file(redirect.url, full_path, count-1) rescue => e count = count - 1 @logger.puts "#{count} retrie(s) left for #{filename} (#{e.message})" if count > 0 sleep 1 return download_file_http(url, full_path, count) end output e.message return false end end end
command could be an array of args, or one string containing a command passed to the shell. See Process.spawn for more information.
# File lib/mini_portile2/mini_portile.rb, line 580 def execute(action, command, command_opts={}) opt_message = command_opts.fetch(:initial_message, true) opt_debug = command_opts.fetch(:debug, false) opt_cd = command_opts.fetch(:cd) { work_path } opt_env = command_opts.fetch(:env) { Hash.new } opt_altlog = command_opts.fetch(:altlog, nil) log_out = log_file(action) Dir.chdir(opt_cd) do output "DEBUG: env is #{opt_env.inspect}" if opt_debug output "DEBUG: command is #{command.inspect}" if opt_debug message "Running '#{action}' for #{@name} #{@version}... " if opt_message if Process.respond_to?(:spawn) && ! RbConfig.respond_to?(:java) options = {[:out, :err]=>[log_out, "a"]} output "DEBUG: options are #{options.inspect}" if opt_debug args = [opt_env, command, options].flatten pid = spawn(*args) Process.wait(pid) else env_args = opt_env.map { |k,v| "#{k}=#{v}".shellescape }.join(" ") c = if command.kind_of?(Array) command.map(&:shellescape).join(" ") else command end redirected = %Q{env #{env_args} #{c} > #{log_out.shellescape} 2>&1} output "DEBUG: final command is #{redirected.inspect}" if opt_debug system redirected end if $?.success? output "OK" return true else output "ERROR. Please review logs to see what happened:\n" [log_out, opt_altlog].compact.each do |log| next unless File.exist?(log) output("----- contents of '#{log}' -----") output(File.read(log)) output("----- end of file -----") end raise "Failed to complete #{action} task" end end end
# File lib/mini_portile2/mini_portile.rb, line 570 def extract_file(file, target) filename = File.basename(file) FileUtils.mkdir_p target message "Extracting #{filename} into #{target}... " execute('extract', tar_command(file, target) , {:cd => Dir.pwd, :initial_message => false}) end
# File lib/mini_portile2/mini_portile.rb, line 433 def files_hashs @files.map do |file| hash = case file when String { :url => file } when Hash file.dup else raise ArgumentError, "files must be an Array of Stings or Hashs" end url = hash.fetch(:url){ raise ArgumentError, "no url given" } filename = File.basename(url) hash[:local_path] = File.join(archives_path, filename) hash end end
# File lib/mini_portile2/mini_portile.rb, line 504 def log_file(action) @log_files[action] ||= File.expand_path("#{action}.log", tmp_path).tap { |file| File.unlink(file) if File.exist?(file) } end
print out a message with the logger
# File lib/mini_portile2/mini_portile.rb, line 637 def message(text) @logger.print text @logger.flush end
this minimal version of pkg_config is based on ruby 29dc9378 (2023-01-09)
specifically with the fix from b90e56e6 to support multiple pkg-config options, and removing code paths that aren’t helpful for mini-portile’s use case of parsing pc files.
# File lib/mini_portile2/mini_portile.rb, line 777 def minimal_pkg_config(pkg, *pcoptions) if pcoptions.empty? raise ArgumentError, "no pkg-config options are given" end if ($PKGCONFIG ||= (pkgconfig = MakeMakefile.with_config("pkg-config") {MakeMakefile.config_string("PKG_CONFIG") || "pkg-config"}) && MakeMakefile.find_executable0(pkgconfig) && pkgconfig) pkgconfig = $PKGCONFIG else raise RuntimeError, "pkg-config is not found" end pcoptions = Array(pcoptions).map { |o| "--#{o}" } response = IO.popen([pkgconfig, *pcoptions, pkg], err:[:child, :out], &:read) raise RuntimeError, response unless $?.success? response.strip end
# File lib/mini_portile2/mini_portile.rb, line 390 def native_path(path) MiniPortile.native_path(path) end
# File lib/mini_portile2/mini_portile.rb, line 628 def newer?(target, checkpoint) if (target && File.exist?(target)) && (checkpoint && File.exist?(checkpoint)) File.mtime(target) > File.mtime(checkpoint) else false end end
print out a message using the logger but return to a new line
# File lib/mini_portile2/mini_portile.rb, line 643 def output(text = "") @logger.puts text @logger.flush end
# File lib/mini_portile2/mini_portile.rb, line 402 def port_path "#{@target}/#{@host}/#{@name}/#{@version}" end
# File lib/mini_portile2/mini_portile.rb, line 394 def posix_path(path) MiniPortile.posix_path(path) end
# File lib/mini_portile2/mini_portile.rb, line 556 def tar_command(file, target) case File.extname(file) when '.gz', '.tgz' [tar_exe, 'xzf', file, '-C', target] when '.bz2', '.tbz2' [tar_exe, 'xjf', file, '-C', target] when '.xz' # NOTE: OpenBSD's tar command does not support the -J option "xzcat #{file.shellescape} | #{tar_exe.shellescape} xf - -C #{target.shellescape}" else [tar_exe, 'xf', file, '-C', target] end end
# File lib/mini_portile2/mini_portile.rb, line 548 def tar_exe @@tar_exe ||= begin TAR_EXECUTABLES.find { |c| which(c) } or raise("tar not found - please make sure that one of the following commands is in the PATH: #{TAR_EXECUTABLES.join(", ")}") end end
# File lib/mini_portile2/mini_portile.rb, line 398 def tmp_path "tmp/#{@host}/ports/#{@name}/#{@version}" end
# File lib/mini_portile2/mini_portile.rb, line 453 def verify_file(file) if file.has_key?(:gpg) gpg = file[:gpg] signature_url = gpg[:signature_url] || "#{file[:url]}.asc" signature_file = file[:local_path] + ".asc" # download the signature file download_file(signature_url, signature_file) gpg_exe = which('gpg2') || which('gpg') || raise("Neither GPG nor GPG2 is installed") # import the key into our own keyring gpg_error = nil gpg_status = Open3.popen3(gpg_exe, "--status-fd", "1", "--no-default-keyring", "--keyring", KEYRING_NAME, "--import") do |gpg_in, gpg_out, gpg_err, _thread| gpg_in.write gpg[:key] gpg_in.close gpg_error = gpg_err.read gpg_out.read end key_ids = gpg_status.scan(/\[GNUPG:\] IMPORT_OK \d+ (?<key_id>[0-9a-f]+)/i).map(&:first) raise "invalid gpg key provided:\n#{gpg_error}" if key_ids.empty? begin # verify the signature against our keyring gpg_status, gpg_error, _status = Open3.capture3(gpg_exe, "--status-fd", "1", "--no-default-keyring", "--keyring", KEYRING_NAME, "--verify", signature_file, file[:local_path]) raise "signature mismatch:\n#{gpg_error}" unless gpg_status.match(/^\[GNUPG:\] VALIDSIG/) ensure # remove the key from our keyring key_ids.each do |key_id| IO.popen([gpg_exe, "--batch", "--yes", "--no-default-keyring", "--keyring", KEYRING_NAME, "--delete-keys", key_id], &:read) raise "unable to delete the imported key" unless $?.exitstatus==0 end end else digest = case when exp=file[:sha256] then Digest::SHA256 when exp=file[:sha1] then Digest::SHA1 when exp=file[:md5] then Digest::MD5 end if digest is = digest.file(file[:local_path]).hexdigest unless is == exp.downcase raise "Downloaded file '#{file[:local_path]}' has wrong hash: expected: #{exp} is: #{is}" end end end end
From: stackoverflow.com/a/5471032/7672 Thanks, Mislav!
Cross-platform way of finding an executable in the $PATH.
which('ruby') #=> /usr/bin/ruby
# File lib/mini_portile2/mini_portile.rb, line 517 def which(cmd) exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : [''] ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| exts.each { |ext| exe = File.join(path, "#{cmd}#{ext}") return exe if File.executable? exe } end return nil end
# File lib/mini_portile2/mini_portile.rb, line 761 def with_tempfile(filename, full_path) temp_file = Tempfile.new("download-#{filename}") temp_file.binmode yield temp_file temp_file.close File.unlink full_path if File.exist?(full_path) FileUtils.mkdir_p File.dirname(full_path) FileUtils.mv temp_file.path, full_path, :force => true end
# File lib/mini_portile2/mini_portile.rb, line 410 def work_path Dir.glob("#{tmp_path}/*").find { |d| File.directory?(d) } end