├── examples ├── .gitignore ├── zlib-1.2.8.tar.gz ├── c-ares-1.7.5.tar.gz ├── libiconv-patches │ └── 1-simple.patch └── Rakefile ├── test ├── assets │ ├── gpg-fixtures │ │ ├── data │ │ ├── data.asc │ │ └── data.invalid.asc │ ├── test-cmake-1.0 │ │ ├── hello.c │ │ └── CMakeLists.txt │ ├── git │ │ └── config │ ├── test-download-archive.tar.gz │ ├── patch 1.diff │ ├── test mini portile-1.0.0 │ │ └── configure │ └── pkgconf │ │ ├── libxslt │ │ ├── libexslt.pc │ │ └── libxslt.pc │ │ └── libxml2 │ │ └── libxml-2.0.pc ├── test_recipe.rb ├── test_execute.rb ├── helper.rb ├── test_download.rb ├── test_activate.rb ├── test_proxy.rb ├── test_cook.rb ├── test_mkmf_config.rb ├── test_digest.rb └── test_cmake.rb ├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ ├── upstream.yml │ ├── downstream.yml │ └── ci.yml ├── lib ├── mini_portile2 │ ├── version.rb │ ├── mini_portile_cmake.rb │ └── mini_portile.rb └── mini_portile2.rb ├── Rakefile ├── Gemfile ├── SECURITY.md ├── LICENSE.txt ├── mini_portile2.gemspec ├── README.md └── CHANGELOG.md /examples/.gitignore: -------------------------------------------------------------------------------- 1 | ports 2 | tmp 3 | -------------------------------------------------------------------------------- /test/assets/gpg-fixtures/data: -------------------------------------------------------------------------------- 1 | test 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | Gemfile.lock 3 | pkg 4 | ports 5 | tmp 6 | mkmf.log 7 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: flavorjones 2 | tidelift: rubygems/mini_portile2 3 | -------------------------------------------------------------------------------- /lib/mini_portile2/version.rb: -------------------------------------------------------------------------------- 1 | class MiniPortile 2 | VERSION = "2.8.9" 3 | end 4 | -------------------------------------------------------------------------------- /test/assets/test-cmake-1.0/hello.c: -------------------------------------------------------------------------------- 1 | int main(int argc, char** argv) 2 | { 3 | return 0 ; 4 | } 5 | -------------------------------------------------------------------------------- /examples/zlib-1.2.8.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flavorjones/mini_portile/HEAD/examples/zlib-1.2.8.tar.gz -------------------------------------------------------------------------------- /examples/c-ares-1.7.5.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flavorjones/mini_portile/HEAD/examples/c-ares-1.7.5.tar.gz -------------------------------------------------------------------------------- /test/assets/git/config: -------------------------------------------------------------------------------- 1 | [core] 2 | whitespace=tab-in-indent,-indent-with-non-tab 3 | [apply] 4 | whitespace=fix 5 | -------------------------------------------------------------------------------- /test/assets/test-download-archive.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flavorjones/mini_portile/HEAD/test/assets/test-download-archive.tar.gz -------------------------------------------------------------------------------- /lib/mini_portile2.rb: -------------------------------------------------------------------------------- 1 | require "mini_portile2/version" 2 | require "mini_portile2/mini_portile" 3 | require "mini_portile2/mini_portile_cmake" 4 | -------------------------------------------------------------------------------- /test/assets/patch 1.diff: -------------------------------------------------------------------------------- 1 | diff --git "a/patch 1.txt" "b/patch 1.txt" 2 | new file mode 100644 3 | index 0000000..70885e4 4 | --- /dev/null 5 | +++ "b/patch 1.txt" 6 | @@ -0,0 +1 @@ 7 | + change 1 8 | -------------------------------------------------------------------------------- /test/assets/test mini portile-1.0.0/configure: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | ruby -e "p ARGV" -- "$@" > configure.txt 5 | 6 | cat <Makefile 7 | all: 8 | ruby -e "p ARGV" -- "\$@" > compile.txt 9 | install: 10 | ruby -e "p ARGV" -- "\$@" > install.txt 11 | EOT 12 | -------------------------------------------------------------------------------- /test/assets/gpg-fixtures/data.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP SIGNATURE----- 2 | Version: GnuPG v1 3 | 4 | iJwEAAECAAYFAlcFOD8ACgkQZeg+SWTDcLNIswP/XvVRoJ+eQ2u2v+WjXdBBKBSW 5 | pzM216aJPRBxPl98xNUUKjqga+tjKmIHJn5T4CIxHqis1toPxtE5tKnc6cVO1aqY 6 | bCUfkWyt/A3qRHQuniRUWSBKZWdk+j3AopTpd3i/r/s0pDj3bMHJ7bDOTsEskNcM 7 | KpgFfNM1ieFRQmIWPWg= 8 | =kbKc 9 | -----END PGP SIGNATURE----- 10 | -------------------------------------------------------------------------------- /test/assets/pkgconf/libxslt/libexslt.pc: -------------------------------------------------------------------------------- 1 | prefix=/foo/libxslt/1.1.38 2 | exec_prefix=${prefix} 3 | libdir=/foo/libxslt/1.1.38/lib 4 | includedir=${prefix}/include 5 | 6 | 7 | Name: libexslt 8 | Version: 0.8.21 9 | Description: EXSLT Extension library 10 | Requires: libxml-2.0, libxslt 11 | Cflags: -I${includedir} 12 | Libs: -L${libdir} -lexslt 13 | Libs.private: -lm 14 | -------------------------------------------------------------------------------- /test/assets/test-cmake-1.0/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # CMakeLists files in this project can 2 | # refer to the root source directory of the project as ${HELLO_SOURCE_DIR} and 3 | # to the root binary directory of the project as ${HELLO_BINARY_DIR}. 4 | cmake_minimum_required (VERSION 3.5) 5 | project (HELLO) 6 | add_executable (hello hello.c) 7 | install (TARGETS hello DESTINATION bin) 8 | -------------------------------------------------------------------------------- /test/assets/gpg-fixtures/data.invalid.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP SIGNATURE----- 2 | Version: GnuPG v1 3 | 4 | iJwEAQECAAYFAlcFLEgACgkQZeg+SWTDcLPVwgQAg8KTI91Ryx38YplzgWV9tUPj 5 | o7J7IEzb8faE7m2mgtq8m62DvA4h/PJzmbh1EJJ4VkO+A4O2LVh/bTgnyYXv+kMu 6 | sEmvK35PnAC8r7pv98VSbMEXyV/rK3+uGhTvnXZYkULvMVYkN/EHIh2bCQJ3R14X 7 | MY8El95QST8/dR/yBkw= 8 | =qbod 9 | -----END PGP SIGNATURE----- 10 | -------------------------------------------------------------------------------- /test/assets/pkgconf/libxslt/libxslt.pc: -------------------------------------------------------------------------------- 1 | prefix=/foo/libxslt/1.1.38 2 | exec_prefix=${prefix} 3 | libdir=/foo/libxslt/1.1.38/lib 4 | includedir=${prefix}/include 5 | 6 | 7 | Name: libxslt 8 | Version: 1.1.38 9 | Description: XSLT library version 2. 10 | Requires: libxml-2.0 11 | Cflags: -I${includedir} -Wno-deprecated-enum-enum-conversion 12 | Libs: -L${libdir} -lxslt 13 | Libs.private: -lm 14 | -------------------------------------------------------------------------------- /test/assets/pkgconf/libxml2/libxml-2.0.pc: -------------------------------------------------------------------------------- 1 | prefix=/foo/libxml2/2.11.5 2 | exec_prefix=${prefix} 3 | libdir=/foo/libxml2/2.11.5/lib 4 | includedir=${prefix}/include 5 | modules=1 6 | 7 | Name: libXML 8 | Version: 2.11.5 9 | Description: libXML library version2. 10 | Requires: 11 | Libs: -L${libdir} -lxml2 12 | Libs.private: -L/foo/zlib/1.3/lib -lz -lm 13 | Cflags: -I${includedir}/libxml2 -ggdb3 14 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "rake/clean" 2 | require "bundler/gem_tasks" 3 | require "rake/testtask" 4 | 5 | Rake::TestTask.new("test:unit") 6 | 7 | namespace :test do 8 | desc "Test MiniPortile by compiling examples" 9 | task :examples do 10 | Dir.chdir("examples") do 11 | sh "rake ports:all" 12 | end 13 | end 14 | end 15 | 16 | task :clean do 17 | FileUtils.rm_rf ["examples/ports", "examples/tmp"], :verbose => true 18 | end 19 | 20 | desc "Run all tests" 21 | task :test => ["test:unit", "test:examples"] 22 | 23 | task :default => [:test] 24 | -------------------------------------------------------------------------------- /examples/libiconv-patches/1-simple.patch: -------------------------------------------------------------------------------- 1 | This file tests the 'patch' functionality, as well as working around a 2 | libiconv compilation issue with glibc >= 2.16. 3 | 4 | --- ./srclib/stat.c 2017-01-01 18:02:22.000000000 -0500 5 | +++ ./srclib/stat.c 2022-10-18 20:27:18.831323906 -0400 6 | @@ -48,6 +48,8 @@ 7 | return stat (filename, buf); 8 | } 9 | 10 | +/* this was patched! */ 11 | + 12 | /* Specification. */ 13 | /* Write "sys/stat.h" here, not , otherwise OSF/1 5.1 DTK cc 14 | eliminates this include because of the preliminary #include 15 | -------------------------------------------------------------------------------- /test/test_recipe.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper', __FILE__) 2 | 3 | class TestRecipe < TestCase 4 | def test_path 5 | recipe = MiniPortile.new("libfoo", "1.0.0") 6 | assert_equal(File.expand_path(File.join(recipe.target, recipe.host, recipe.name, recipe.version)), recipe.path) 7 | end 8 | 9 | def test_lib_path 10 | recipe = MiniPortile.new("libfoo", "1.0.0") 11 | assert_equal(File.join(recipe.path, "lib"), recipe.lib_path) 12 | end 13 | 14 | def test_include_path 15 | recipe = MiniPortile.new("libfoo", "1.0.0") 16 | assert_equal(File.join(recipe.path, "include"), recipe.include_path) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "minitar", "0.9" 7 | gem "minitest", "~> 5.15" # open range for ruby 2.3 support 8 | gem "minitest-hooks", "1.5.2" 9 | gem "rake", "13.2.1" 10 | if RUBY_VERSION >= "3.4" 11 | gem "webrick", git: "https://github.com/ruby/webrick" # shouldn't be necessary to pin once webrick 1.8.2 or 1.9.0 is released 12 | else 13 | gem "webrick" 14 | end 15 | 16 | gem "net-ftp" if Gem::Requirement.new("> 3.1.0.dev").satisfied_by?(Gem::Version.new(RUBY_VERSION)) 17 | gem "logger", "1.6.5" if Gem::Requirement.new("> 3.5.0.dev").satisfied_by?(Gem::Version.new(RUBY_VERSION)) 18 | end 19 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security and Vulnerability Reporting 2 | 3 | The mini_portile core contributors take security very seriously and investigate all reported vulnerabilities. 4 | 5 | If you would like to report a vulnerablity or have a security concern regarding mini_portile, please [report it via Tidelift](https://tidelift.com/security). 6 | 7 | Your report will be acknowledged within 48 hours, and you'll receive a more detailed response within 96 hours indicating next steps in handling your report. 8 | 9 | If you have not received a reply to your submission within 96 hours, Contact the current security coordinator, Mike Dalessio . 10 | 11 | The information you share with the mini_portile core contributors as part of this process will be kept confidential within the team, unless or until we need to share information upstream with our dependent libraries' core teams, at which point we will notify you. 12 | 13 | If a vulnerability is first reported by you, we will credit you with the discovery in the public disclosure. 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2016 Luis Lavena and Mike Dalessio 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.github/workflows/upstream.yml: -------------------------------------------------------------------------------- 1 | name: upstream 2 | concurrency: 3 | group: "${{github.workflow}}-${{github.ref}}" 4 | cancel-in-progress: true 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 8 * * 5" # At 08:00 on Friday # https://crontab.guru/#0_8_*_*_5 9 | pull_request: 10 | types: [opened, synchronize] 11 | branches: 12 | - "*" 13 | paths: 14 | - .github/workflows/upstream.yml # this file 15 | 16 | jobs: 17 | test-unit: 18 | env: 19 | MAKEFLAGS: -j2 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | platform: [ubuntu-latest, windows-latest, macos-latest] 24 | ruby: ["head"] 25 | runs-on: ${{ matrix.platform }} 26 | steps: 27 | - name: configure git crlf on windows 28 | if: matrix.platform == 'windows-latest' 29 | run: | 30 | git config --system core.autocrlf false 31 | git config --system core.eol lf 32 | - uses: actions/checkout@v4 33 | - uses: MSP-Greg/setup-ruby-pkgs@v1 34 | with: 35 | apt-get: _update_ build-essential cmake 36 | mingw: _upgrade_ cmake 37 | ruby-version: ${{ matrix.ruby }} 38 | bundler-cache: true 39 | - run: bundle exec rake test:unit 40 | -------------------------------------------------------------------------------- /test/test_execute.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | class TestExecute < TestCase 4 | def setup 5 | super 6 | @env = {"TEST_ENV_VAR1" => "VAR1_VALUE", "TEST_ENV_VAR2" => "VAR2_VALUE"} 7 | @logger = StringIO.new 8 | @recipe = MiniPortile.new("test_execute", "1.0.0", logger: @logger) 9 | @log_path = @recipe.send(:tmp_path) 10 | FileUtils.mkdir_p File.join(@log_path, "subdir") # normally created by `download` 11 | end 12 | 13 | def test_execute_one_big_string_arg 14 | class << @recipe 15 | def execute_with_env(env) 16 | execute("testenv1", 17 | %Q(ruby -e "puts ENV['TEST_ENV_VAR1'].inspect ; exit 0"), 18 | {:env => env, :initial_message => false}) 19 | end 20 | end 21 | 22 | @recipe.execute_with_env(@env) 23 | 24 | assert_equal("VAR1_VALUE".inspect, IO.read(File.join(@log_path, "testenv1.log")).chomp) 25 | end 26 | 27 | def test_execute_array_args 28 | class << @recipe 29 | def execute_with_env(env) 30 | execute("testenv2", 31 | ["ruby", "-e", "puts ENV['TEST_ENV_VAR2'].inspect"], 32 | {:env => env, :initial_message => false}) 33 | end 34 | end 35 | 36 | @recipe.execute_with_env(@env) 37 | 38 | assert_equal("VAR2_VALUE".inspect, IO.read(File.join(@log_path, "testenv2.log")).chomp) 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /mini_portile2.gemspec: -------------------------------------------------------------------------------- 1 | require_relative "lib/mini_portile2/version" 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "mini_portile2" 5 | spec.version = MiniPortile::VERSION 6 | 7 | spec.authors = ["Luis Lavena", "Mike Dalessio", "Lars Kanis"] 8 | spec.email = "mike.dalessio@gmail.com" 9 | 10 | spec.summary = "Simple autoconf and cmake builder for developers" 11 | spec.description = <<~TEXT 12 | Simple autoconf and cmake builder for developers. It provides a standard way to compile against 13 | dependency libraries without requiring system-wide installation. It also simplifies 14 | vendoring and cross-compilation by providing a consistent build interface. 15 | TEXT 16 | 17 | spec.homepage = "https://github.com/flavorjones/mini_portile" 18 | spec.licenses = ["MIT"] 19 | 20 | begin 21 | spec.files = `git ls-files -z`.split("\x0") 22 | rescue Exception => e 23 | warn "WARNING: could not set spec.files: #{e.class}: #{e}" 24 | end 25 | 26 | # omit the `examples` directory from the gem, because it's large and 27 | # not necessary to be packaged in the gem. 28 | example_files = spec.files.grep(%r{^examples/}) 29 | spec.files -= example_files 30 | 31 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 32 | spec.test_files = spec.files.grep(%r{^(test|spec|features|examples)/}) 33 | spec.require_paths = ["lib"] 34 | 35 | spec.required_ruby_version = ">= 2.3.0" 36 | 37 | spec.metadata["changelog_uri"] = spec.homepage + "/blob/main/CHANGELOG.md" 38 | end 39 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'minitest/autorun' 2 | require 'minitest/unit' 3 | require 'minitest/spec' 4 | require 'minitest/hooks/test' 5 | require 'webrick' 6 | require 'fileutils' 7 | require 'zlib' 8 | require 'archive/tar/minitar' 9 | require 'fileutils' 10 | require 'erb' 11 | require 'mini_portile2' 12 | require 'logger' 13 | 14 | puts "#{__FILE__}:#{__LINE__}: relevant RbConfig::CONFIG values:" 15 | %w[target_os target_cpu CC CXX].each do |key| 16 | puts "- #{key}: #{RbConfig::CONFIG[key].inspect}" 17 | end 18 | 19 | class TestCase < Minitest::Test 20 | include Minitest::Hooks 21 | 22 | HTTP_PORT = 23523 23 | 24 | attr_accessor :webrick 25 | 26 | def start_webrick(path) 27 | @webrick = WEBrick::HTTPServer.new( 28 | :Port => HTTP_PORT, 29 | :DocumentRoot => path, 30 | :Logger => Logger.new(File::NULL), 31 | :AccessLog => [], 32 | ).tap do |w| 33 | Thread.new do 34 | w.start 35 | end 36 | until w.status==:Running 37 | sleep 0.1 38 | end 39 | end 40 | end 41 | 42 | def stop_webrick 43 | if w=@webrick 44 | w.shutdown 45 | until w.status==:Stop 46 | sleep 0.1 47 | end 48 | end 49 | end 50 | 51 | def create_tar(tar_path, assets_path, directory) 52 | FileUtils.mkdir_p(File.dirname(tar_path)) 53 | Zlib::GzipWriter.open(tar_path) do |fdtgz| 54 | Dir.chdir(assets_path) do 55 | Archive::Tar::Minitar.pack(directory, fdtgz) 56 | end 57 | end 58 | end 59 | 60 | def work_dir(r=recipe) 61 | "tmp/#{r.host}/ports/#{r.name}/#{r.version}/#{r.name}-#{r.version}" 62 | end 63 | 64 | def with_custom_git_dir(dir) 65 | old = ENV['GIT_DIR'] 66 | ENV['GIT_DIR'] = dir 67 | yield 68 | ensure 69 | ENV['GIT_DIR'] = old 70 | end 71 | 72 | def with_env(env) 73 | before = ENV.to_h.dup 74 | env.each { |k, v| ENV[k] = v } 75 | yield 76 | ensure 77 | ENV.replace(before) 78 | end 79 | 80 | def without_env(*keys, &blk) 81 | before = ENV.to_h.dup 82 | keys.flatten.each { |k| ENV.delete(k) } 83 | yield 84 | ensure 85 | ENV.replace(before) 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /test/test_download.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper', __FILE__) 2 | 3 | describe "recipe download" do 4 | include Minitest::Hooks 5 | 6 | attr :recipe 7 | 8 | def server_must_receive_connection(connections = 3, &block) 9 | request_count = 0 10 | 11 | server = TCPServer.open('localhost', TestCase::HTTP_PORT) 12 | thread = Thread.new do 13 | connections.times do 14 | conn = server.accept 15 | request_count += 1 16 | conn.puts "CONNECTION SUCESSFULLY MADE" rescue SystemCallError 17 | conn.close 18 | end 19 | end 20 | 21 | begin 22 | block.call 23 | ensure 24 | thread.kill 25 | server.close 26 | end 27 | 28 | assert_operator(request_count, :>, 0) 29 | end 30 | 31 | before do 32 | @request_count = 0 33 | @logger = StringIO.new 34 | @recipe = MiniPortile.new("test-download", "1.1.1", logger: @logger) 35 | end 36 | 37 | describe "urls" do 38 | it "ftp" do 39 | @recipe.files << "ftp://localhost:#{TestCase::HTTP_PORT}/foo" 40 | server_must_receive_connection 1 do 41 | @recipe.download 42 | end 43 | end 44 | 45 | it "handles http" do 46 | @recipe.files << "http://localhost:#{TestCase::HTTP_PORT}/foo" 47 | server_must_receive_connection 3 do 48 | @recipe.download 49 | end 50 | end 51 | 52 | it "handles https" do 53 | @recipe.files << "https://localhost:#{TestCase::HTTP_PORT}/foo" 54 | server_must_receive_connection 3 do 55 | @recipe.download 56 | end 57 | end 58 | 59 | it "file" do 60 | dest = "ports/archives/test-download-archive.tar.gz" 61 | FileUtils.rm_f dest 62 | path = File.expand_path(File.join(File.dirname(__FILE__), "assets", "test-download-archive.tar.gz")) 63 | @recipe.files << "file://#{path}" 64 | @recipe.download 65 | assert File.exist?(dest) 66 | assert_equal("ee0e9f44e72213015ef976d5ac23931d", Digest::MD5.file(dest).hexdigest) 67 | end 68 | 69 | it "other" do 70 | @recipe.files << "foo://foo" 71 | assert_raises(ArgumentError) { @recipe.download } 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /.github/workflows/downstream.yml: -------------------------------------------------------------------------------- 1 | name: downstream 2 | concurrency: 3 | group: "${{github.workflow}}-${{github.ref}}" 4 | cancel-in-progress: true 5 | on: 6 | workflow_dispatch: 7 | schedule: 8 | - cron: "0 7 * * 1,3,5" # At 07:00 on Monday, Wednesday, and Friday # https://crontab.guru/#0_7_*_*_1,3,5 9 | push: 10 | branches: 11 | - main 12 | - "v*.*.x" 13 | tags: 14 | - v*.*.* 15 | pull_request: 16 | types: [opened, synchronize] 17 | branches: 18 | - '*' 19 | 20 | jobs: 21 | downstream: 22 | name: downstream-${{matrix.name}}-${{matrix.platform}} 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | name: [re2, nokogiri, sqlite3] 27 | platform: [ubuntu-latest, windows-latest, macos-latest] 28 | include: 29 | - name: re2 30 | url: https://github.com/mudge/re2 31 | command: "bundle exec rake compile spec" 32 | ruby: "3.3" 33 | - name: nokogiri 34 | url: https://github.com/sparklemotion/nokogiri 35 | command: "bundle exec rake compile test" 36 | ruby: "3.3" 37 | - name: sqlite3 38 | url: https://github.com/sparklemotion/sqlite3-ruby 39 | command: "bundle exec rake compile test" 40 | ruby: "3.3" 41 | runs-on: ${{matrix.platform}} 42 | steps: 43 | - name: configure git crlf 44 | if: ${{ startsWith(matrix.platform, 'windows') }} 45 | run: | 46 | git config --system core.autocrlf false 47 | git config --system core.eol lf 48 | - uses: actions/checkout@v4 49 | - uses: ruby/setup-ruby@v1 50 | with: 51 | ruby-version: ${{matrix.ruby}} 52 | bundler-cache: true 53 | bundler: latest 54 | - run: git clone --depth=1 ${{matrix.url}} ${{matrix.name}} 55 | - uses: actions/cache@v4 56 | with: 57 | path: ${{matrix.name}}/ports/archives 58 | key: tarballs-${{matrix.name}} 59 | enableCrossOsArchive: true 60 | - name: ${{matrix.name}} test suite 61 | working-directory: ${{matrix.name}} 62 | run: | 63 | bundle remove mini_portile2 || true 64 | bundle add mini_portile2 --path=".." 65 | bundle install --local || bundle install 66 | ${{matrix.command}} 67 | -------------------------------------------------------------------------------- /lib/mini_portile2/mini_portile_cmake.rb: -------------------------------------------------------------------------------- 1 | require 'mini_portile2/mini_portile' 2 | require 'open3' 3 | 4 | class MiniPortileCMake < MiniPortile 5 | attr_accessor :system_name 6 | 7 | def configure_prefix 8 | "-DCMAKE_INSTALL_PREFIX=#{File.expand_path(port_path)}" 9 | end 10 | 11 | def initialize(name, version, **kwargs) 12 | super(name, version, **kwargs) 13 | @cmake_command = kwargs[:cmake_command] 14 | @cmake_build_type = kwargs[:cmake_build_type] 15 | end 16 | 17 | def configure_defaults 18 | [ 19 | generator_defaults, 20 | cmake_compile_flags, 21 | ].flatten 22 | end 23 | 24 | def configure 25 | return if configured? 26 | 27 | cache_file = File.join(tmp_path, 'configure.options_cache') 28 | File.open(cache_file, "w") { |f| f.write computed_options.to_s } 29 | 30 | execute('configure', [cmake_cmd] + computed_options + ["."]) 31 | end 32 | 33 | def configured? 34 | configure = File.join(work_path, 'configure') 35 | makefile = File.join(work_path, 'CMakefile') 36 | cache_file = File.join(tmp_path, 'configure.options_cache') 37 | 38 | stored_options = File.exist?(cache_file) ? File.read(cache_file) : "" 39 | current_options = computed_options.to_s 40 | 41 | (current_options == stored_options) && newer?(makefile, configure) 42 | end 43 | 44 | def make_cmd 45 | return "nmake" if MiniPortile.mswin? 46 | super 47 | end 48 | 49 | def cmake_cmd 50 | (ENV["CMAKE"] || @cmake_command || "cmake").dup 51 | end 52 | 53 | def cmake_build_type 54 | (ENV["CMAKE_BUILD_TYPE"] || @cmake_build_type || "Release").dup 55 | end 56 | 57 | private 58 | 59 | def generator_defaults 60 | if MiniPortile.mswin? && generator_available?('NMake') 61 | ['-G', 'NMake Makefiles'] 62 | elsif MiniPortile.mingw? && generator_available?('MSYS') 63 | ['-G', 'MSYS Makefiles'] 64 | else 65 | [] 66 | end 67 | end 68 | 69 | def cmake_compile_flags 70 | # RbConfig::CONFIG['CC'] and RbConfig::CONFIG['CXX'] can contain additional flags, for example 71 | # "clang++ -std=gnu++11" or "clang -fdeclspec". CMake is just looking for the command name. 72 | cc_compiler = cc_cmd.split.first 73 | cxx_compiler = cxx_cmd.split.first 74 | 75 | # needed to ensure cross-compilation with CMake targets the right CPU and compilers 76 | [ 77 | "-DCMAKE_SYSTEM_NAME=#{cmake_system_name}", 78 | "-DCMAKE_SYSTEM_PROCESSOR=#{cpu_type}", 79 | "-DCMAKE_C_COMPILER=#{cc_compiler}", 80 | "-DCMAKE_CXX_COMPILER=#{cxx_compiler}", 81 | "-DCMAKE_BUILD_TYPE=#{cmake_build_type}", 82 | ] 83 | end 84 | 85 | # Full list: https://gitlab.kitware.com/cmake/cmake/-/blob/v3.26.4/Modules/CMakeDetermineSystem.cmake?ref_type=tags#L12-31 86 | def cmake_system_name 87 | return system_name if system_name 88 | 89 | if MiniPortile.linux? 90 | 'Linux' 91 | elsif MiniPortile.darwin? 92 | 'Darwin' 93 | elsif MiniPortile.windows? 94 | 'Windows' 95 | elsif MiniPortile.freebsd? 96 | 'FreeBSD' 97 | elsif MiniPortile.openbsd? 98 | 'OpenBSD' 99 | elsif MiniPortile.solaris? 100 | 'SunOS' 101 | else 102 | raise "Unable to set CMAKE_SYSTEM_NAME for #{MiniPortile.target_os}" 103 | end 104 | end 105 | 106 | def generator_available?(generator_type) 107 | stdout_str, status = Open3.capture2("#{cmake_cmd} --help") 108 | 109 | raise 'Unable to determine whether CMake supports #{generator_type} Makefile generator' unless status.success? 110 | 111 | stdout_str.include?("#{generator_type} Makefiles") 112 | end 113 | 114 | def cpu_type 115 | return 'x86_64' if MiniPortile.target_cpu == 'x64' 116 | 117 | MiniPortile.target_cpu 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /test/test_activate.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper', __FILE__) 2 | 3 | class TestActivate < TestCase 4 | attr_reader :recipe 5 | 6 | def setup 7 | super 8 | 9 | @save_env = %w[PATH CPATH LIBRARY_PATH LDFLAGS].inject({}) do |env, var| 10 | env.update(var => ENV[var]) 11 | end 12 | 13 | FileUtils.rm_rf(["tmp", "ports"]) # remove any previous test files 14 | 15 | @recipe = MiniPortile.new("foo", "1.0.0").tap do |recipe| 16 | recipe.logger = StringIO.new 17 | end 18 | end 19 | 20 | def teardown 21 | FileUtils.rm_rf(["tmp", "ports"]) # remove any previous test files 22 | 23 | @save_env.each do |var, val| 24 | ENV[var] = val 25 | end 26 | 27 | super 28 | end 29 | 30 | def test_PATH_env_var_when_bin_does_not_exist 31 | ENV["PATH"] = "foo" 32 | refute(Dir.exist?(bin_path)) 33 | refute_includes(path_elements('PATH'), bin_path) 34 | 35 | recipe.activate 36 | 37 | refute_includes(path_elements('PATH'), bin_path) 38 | end 39 | 40 | def test_PATH_env_var_when_bin_exists 41 | ENV["PATH"] = "foo" 42 | FileUtils.mkdir_p(bin_path) 43 | refute_includes(path_elements('PATH'), bin_path) 44 | 45 | recipe.activate 46 | 47 | assert_includes(path_elements('PATH'), bin_path) 48 | assert_equal(path_elements('PATH').first, bin_path) 49 | end 50 | 51 | def test_CPATH_env_var_when_include_does_not_exist 52 | ENV["CPATH"] = "foo" 53 | refute(Dir.exist?(include_path)) 54 | refute_includes(path_elements('CPATH'), include_path) 55 | 56 | recipe.activate 57 | 58 | refute_includes(path_elements('CPATH'), include_path) 59 | end 60 | 61 | def test_CPATH_env_var_when_include_exists 62 | ENV["CPATH"] = "foo" 63 | FileUtils.mkdir_p(include_path) 64 | refute_includes(path_elements('CPATH'), include_path) 65 | 66 | recipe.activate 67 | 68 | assert_includes(path_elements('CPATH'), include_path) 69 | assert_equal(path_elements('CPATH').first, include_path) 70 | end 71 | 72 | def test_LIBRARY_PATH_env_var_when_lib_does_not_exist 73 | ENV["LIBRARY_PATH"] = "foo" 74 | refute(Dir.exist?(lib_path)) 75 | refute_includes(path_elements('LIBRARY_PATH'), lib_path) 76 | 77 | recipe.activate 78 | 79 | refute_includes(path_elements('LIBRARY_PATH'), lib_path) 80 | end 81 | 82 | def test_LIBRARY_PATH_env_var_when_lib_exists 83 | ENV["LIBRARY_PATH"] = "foo" 84 | FileUtils.mkdir_p(lib_path) 85 | refute_includes(path_elements('LIBRARY_PATH'), lib_path) 86 | 87 | recipe.activate 88 | 89 | assert_includes(path_elements('LIBRARY_PATH'), lib_path) 90 | assert_equal(path_elements('LIBRARY_PATH').first, lib_path) 91 | end 92 | 93 | def test_LDFLAGS_env_var_when_not_cross_compiling 94 | ENV["LDFLAGS"] = "-lfoo" 95 | FileUtils.mkdir_p(lib_path) 96 | assert_equal(recipe.host, recipe.original_host) # assert on setup) 97 | 98 | refute_includes(flag_elements('LDFLAGS'), "-L#{lib_path}") 99 | 100 | recipe.activate 101 | 102 | refute_includes(flag_elements('LDFLAGS'), "-L#{lib_path}") 103 | end 104 | 105 | def test_LDFLAGS_env_var_when_cross_compiling 106 | ENV["LDFLAGS"] = "-lfoo" 107 | recipe.host = recipe.original_host + "-x" # make them not-equal 108 | FileUtils.mkdir_p(lib_path) 109 | 110 | refute_includes(flag_elements('LDFLAGS'), "-L#{lib_path}") 111 | 112 | recipe.activate 113 | 114 | assert_includes(flag_elements('LDFLAGS'), "-L#{lib_path}") 115 | assert_equal(flag_elements('LDFLAGS').first, "-L#{lib_path}") 116 | end 117 | 118 | private 119 | 120 | def path_elements(varname) 121 | ENV.fetch(varname, "").split(File::PATH_SEPARATOR) 122 | end 123 | 124 | def flag_elements(varname) 125 | ENV.fetch(varname, "").split 126 | end 127 | 128 | def bin_path 129 | MiniPortile.native_path(File.join(recipe.path, "bin")) 130 | end 131 | 132 | def include_path 133 | MiniPortile.native_path(File.join(recipe.path, "include")) 134 | end 135 | 136 | def lib_path 137 | MiniPortile.native_path(File.join(recipe.path, "lib")) 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /test/test_proxy.rb: -------------------------------------------------------------------------------- 1 | # Encoding: utf-8 2 | 3 | require File.expand_path('../helper', __FILE__) 4 | require 'socket' 5 | 6 | class TestProxy < TestCase 7 | def with_dummy_proxy(username=nil, password=nil) 8 | gs = TCPServer.open('localhost', 0) 9 | th = Thread.new do 10 | s = gs.accept 11 | gs.close 12 | begin 13 | req = ''.dup 14 | while (l=s.gets) && !l.chomp.empty? 15 | req << l 16 | end 17 | req 18 | ensure 19 | s.close 20 | end 21 | end 22 | 23 | if username && password 24 | yield "http://#{ERB::Util.url_encode(username)}:#{ERB::Util.url_encode(password)}@localhost:#{gs.addr[1]}" 25 | else 26 | yield "http://localhost:#{gs.addr[1]}" 27 | end 28 | 29 | # Set timeout for reception of the request 30 | Thread.new do 31 | sleep 1 32 | th.kill 33 | end 34 | th.value 35 | end 36 | 37 | def setup 38 | # remove any download files 39 | FileUtils.rm_rf("port/archives") 40 | @logger = StringIO.new 41 | end 42 | 43 | def assert_proxy_auth(expected, request) 44 | if request =~ /^Proxy-Authorization: Basic (.*)/ 45 | assert_equal 'user: @name:@12: üMp', $1.unpack("m")[0].force_encoding(__ENCODING__) 46 | else 47 | flunk "No authentication request" 48 | end 49 | end 50 | 51 | def test_http_proxy 52 | recipe = MiniPortile.new("test http_proxy", "1.0.0", logger: @logger) 53 | recipe.files << "http://myserver/path/to/tar.gz" 54 | request = with_dummy_proxy do |url, thread| 55 | ENV['http_proxy'] = url 56 | recipe.download rescue RuntimeError 57 | ENV.delete('http_proxy') 58 | end 59 | assert_match(/GET http:\/\/myserver\/path\/to\/tar.gz/, request) 60 | end 61 | 62 | def test_http_proxy_with_basic_auth 63 | recipe = MiniPortile.new("test http_proxy", "1.0.0", logger: @logger) 64 | recipe.files << "http://myserver/path/to/tar.gz" 65 | request = with_dummy_proxy('user: @name', '@12: üMp') do |url, thread| 66 | ENV['http_proxy'] = url 67 | recipe.download rescue RuntimeError 68 | ENV.delete('http_proxy') 69 | end 70 | 71 | assert_match(/GET http:\/\/myserver\/path\/to\/tar.gz/, request) 72 | assert_proxy_auth 'user: @name:@12: üMp', request 73 | end 74 | 75 | def test_https_proxy 76 | recipe = MiniPortile.new("test https_proxy", "1.0.0", logger: @logger) 77 | recipe.files << "https://myserver/path/to/tar.gz" 78 | request = with_dummy_proxy do |url, thread| 79 | ENV['https_proxy'] = url 80 | recipe.download rescue RuntimeError 81 | ENV.delete('https_proxy') 82 | end 83 | assert_match(/CONNECT myserver:443/, request) 84 | end 85 | 86 | def test_https_proxy_with_basic_auth 87 | recipe = MiniPortile.new("test https_proxy", "1.0.0", logger: @logger) 88 | recipe.files << "https://myserver/path/to/tar.gz" 89 | request = with_dummy_proxy('user: @name', '@12: üMp') do |url, thread| 90 | ENV['https_proxy'] = url 91 | recipe.download rescue RuntimeError 92 | ENV.delete('https_proxy') 93 | end 94 | 95 | assert_match(/CONNECT myserver:443/, request) 96 | assert_proxy_auth 'user: @name:@12: üMp', request 97 | end 98 | 99 | def test_ftp_proxy 100 | recipe = MiniPortile.new("test ftp_proxy", "1.0.0", logger: @logger) 101 | recipe.files << "ftp://myserver/path/to/tar.gz" 102 | request = with_dummy_proxy do |url, thread| 103 | ENV['ftp_proxy'] = url 104 | recipe.download rescue RuntimeError 105 | ENV.delete('ftp_proxy') 106 | end 107 | assert_match(/GET ftp:\/\/myserver\/path\/to\/tar.gz/, request) 108 | end 109 | 110 | def test_ftp_proxy_with_basic_auth 111 | recipe = MiniPortile.new("test ftp_proxy", "1.0.0", logger: @logger) 112 | recipe.files << "ftp://myserver/path/to/tar.gz" 113 | request = with_dummy_proxy('user: @name', '@12: üMp') do |url, thread| 114 | ENV['ftp_proxy'] = url 115 | recipe.download rescue RuntimeError 116 | ENV.delete('ftp_proxy') 117 | end 118 | 119 | assert_match(/GET ftp:\/\/myserver\/path\/to\/tar.gz/, request) 120 | assert_proxy_auth 'user: @name:@12: üMp', request 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /examples/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | 3 | $: << File.expand_path(File.join(File.dirname(__FILE__), "../lib")) 4 | require "mini_portile2" 5 | 6 | recipes = [] 7 | recipe_hooks = {} 8 | 9 | def windows? 10 | RbConfig::CONFIG['target_os'] =~ /mswin|mingw32/ 11 | end 12 | 13 | def arm64_darwin? 14 | RUBY_PLATFORM =~ /arm64-darwin/ 15 | end 16 | 17 | # libiconv is still shipping an old version of automake that doesn't support arm64-darwin 18 | unless arm64_darwin? 19 | libiconv = MiniPortile.new "libiconv", "1.15" 20 | libiconv.files << "ftp://ftp.gnu.org/pub/gnu/#{libiconv.name}/#{libiconv.name}-#{libiconv.version}.tar.gz" 21 | libiconv.patch_files = Dir[File.join(__dir__, "libiconv-patches", "*.patch")].sort 22 | recipes.push libiconv 23 | end 24 | 25 | 26 | # libxml2 2.9.13 is still shipping an old version of automake that doesn't support arm64-darwin 27 | unless arm64_darwin? 28 | # test the version of libxml2 with an xz extension 29 | libxml2 = MiniPortile.new "libxml2", "2.9.13" 30 | libxml2.files << "https://download.gnome.org/sources/libxml2/2.9/libxml2-2.9.13.tar.xz" 31 | libxml2.configure_options += [ 32 | "--without-python", 33 | "--without-readline", 34 | ] 35 | recipes.push libxml2 36 | end 37 | 38 | 39 | # libxml2 2.9.13 is still shipping an old version of automake that doesn't support arm64-darwin 40 | unless windows? || arm64_darwin? 41 | # i can't get this version to build on Github Actions windows-latest / windows-2019 42 | sqlite3 = MiniPortile.new "sqlite3", "3.35.4" 43 | sqlite3.files << "https://www.sqlite.org/2021/sqlite-autoconf-3350400.tar.gz" 44 | 45 | recipes.push sqlite3 46 | end 47 | 48 | 49 | unless windows? || arm64_darwin? 50 | # i can't get this version to build on Github Actions windows-latest / windows-2019 51 | # c-ares 52 | c_ares = MiniPortile.new "c-ares", "1.7.5" 53 | c_ares.files << { 54 | url: "file://#{File.dirname(__FILE__)}/c-ares-1.7.5.tar.gz", 55 | md5: "800875fc23cd8e1924d8af9172ed33e7" 56 | } 57 | 58 | recipes.push c_ares 59 | end 60 | 61 | 62 | # zlib 63 | class ZlibRecipe < MiniPortile 64 | def windows? 65 | !(host =~ /mswin|mingw/).nil? 66 | end 67 | 68 | def configure 69 | return super unless windows? 70 | 71 | Dir.chdir work_path do 72 | mk = File.read 'win32/Makefile.gcc' 73 | File.open 'win32/Makefile.gcc', 'wb' do |f| 74 | f.puts "BINARY_PATH = #{path}/bin" 75 | f.puts "LIBRARY_PATH = #{path}/lib" 76 | f.puts "INCLUDE_PATH = #{path}/include" 77 | 78 | cross_build? and 79 | mk.sub!(/^PREFIX\s*=\s*$/, "PREFIX = #{host}-") 80 | 81 | f.puts mk 82 | end 83 | end 84 | end 85 | 86 | def configure_defaults 87 | ["--static"] 88 | end 89 | 90 | def configured? 91 | return super unless windows? 92 | 93 | !!(File.read(File.join(work_path, 'win32/Makefile.gcc')) =~ /^BINARY_PATH/) 94 | end 95 | 96 | def compile 97 | return super unless windows? 98 | 99 | execute "compile", "make -f win32/Makefile.gcc" 100 | end 101 | 102 | def install 103 | return if installed? 104 | return super unless windows? 105 | 106 | execute "install", %Q(#{make_cmd} -f win32/Makefile.gcc install) 107 | end 108 | 109 | def cross_build? 110 | host != original_host 111 | end 112 | end 113 | 114 | zlib = ZlibRecipe.new "zlib", "1.2.8" 115 | zlib.files << { 116 | # url: "http://zlib.net/#{zlib.name}-#{zlib.version}.tar.gz", 117 | url: "file://#{File.dirname(__FILE__)}/#{zlib.name}-#{zlib.version}.tar.gz", 118 | md5: "44d667c142d7cda120332623eab69f40", 119 | } 120 | 121 | recipes.push zlib 122 | 123 | # 124 | # libyaml, using pkgconf for configuration 125 | # 126 | yaml = MiniPortile.new("yaml", "0.2.5") 127 | yaml.files = [{ 128 | url: "https://github.com/yaml/libyaml/releases/download/0.2.5/yaml-0.2.5.tar.gz", 129 | sha256: "c642ae9b75fee120b2d96c712538bd2cf283228d2337df2cf2988e3c02678ef4", 130 | }] 131 | yaml.configure_options << "CFLAGS=-fPIC" 132 | recipes.unshift(yaml) 133 | recipe_hooks["yaml"] = lambda do |recipe| 134 | recipe.mkmf_config(pkg: "yaml-0.1", static: "yaml") 135 | 136 | expected = File.join(recipe.path, "lib", "libyaml.a") 137 | $libs.include?(expected) or raise(<<~MSG) 138 | assertion failed: $libs not updated correctly: 139 | #{$libs} 140 | should have included '#{expected}' 141 | MSG 142 | 143 | unless have_func("yaml_get_version", "yaml.h") 144 | raise("could not find libyaml development environment") 145 | end 146 | end 147 | 148 | namespace :ports do 149 | directory "ports" 150 | 151 | task :before do 152 | FileUtils.rm_rf(File.expand_path("tmp"), verbose: true); 153 | recipes.each do |recipe| 154 | FileUtils.rm_rf(recipe.path, verbose: true) 155 | end 156 | end 157 | task :all => :before 158 | 159 | recipes.each do |recipe| 160 | desc "Install port #{recipe.name} #{recipe.version}" 161 | task recipe.name => ["ports"] do |t| 162 | recipe.cook 163 | if hook = recipe_hooks[recipe.name] 164 | hook.call(recipe) 165 | else 166 | recipe.activate 167 | end 168 | end 169 | 170 | task :all => recipe.name 171 | end 172 | 173 | desc "Install all ports and display installation location" 174 | task :all do 175 | recipes.each do |recipe| 176 | puts "Artifacts of '#{recipe.name}' in '#{recipe.path}'" 177 | end 178 | puts "---" 179 | puts "LIBRARY_PATH: #{ENV['LIBRARY_PATH'].inspect}" 180 | puts "LDFLAGS: #{ENV['LDFLAGS'].inspect}" 181 | puts "---" 182 | puts "$INCFLAGS: #{$INCFLAGS.inspect}" 183 | puts "$CFLAGS: #{$CFLAGS.inspect}" 184 | puts "$LIBPATH: #{$LIBPATH.inspect}" 185 | puts "$libs: #{$libs.inspect}" 186 | end 187 | end 188 | 189 | 190 | desc "Adjust all recipes host for cross-compilation" 191 | task :cross do 192 | recipes.each do |recipe| 193 | recipe.host = "i686-w64-mingw32" 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | concurrency: 3 | group: "${{github.workflow}}-${{github.ref}}" 4 | cancel-in-progress: true 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: 9 | - main 10 | - v*.*.x 11 | tags: 12 | - v*.*.* 13 | pull_request: 14 | types: [opened, synchronize] 15 | branches: 16 | - "*" 17 | schedule: 18 | - cron: "0 8 * * 5" # At 08:00 on Friday # https://crontab.guru/#0_8_*_*_5 19 | 20 | jobs: 21 | test-unit: 22 | env: 23 | MAKEFLAGS: -j2 24 | strategy: 25 | fail-fast: false 26 | matrix: 27 | platform: [ubuntu-latest, windows-latest, macos-latest] 28 | ruby: ["2.3", "2.4", "2.5", "2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "3.4"] 29 | exclude: 30 | # I can't figure out how to install these on macos through setup-ruby 31 | - ruby: "2.3" 32 | platform: "macos-latest" 33 | - ruby: "2.4" 34 | platform: "macos-latest" 35 | - ruby: "2.5" 36 | platform: "macos-latest" 37 | runs-on: ${{ matrix.platform }} 38 | steps: 39 | - name: configure git crlf on windows 40 | if: matrix.platform == 'windows-latest' 41 | run: | 42 | git config --system core.autocrlf false 43 | git config --system core.eol lf 44 | - uses: actions/checkout@v4 45 | - uses: MSP-Greg/setup-ruby-pkgs@v1 46 | with: 47 | apt-get: _update_ build-essential cmake 48 | mingw: _upgrade_ cmake 49 | ruby-version: ${{ matrix.ruby }} 50 | bundler-cache: true 51 | - run: bundle exec rake test:unit 52 | 53 | test-examples: 54 | env: 55 | MAKEFLAGS: -j2 56 | LDFLAGS: "-L/usr/local/opt/libiconv/lib" # for macos-13, sigh 57 | strategy: 58 | fail-fast: false 59 | matrix: 60 | # use macos-13 (not 14) because libyaml 0.2.5 doesn't have up-to-date config.guess and config.sub 61 | platform: [ubuntu-latest, windows-latest, macos-13] 62 | ruby: ["3.1"] 63 | runs-on: ${{ matrix.platform }} 64 | steps: 65 | - name: configure git crlf on windows 66 | if: matrix.platform == 'windows-latest' 67 | run: | 68 | git config --system core.autocrlf false 69 | git config --system core.eol lf 70 | - uses: actions/checkout@v4 71 | - uses: MSP-Greg/setup-ruby-pkgs@v1 72 | with: 73 | apt-get: _update_ build-essential cmake 74 | mingw: _upgrade_ cmake 75 | brew: libiconv 76 | ruby-version: ${{ matrix.ruby }} 77 | bundler-cache: true 78 | - uses: actions/cache@v4 79 | with: 80 | path: examples/ports/archives 81 | key: examples-${{ hashFiles('examples/Rakefile') }} 82 | - run: bundle exec rake test:examples 83 | 84 | fedora: # see https://github.com/flavorjones/mini_portile/issues/118 85 | strategy: 86 | fail-fast: false 87 | matrix: 88 | task: ["test:unit", "test:examples"] 89 | runs-on: ubuntu-latest 90 | container: 91 | image: fedora:35 92 | steps: 93 | - run: | 94 | dnf group install -y "C Development Tools and Libraries" 95 | dnf install -y ruby ruby-devel libyaml-devel git-all patch cmake xz 96 | - uses: actions/checkout@v4 97 | - uses: actions/cache@v4 98 | with: 99 | path: examples/ports/archives 100 | key: examples-${{ hashFiles('examples/Rakefile') }} 101 | - run: bundle install 102 | - run: bundle exec rake ${{ matrix.task }} 103 | 104 | freebsd: 105 | strategy: 106 | fail-fast: false 107 | matrix: 108 | task: ["test:unit", "test:examples"] 109 | runs-on: ubuntu-latest 110 | env: 111 | MAKE: gmake 112 | steps: 113 | - uses: actions/checkout@v4 114 | - uses: actions/cache@v4 115 | with: 116 | path: examples/ports/archives 117 | key: examples-${{ hashFiles('examples/Rakefile') }} 118 | - uses: vmactions/freebsd-vm@v1 119 | with: 120 | envs: MAKE 121 | usesh: true 122 | copyback: false 123 | prepare: pkg install -y ruby devel/ruby-gems pkgconf git cmake devel/gmake textproc/libyaml security/gnupg 124 | run: | 125 | git config --global --add safe.directory /home/runner/work/mini_portile/mini_portile 126 | gem install bundler 127 | bundle install 128 | bundle exec rake ${{ matrix.task }} 129 | 130 | openbsd: 131 | strategy: 132 | fail-fast: false 133 | matrix: 134 | task: ["test:unit", "test:examples"] 135 | runs-on: ubuntu-latest 136 | env: 137 | MAKE: gmake 138 | steps: 139 | - uses: actions/checkout@v4 140 | - uses: vmactions/openbsd-vm@v1 141 | with: 142 | envs: MAKE 143 | usesh: true 144 | copyback: false 145 | prepare: | 146 | pkg_add ruby%3.4 gmake cmake git pkgconf security/gnupg 147 | ln -sf /usr/local/bin/ruby34 /usr/local/bin/ruby 148 | ln -sf /usr/local/bin/bundle34 /usr/local/bin/bundle 149 | ln -sf /usr/local/bin/bundler34 /usr/local/bin/bundler 150 | ln -sf /usr/local/bin/erb34 /usr/local/bin/erb 151 | ln -sf /usr/local/bin/gem34 /usr/local/bin/gem 152 | ln -sf /usr/local/bin/irb34 /usr/local/bin/irb 153 | ln -sf /usr/local/bin/racc34 /usr/local/bin/racc 154 | ln -sf /usr/local/bin/rake34 /usr/local/bin/rake 155 | ln -sf /usr/local/bin/rbs34 /usr/local/bin/rbs 156 | ln -sf /usr/local/bin/rdbg34 /usr/local/bin/rdbg 157 | ln -sf /usr/local/bin/rdoc34 /usr/local/bin/rdoc 158 | ln -sf /usr/local/bin/ri34 /usr/local/bin/ri 159 | ln -sf /usr/local/bin/syntax_suggest34 /usr/local/bin/syntax_suggest 160 | ln -sf /usr/local/bin/typeprof34 /usr/local/bin/typeprof 161 | run: | 162 | git config --global --add safe.directory /home/runner/work/mini_portile/mini_portile 163 | gem install bundler 164 | bundle install 165 | bundle exec rake ${{ matrix.task }} 166 | -------------------------------------------------------------------------------- /test/test_cook.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper', __FILE__) 2 | 3 | class TestCook < TestCase 4 | attr_accessor :assets_path, :tar_path, :recipe 5 | 6 | def before_all 7 | super 8 | @assets_path = File.expand_path("../assets", __FILE__) 9 | @tar_path = File.expand_path("../../tmp/test mini portile-1.0.0.tar.gz", __FILE__) 10 | 11 | FileUtils.rm_rf("tmp") # remove any previous test files 12 | 13 | create_tar(@tar_path, @assets_path, "test mini portile-1.0.0") 14 | start_webrick(File.dirname(@tar_path)) 15 | 16 | @logger = StringIO.new # IO to keep recipe logs in case we need to debug 17 | @recipe = MiniPortile.new("test mini portile", "1.0.0").tap do |recipe| 18 | recipe.logger = @logger 19 | recipe.files << "http://localhost:#{HTTP_PORT}/#{ERB::Util.url_encode(File.basename(@tar_path))}" 20 | recipe.patch_files << File.join(@assets_path, "patch 1.diff") 21 | recipe.configure_options << "--option=\"path with 'space'\"" 22 | git_dir = File.join(@assets_path, "git") 23 | with_custom_git_dir(git_dir) do 24 | recipe.cook 25 | end 26 | end 27 | rescue => e 28 | puts @logger.string 29 | raise e 30 | end 31 | 32 | def after_all 33 | super 34 | stop_webrick 35 | FileUtils.rm_rf("tmp") # remove test files 36 | end 37 | 38 | def test_download 39 | download = "ports/archives/test%20mini%20portile-1.0.0.tar.gz" 40 | assert File.exist?(download), download 41 | end 42 | 43 | def test_untar 44 | configure = File.join(work_dir, "configure") 45 | assert File.exist?(configure), configure 46 | assert_match( /^#!\/bin\/sh/, IO.read(configure) ) 47 | end 48 | 49 | def test_patch 50 | patch1 = File.join(work_dir, "patch 1.txt") 51 | assert File.exist?(patch1), patch1 52 | assert_match( /^\tchange 1/, IO.read(patch1) ) 53 | end 54 | 55 | def test_configure 56 | txt = File.join(work_dir, "configure.txt") 57 | assert File.exist?(txt), txt 58 | opts = recipe.configure_options + ["--prefix=#{recipe.path}"] 59 | assert_equal( opts.inspect, IO.read(txt).chomp ) 60 | end 61 | 62 | def test_compile 63 | txt = File.join(work_dir, "compile.txt") 64 | assert File.exist?(txt), txt 65 | assert_equal( ["all"].inspect, IO.read(txt).chomp ) 66 | end 67 | 68 | def test_install 69 | txt = File.join(work_dir, "install.txt") 70 | assert File.exist?(txt), txt 71 | assert_equal( ["install"].inspect, IO.read(txt).chomp ) 72 | end 73 | end 74 | 75 | class TestCookConfiguration < TestCase 76 | def test_make_command_configuration 77 | without_env("MAKE") do 78 | assert_equal("make", MiniPortile.new("test", "1.0.0").make_cmd) 79 | assert_equal("xyzzy", MiniPortile.new("test", "1.0.0", make_command: "xyzzy").make_cmd) 80 | end 81 | with_env("MAKE"=>"asdf") do 82 | assert_equal("asdf", MiniPortile.new("test", "1.0.0").make_cmd) 83 | assert_equal("asdf", MiniPortile.new("test", "1.0.0", make_command: "xyzzy").make_cmd) 84 | end 85 | end 86 | 87 | def test_cc_command_configuration 88 | without_env("CC") do 89 | expected_compiler = RbConfig::CONFIG["CC"] || "gcc" 90 | assert_equal(expected_compiler, MiniPortile.new("test", "1.0.0").cc_cmd) 91 | assert_equal(expected_compiler, MiniPortile.new("test", "1.0.0").gcc_cmd) 92 | assert_equal("xyzzy", MiniPortile.new("test", "1.0.0", cc_command: "xyzzy").cc_cmd) 93 | assert_equal("xyzzy", MiniPortile.new("test", "1.0.0", gcc_command: "xyzzy").cc_cmd) 94 | assert_equal("xyzzy", MiniPortile.new("test", "1.0.0", cc_command: "xyzzy").gcc_cmd) 95 | assert_equal("xyzzy", MiniPortile.new("test", "1.0.0", gcc_command: "xyzzy").gcc_cmd) 96 | end 97 | with_env("CC"=>"asdf") do 98 | assert_equal("asdf", MiniPortile.new("test", "1.0.0").cc_cmd) 99 | assert_equal("asdf", MiniPortile.new("test", "1.0.0").gcc_cmd) 100 | assert_equal("asdf", MiniPortile.new("test", "1.0.0", cc_command: "xyzzy").cc_cmd) 101 | assert_equal("asdf", MiniPortile.new("test", "1.0.0", gcc_command: "xyzzy").cc_cmd) 102 | assert_equal("asdf", MiniPortile.new("test", "1.0.0", cc_command: "xyzzy").gcc_cmd) 103 | assert_equal("asdf", MiniPortile.new("test", "1.0.0", gcc_command: "xyzzy").gcc_cmd) 104 | end 105 | end 106 | 107 | def test_cxx_command_configuration 108 | without_env("CXX") do 109 | expected_compiler = RbConfig::CONFIG["CXX"] || "g++" 110 | assert_equal(expected_compiler, MiniPortile.new("test", "1.0.0").cxx_cmd) 111 | assert_equal("xyzzy", MiniPortile.new("test", "1.0.0", cxx_command: "xyzzy").cxx_cmd) 112 | end 113 | with_env("CXX"=>"asdf") do 114 | assert_equal("asdf", MiniPortile.new("test", "1.0.0").cxx_cmd) 115 | assert_equal("asdf", MiniPortile.new("test", "1.0.0", cxx_command: "xyzzy").cxx_cmd) 116 | end 117 | end 118 | end 119 | 120 | 121 | class TestCookWithBrokenGitDir < TestCase 122 | # 123 | # this is a test for #69 124 | # https://github.com/flavorjones/mini_portile/issues/69 125 | # 126 | attr_accessor :assets_path, :tar_path, :recipe 127 | 128 | def before_all 129 | super 130 | @assets_path = File.expand_path("../assets", __FILE__) 131 | @tar_path = File.expand_path("../../tmp/test-mini-portile-1.0.0.tar.gz", __FILE__) 132 | 133 | @git_dir = File.join(@assets_path, "git-broken") 134 | FileUtils.rm_rf @git_dir 135 | FileUtils.mkdir_p @git_dir 136 | Dir.chdir(@git_dir) do 137 | File.open ".git", "w" do |f| 138 | f.write "gitdir: /nonexistent" 139 | end 140 | end 141 | 142 | create_tar(@tar_path, @assets_path, "test mini portile-1.0.0") 143 | 144 | @logger = StringIO.new # IO to keep recipe logs in case we need to debug 145 | @recipe = MiniPortile.new("test mini portile", "1.0.0").tap do |recipe| 146 | recipe.logger = @logger 147 | recipe.files << "file://#{@tar_path}" 148 | recipe.patch_files << File.join(@assets_path, "patch 1.diff") 149 | recipe.configure_options << "--option=\"path with 'space'\"" 150 | end 151 | 152 | Dir.chdir(@git_dir) do 153 | @recipe.cook 154 | end 155 | end 156 | 157 | def after_all 158 | FileUtils.rm_rf @git_dir 159 | end 160 | 161 | def test_patch 162 | Dir.chdir(@git_dir) do 163 | patch1 = File.join(work_dir, "patch 1.txt") 164 | assert File.exist?(patch1), patch1 165 | assert_match( /^\tchange 1/, IO.read(patch1) ) 166 | end 167 | end 168 | end 169 | 170 | class TestCookAgainstSourceDirectory < TestCase 171 | attr_accessor :recipe 172 | 173 | def setup 174 | super 175 | 176 | @logger = StringIO.new # IO to keep recipe logs in case we need to debug 177 | @recipe ||= MiniPortile.new("test mini portile", "1.0.0").tap do |recipe| 178 | recipe.logger = @logger 179 | recipe.source_directory = File.expand_path("../assets/test mini portile-1.0.0", __FILE__) 180 | end 181 | end 182 | 183 | def test_source_directory 184 | recipe.cook 185 | 186 | path = File.join(work_dir, "configure.txt") 187 | assert(File.exist?(path)) 188 | assert_equal((recipe.configure_options + ["--prefix=#{recipe.path}"]).inspect, 189 | File.read(path).chomp); 190 | 191 | path = File.join(work_dir, "compile.txt") 192 | assert(File.exist?(path)) 193 | assert_equal("[\"all\"]", File.read(path).chomp); 194 | 195 | path = File.join(work_dir, "install.txt") 196 | assert(File.exist?(path)) 197 | assert_equal("[\"install\"]", File.read(path).chomp); 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /test/test_mkmf_config.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper', __FILE__) 2 | 3 | require "mkmf" # initialize $LDFLAGS et al here, instead of in the middle of a test 4 | 5 | class TestMkmfConfig < TestCase 6 | attr_reader :recipe 7 | 8 | LIBXML_PCP = File.join(__dir__, "assets", "pkgconf", "libxml2") 9 | LIBXSLT_PCP = File.join(__dir__, "assets", "pkgconf", "libxslt") 10 | 11 | def make_recipe(name, version) 12 | MiniPortile.new(name, version).tap do |recipe| 13 | recipe.logger = StringIO.new # hush output 14 | end 15 | end 16 | 17 | def setup 18 | super 19 | 20 | @save_env = %w[PATH CPATH LIBRARY_PATH LDFLAGS PKG_CONFIG_PATH].inject({}) do |env, var| 21 | env.update(var => ENV[var]) 22 | end 23 | $INCFLAGS = "-I/xxx" 24 | $LIBPATH = ["xxx"] 25 | $CFLAGS = "-xxx" 26 | $CXXFLAGS = "-xxx" 27 | $libs = "-lxxx" 28 | $MINI_PORTILE_STATIC_LIBS = {} 29 | 30 | FileUtils.rm_rf(["tmp", "ports"]) # remove any previous test files 31 | 32 | @recipe = make_recipe("libfoo", "1.0.0") 33 | end 34 | 35 | def teardown 36 | FileUtils.rm_rf(["tmp", "ports"]) # remove any previous test files 37 | 38 | $INCFLAGS = "" 39 | $LIBPATH = [] 40 | $CFLAGS = "" 41 | $CXXFLAGS = "" 42 | $libs = "" 43 | $MINI_PORTILE_STATIC_LIBS = {} 44 | @save_env.each do |var, val| 45 | ENV[var] = val 46 | end 47 | 48 | super 49 | end 50 | 51 | def test_mkmf_config_recipe_LIBPATH_global_lib_dir_does_not_exist 52 | recipe.mkmf_config 53 | 54 | refute_includes($LIBPATH, recipe.lib_path) 55 | refute_includes($libs.shellsplit, "-lfoo") 56 | end 57 | 58 | def test_mkmf_config_recipe_LIBPATH_global_not_static 59 | FileUtils.mkdir_p(recipe.lib_path) 60 | 61 | recipe.mkmf_config 62 | 63 | assert_includes($LIBPATH, recipe.lib_path) 64 | assert_operator($LIBPATH.index(recipe.lib_path), :<, $LIBPATH.index("xxx")) # prepend 65 | 66 | assert_includes($libs.shellsplit, "-lfoo") # note the recipe name is "libfoo" 67 | assert_match(%r{-lfoo.*-lxxx}, $libs) # prepend 68 | end 69 | 70 | def test_mkmf_config_recipe_LIBPATH_global_static 71 | FileUtils.mkdir_p(recipe.lib_path) 72 | static_lib_path = File.join(recipe.lib_path, "libfoo.#{$LIBEXT}") 73 | 74 | recipe.mkmf_config(static: "foo") 75 | 76 | refute_includes($LIBPATH, recipe.lib_path) 77 | 78 | refute_includes($libs.shellsplit, "-lfoo") # note the recipe name is "libfoo" 79 | assert_includes($libs.shellsplit, static_lib_path) 80 | assert_match(%r{#{static_lib_path}.*-lxxx}, $libs) # prepend 81 | end 82 | 83 | def test_mkmf_config_recipe_INCFLAGS_global_include_dir_does_not_exist 84 | recipe.mkmf_config 85 | 86 | refute_includes($INCFLAGS.shellsplit, "-I#{recipe.include_path}") 87 | end 88 | 89 | def test_mkmf_config_recipe_INCFLAGS_global 90 | FileUtils.mkdir_p(recipe.include_path) 91 | 92 | recipe.mkmf_config 93 | 94 | assert_includes($INCFLAGS.shellsplit, "-I#{recipe.include_path}") 95 | assert_match(%r{-I#{recipe.include_path}.*-I/xxx}, $INCFLAGS) # prepend 96 | end 97 | 98 | def test_mkmf_config_pkgconf_does_not_exist 99 | assert_raises(ArgumentError) do 100 | recipe.mkmf_config(pkg: "foo") 101 | end 102 | end 103 | 104 | def test_mkmf_config_pkgconf_LIBPATH_global_not_static 105 | # can't get the pkgconf utility to install on windows with ruby 2.3 in CI 106 | skip if MiniPortile.windows? && RUBY_VERSION < "2.4" 107 | 108 | recipe.mkmf_config(pkg: "libxml-2.0", dir: LIBXML_PCP) 109 | 110 | assert_includes($LIBPATH, "/foo/libxml2/2.11.5/lib") 111 | assert_operator($LIBPATH.index("/foo/libxml2/2.11.5/lib"), :<, $LIBPATH.index("xxx")) # prepend 112 | refute_includes($LIBPATH, "/foo/zlib/1.3/lib") 113 | 114 | assert_includes($libs.shellsplit, "-lxml2") 115 | assert_match(%r{-lxml2.*-lxxx}, $libs) # prepend 116 | refute_includes($libs.shellsplit, "-lz") 117 | end 118 | 119 | def test_mkmf_config_pkgconf_LIBPATH_global_static 120 | # can't get the pkgconf utility to install on windows with ruby 2.3 in CI 121 | skip if MiniPortile.windows? && RUBY_VERSION < "2.4" 122 | 123 | static_lib_path = "/foo/libxml2/2.11.5/lib/libxml2.#{$LIBEXT}" 124 | 125 | recipe.mkmf_config(pkg: "libxml-2.0", dir: LIBXML_PCP, static: "xml2") 126 | 127 | refute_includes($LIBPATH, "/foo/libxml2/2.11.5/lib") 128 | refute_includes($libs.shellsplit, "-lxml2") 129 | assert_includes($libs.shellsplit, static_lib_path) 130 | assert_match(%r{#{static_lib_path}.*-lxxx}, $libs) # prepend 131 | 132 | assert_includes($LIBPATH, "/foo/zlib/1.3/lib") # from --static 133 | assert_includes($libs.shellsplit, "-lz") # from --static 134 | end 135 | 136 | def test_mkmf_config_pkgconf_CFLAGS_global 137 | # can't get the pkgconf utility to install on windows with ruby 2.3 in CI 138 | skip if MiniPortile.windows? && RUBY_VERSION < "2.4" 139 | 140 | recipe.mkmf_config(pkg: "libxml-2.0", dir: LIBXML_PCP) 141 | 142 | assert_includes($INCFLAGS.shellsplit, "-I/foo/libxml2/2.11.5/include/libxml2") 143 | assert_match(%r{-I/foo/libxml2/2.11.5/include/libxml2.*-I/xxx}, $INCFLAGS) # prepend 144 | 145 | assert_includes($CFLAGS.shellsplit, "-ggdb3") 146 | assert_match(%r{-xxx.*-ggdb3}, $CFLAGS) # append 147 | 148 | assert_includes($CXXFLAGS.shellsplit, "-ggdb3") 149 | assert_match(%r{-xxx.*-ggdb3}, $CXXFLAGS) # append 150 | end 151 | 152 | def test_mkmf_config_pkgconf_path_accumulation 153 | # can't get the pkgconf utility to install on windows with ruby 2.3 in CI 154 | skip if MiniPortile.windows? && RUBY_VERSION < "2.4" 155 | 156 | (ENV["PKG_CONFIG_PATH"] || "").split(File::PATH_SEPARATOR).tap do |pcpaths| 157 | refute_includes(pcpaths, LIBXML_PCP) 158 | refute_includes(pcpaths, LIBXSLT_PCP) 159 | end 160 | 161 | make_recipe("libxml2", "2.11.5").tap do |recipe| 162 | recipe.mkmf_config(pkg: "libxml-2.0", dir: LIBXML_PCP, static: "xml2") 163 | 164 | ENV["PKG_CONFIG_PATH"].split(File::PATH_SEPARATOR).tap do |pcpaths| 165 | assert_includes(pcpaths, LIBXML_PCP) 166 | refute_includes(pcpaths, LIBXSLT_PCP) 167 | end 168 | end 169 | 170 | make_recipe("libxslt", "1.13.8").tap do |recipe| 171 | recipe.mkmf_config(pkg: "libxslt", dir: LIBXSLT_PCP, static: "xslt") 172 | 173 | ENV["PKG_CONFIG_PATH"].split(File::PATH_SEPARATOR).tap do |pcpaths| 174 | assert_includes(pcpaths, LIBXML_PCP) 175 | assert_includes(pcpaths, LIBXSLT_PCP) 176 | end 177 | 178 | recipe.mkmf_config(pkg: "libexslt", dir: LIBXSLT_PCP, static: "exslt") 179 | end 180 | 181 | $INCFLAGS.shellsplit.tap do |incflags| 182 | assert_includes(incflags, "-I/foo/libxml2/2.11.5/include/libxml2") 183 | assert_includes(incflags, "-I/foo/libxslt/1.1.38/include") 184 | end 185 | $CFLAGS.shellsplit.tap do |cflags| 186 | assert_includes(cflags, "-ggdb3") 187 | assert_includes(cflags, "-Wno-deprecated-enum-enum-conversion") 188 | end 189 | refute_includes($LIBPATH, "/foo/libxml2/2.11.5/lib") 190 | refute_includes($LIBPATH, "/foo/libxslt/1.1.38/lib") 191 | assert_includes($LIBPATH, "/foo/zlib/1.3/lib") # from `--static` 192 | $libs.shellsplit.tap do |libflags| 193 | refute_includes(libflags, "-lxml2") 194 | assert_includes(libflags, "/foo/libxml2/2.11.5/lib/libxml2.#{$LIBEXT}") 195 | refute_includes(libflags, "-lxslt") 196 | assert_includes(libflags, "/foo/libxslt/1.1.38/lib/libxslt.#{$LIBEXT}") 197 | refute_includes(libflags, "-lexslt") 198 | assert_includes(libflags, "/foo/libxslt/1.1.38/lib/libexslt.#{$LIBEXT}") 199 | assert_includes(libflags, "-lz") # from `--static` 200 | end 201 | end 202 | end 203 | -------------------------------------------------------------------------------- /test/test_digest.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper', __FILE__) 2 | 3 | class TestDigest < TestCase 4 | attr :assets_path, :tar_path, :recipe 5 | 6 | def before_all 7 | super 8 | @assets_path = File.expand_path("../assets", __FILE__) 9 | @tar_path = File.expand_path("../../tmp/test-digest-1.0.0.tar.gz", __FILE__) 10 | 11 | # remove any previous test files 12 | FileUtils.rm_rf("tmp") 13 | 14 | create_tar(@tar_path, @assets_path, "test mini portile-1.0.0") 15 | start_webrick(File.dirname(@tar_path)) 16 | end 17 | 18 | def after_all 19 | super 20 | stop_webrick 21 | # leave test files for inspection 22 | end 23 | 24 | def setup 25 | super 26 | FileUtils.rm_rf("ports/archives") 27 | @logger = StringIO.new # IO to keep recipe logs in case we need to debug 28 | @recipe = MiniPortile.new("test-digest", "1.0.0", logger: @logger) 29 | end 30 | 31 | def download_with_digest(key, klass) 32 | @recipe.files << { 33 | :url => "http://localhost:#{webrick.config[:Port]}/#{ERB::Util.url_encode(File.basename(tar_path))}", 34 | key => klass.file(tar_path).hexdigest, 35 | } 36 | @recipe.download 37 | end 38 | 39 | def download_with_wrong_digest(key) 40 | @recipe.files << { 41 | :url => "http://localhost:#{webrick.config[:Port]}/#{ERB::Util.url_encode(File.basename(tar_path))}", 42 | key => "0011223344556677", 43 | } 44 | assert_raises(RuntimeError){ @recipe.download } 45 | end 46 | 47 | def test_sha256 48 | download_with_digest(:sha256, Digest::SHA256) 49 | end 50 | 51 | def test_wrong_sha256 52 | download_with_wrong_digest(:sha256) 53 | end 54 | 55 | def test_sha1 56 | download_with_digest(:sha1, Digest::SHA1) 57 | end 58 | 59 | def test_wrong_sha1 60 | download_with_wrong_digest(:sha1) 61 | end 62 | 63 | def test_md5 64 | download_with_digest(:md5, Digest::MD5) 65 | end 66 | 67 | def test_wrong_md5 68 | download_with_wrong_digest(:md5) 69 | end 70 | 71 | def public_key 72 | <<~KEY 73 | -----BEGIN PGP PUBLIC KEY BLOCK----- 74 | Version: GnuPG v1 75 | 76 | mI0EVwUhJQEEAMYxFhgaAdM2Ul5r+XfpqAaI7SOxB14eRjhFjhchy4ylgVxetyLq 77 | di3zeANXBIHsLBl7quYTlnmhJr/+GQRkCnXWiUp0tJsBVzGM3puK7c534gakEUH6 78 | AlDtj5p3IeygzSyn8u7KORv+ainXfhwkvTO04mJmxAb2uT8ngKYFdPa1ABEBAAG0 79 | J1Rlc3QgTWluaXBvcnRpbGUgPHRlc3RAbWluaXBvcnRpbGUub3JnPoi4BBMBAgAi 80 | BQJXBSElAhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRBl6D5JZMNwswAK 81 | A/90Cdb+PX21weBR2Q6uR06M/alPexuXXyJL8ZcwbQMJ/pBBgcS5/h1+rQkBI/CN 82 | qpXdDlw2Xys2k0sNwdjIw3hmYRzBrddXlCSW3Sifq/hS+kfPZ1snQmIjCgy1Xky5 83 | QGCcPUxBUxzmra88LakkDO+euKK3hcrfeFIi611lTum1NLiNBFcFISUBBADoyY6z 84 | 2PwH3RWUbqv0VX1s3/JO3v3xMjCRKPlFwsNwLTBtZoWfR6Ao1ajeCuZKfzNKIQ2I 85 | rn86Rcqyrq4hTj+7BTWjkIPOBthjiL1YqbEBtX7jcYRkYvdQz/IG2F4zVV6X4AAR 86 | Twx7qaXNt67ArzbHCe5gLNRUK6e6OArkahMv7QARAQABiJ8EGAECAAkFAlcFISUC 87 | GwwACgkQZeg+SWTDcLNFiwP/TR33ClqWOz0mpjt0xPEoZ0ORmV6fo4sjjzgQoHH/ 88 | KTdsabJbGp8oLQGW/mx3OxgbsAkyZymb5H5cjaF4HtSd4cxI5t1C9ZS/ytN8pqfR 89 | e29SBje8DAAJn2l57s2OddXLPQ0DUwCcdNEaqgHwSk/Swxc7K+IpfvjLKHKUZZBP 90 | 4Ko= 91 | =SVWi 92 | -----END PGP PUBLIC KEY BLOCK----- 93 | KEY 94 | end 95 | 96 | def test_with_valid_gpg_signature 97 | data_file = File.expand_path(File.join(File.dirname(__FILE__), 'assets', 'gpg-fixtures', 'data')) 98 | 99 | @recipe.files << { 100 | :url => "file://#{data_file}", 101 | :gpg => { 102 | :key => public_key, 103 | :signature_url => "file://#{data_file}.asc" 104 | } 105 | } 106 | @recipe.download 107 | end 108 | 109 | def test_optional_gpg_signature_url 110 | data_file = File.expand_path(File.join(File.dirname(__FILE__), 'assets', 'gpg-fixtures', 'data')) 111 | 112 | @recipe.files << { 113 | :url => "file://#{data_file}", 114 | :gpg => { 115 | :key => public_key 116 | } 117 | } 118 | @recipe.download 119 | end 120 | 121 | def test_with_invalid_gpg_signature 122 | data_file = File.expand_path(File.join(File.dirname(__FILE__), 'assets', 'gpg-fixtures', 'data')) 123 | 124 | @recipe.files << { 125 | :url => "file://#{data_file}", 126 | :gpg => { 127 | :key => public_key, 128 | :signature_url => "file://#{data_file}.invalid.asc" 129 | } 130 | } 131 | exception = assert_raises(RuntimeError){ 132 | @recipe.download 133 | } 134 | assert_includes(exception.message, "signature mismatch") 135 | end 136 | 137 | def test_with_invalid_key 138 | data_file = File.expand_path(File.join(File.dirname(__FILE__), 'assets', 'gpg-fixtures', 'data')) 139 | 140 | @recipe.files << { 141 | :url => "file://#{data_file}", 142 | :gpg => { 143 | :key => "thisisaninvalidkey", 144 | :signature_url => "file://#{data_file}.asc" 145 | } 146 | } 147 | exception = assert_raises(RuntimeError){ @recipe.download } 148 | assert_includes(exception.message, "invalid gpg key provided") 149 | end 150 | 151 | def test_with_different_key_than_one_used_to_sign 152 | key = <<~KEY 153 | -----BEGIN PGP PUBLIC KEY BLOCK----- 154 | Version: GnuPG v1 155 | 156 | mQENBE7SKu8BCADQo6x4ZQfAcPlJMLmL8zBEBUS6GyKMMMDtrTh3Yaq481HB54oR 157 | 0cpKL05Ff9upjrIzLD5TJUCzYYM9GQOhguDUP8+ZU9JpSz3yO2TvH7WBbUZ8FADf 158 | hblmmUBLNgOWgLo3W+FYhl3mz1GFS2Fvid6Tfn02L8CBAj7jxbjL1Qj/OA/WmLLc 159 | m6BMTqI7IBlYW2vyIOIHasISGiAwZfp0ucMeXXvTtt14LGa8qXVcFnJTdwbf03AS 160 | ljhYrQnKnpl3VpDAoQt8C68YCwjaNJW59hKqWB+XeIJ9CW98+EOAxLAFszSyGanp 161 | rCqPd0numj9TIddjcRkTA/ZbmCWK+xjpVBGXABEBAAG0IU1heGltIERvdW5pbiA8 162 | bWRvdW5pbkBtZG91bmluLnJ1PohGBBARAgAGBQJO01Y/AAoJEOzw6QssFyCDVyQA 163 | n3qwTZlcZgyyzWu9Cs8gJ0CXREaSAJ92QjGLT9DijTcbB+q9OS/nl16Z/IhGBBAR 164 | AgAGBQJO02JDAAoJEKk3YTmlJMU+P64AnjCKEXFelSVMtgefJk3+vpyt3QX1AKCH 165 | 9M3MbTWPeDUL+MpULlfdyfvjj4heBBARCAAGBQJRCTwgAAoJEFGFCWhsfl6CzF0B 166 | AJsQ3DJbtGcZ+0VIcM2a06RRQfBvIHqm1A/1WSYmObLGAP90lxWlNjSugvUUlqTk 167 | YEEgRTGozgixSyMWGJrNwqgMYokBOAQTAQIAIgUCTtIq7wIbAwYLCQgHAwIGFQgC 168 | CQoLBBYCAwECHgECF4AACgkQUgqZk6HAUvj+iwf/b4FS6zVzJ5T0v1vcQGD4ZzXe 169 | D5xMC4BJW414wVMU15rfX7aCdtoCYBNiApPxEd7SwiyxWRhRA9bikUq87JEgmnyV 170 | 0iYbHZvCvc1jOkx4WR7E45t1Mi29KBoPaFXA9X5adZkYcOQLDxa2Z8m6LGXnlF6N 171 | tJkxQ8APrjZsdrbDvo3HxU9muPcq49ydzhgwfLwpUs11LYkwB0An9WRPuv3jporZ 172 | /XgI6RfPMZ5NIx+FRRCjn6DnfHboY9rNF6NzrOReJRBhXCi6I+KkHHEnMoyg8XET 173 | 9lVkfHTOl81aIZqrAloX3/00TkYWyM2zO9oYpOg6eUFCX/Lw4MJZsTcT5EKVxIkC 174 | HAQQAQIABgUCVJ1r4wAKCRDrF/Z0x5pAovwnD/9m8aiSDoUo9IbDSx0345a7IsmN 175 | KlEqtz4AQxbqxXV3yTANBbhWWnsX6e7PLbJfzpNE9aoa72upwTcStpk6vlPea0AV 176 | ed83FdVsfxwXm/Sf5iySZKy93PexAZfw7KvXu0ETWxi1YZjFNtNsdUIiuJ4upLNo 177 | h3urG8NC9uIQYgZef9NPTztmj77saerUrdXt3PQmnYp8ti0NWElE3KzgjoC1fIEZ 178 | Na4LZSbEnzdadtuWDehQs1JFxuX/lZhHuVdKgagaMn35j4xubDgy6S9iqRsgJ2Jo 179 | U5o/4B+n5h53uAek4eXAEi0MX3k3RxgAf+ofKiri+oG6zIZcoSpUzj+bOUtVSZwt 180 | 3lsOahDNx5Hd+Atx9iZsylqa/l9iowb+lHfzFAx/58jFhBumn69rNpe9JnJa+vCb 181 | YIsKTiKoJirFSGEgAkcTVXAvo/aD+XiWzc/QP/l+B2X4e5mqR7dF7xLZ5uFbXA0j 182 | AfWMyBtvy/XwBT1SxROXpmCt7J0C9wX5l+3vmTpo6BH6S78BYM+eN/NNZW6eJwAG 183 | km0y3hI1um7pwmzsaE9Pi1xCYEhn6lcLrwPaGXUBCeoTDnO47mrBMAFOmSe8uoRf 184 | 6nYd/TPvXV2Zw0YhjvBzlIfkl5MlJ+j4AZy1hn7Mqe1O//bRd0KKLjjhMQ6tjR6Y 185 | sbUJgKqfgA+W9qxUcLkBDQRO0irvAQgA0LjCc8S6oZzjiap2MjRNhRFA5BYjXZRZ 186 | BdKF2VP74avt2/RELq8GW0n7JWmKn6vvrXabEGLyfkCngAhTq9tJ/K7LPx/bmlO5 187 | +jboO/1inH2BTtLiHjAXvicXZk3oaZt2Sotx5mMI3yzpFQRVqZXsi0LpUTPJEh3o 188 | S8IdYRjslQh1A7P5hfCZwtzwb/hKm8upODe/ITUMuXeWfLuQj/uEU6wMzmfMHb+j 189 | lYMWtb+v98aJa2FODeKPmWCXLa7bliXp1SSeBOEfIgEAmjM6QGlDx5sZhr2Ss2xS 190 | PRdZ8DqD7oiRVzmstX1YoxEzC0yXfaefC7SgM0nMnaTvYEOYJ9CH3wARAQABiQEf 191 | BBgBAgAJBQJO0irvAhsMAAoJEFIKmZOhwFL4844H/jo8icCcS6eOWvnen7lg0FcC 192 | o1fIm4wW3tEmkQdchSHECJDq7pgTloN65pwB5tBoT47cyYNZA9eTfJVgRc74q5ce 193 | xKOYrMC3KuAqWbwqXhkVs0nkWxnOIidTHSXvBZfDFA4Idwte94Thrzf8Pn8UESud 194 | TiqrWoCBXk2UyVsl03gJblSJAeJGYPPeo+Yj6m63OWe2+/S2VTgmbPS/RObn0Aeg 195 | 7yuff0n5+ytEt2KL51gOQE2uIxTCawHr12PsllPkbqPk/PagIttfEJqn9b0CrqPC 196 | 3HREePb2aMJ/Ctw/76COwn0mtXeIXLCTvBmznXfaMKllsqbsy2nCJ2P2uJjOntw= 197 | =4JAR 198 | -----END PGP PUBLIC KEY BLOCK----- 199 | KEY 200 | 201 | data_file = File.expand_path(File.join(File.dirname(__FILE__), 'assets', 'gpg-fixtures', 'data')) 202 | 203 | @recipe.files << { 204 | :url => "file://#{data_file}", 205 | :gpg => { 206 | :key => key, 207 | :signature_url => "file://#{data_file}.asc" 208 | } 209 | } 210 | exception = assert_raises(RuntimeError){ @recipe.download } 211 | assert_includes(exception.message, "signature mismatch") 212 | end 213 | end 214 | -------------------------------------------------------------------------------- /test/test_cmake.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../helper', __FILE__) 2 | 3 | class TestCMake < TestCase 4 | attr_accessor :assets_path, :tar_path, :recipe 5 | 6 | def before_all 7 | super 8 | @assets_path = File.expand_path("../assets", __FILE__) 9 | @tar_path = File.expand_path("../../tmp/test-cmake-1.0.tar.gz", __FILE__) 10 | 11 | # remove any previous test files 12 | FileUtils.rm_rf("tmp") 13 | 14 | create_tar(@tar_path, @assets_path, "test-cmake-1.0") 15 | start_webrick(File.dirname(@tar_path)) 16 | 17 | @logger = StringIO.new # IO to keep recipe logs in case we need to debug 18 | @recipe = init_recipe 19 | 20 | git_dir = File.join(@assets_path, "git") 21 | with_custom_git_dir(git_dir) do 22 | recipe.cook 23 | end 24 | rescue => e 25 | puts @logger.string 26 | raise e 27 | end 28 | 29 | def after_all 30 | super 31 | stop_webrick 32 | # leave test files for inspection 33 | end 34 | 35 | def exe_name 36 | case 37 | when MiniPortile.windows? then "hello.exe" 38 | else "hello" 39 | end 40 | end 41 | 42 | def test_cmake_inherits_from_base 43 | assert(MiniPortileCMake <= MiniPortile) 44 | end 45 | 46 | def test_configure 47 | cmakecache = File.join(work_dir, "CMakeCache.txt") 48 | assert File.exist?(cmakecache), cmakecache 49 | 50 | assert_includes(IO.read(cmakecache), "CMAKE_INSTALL_PREFIX:PATH=#{recipe.path}") 51 | end 52 | 53 | def test_compile 54 | binary = File.join(work_dir, exe_name) 55 | assert File.exist?(binary), binary 56 | end 57 | 58 | def test_install 59 | binary = File.join(recipe.path, "bin", exe_name) 60 | assert File.exist?(binary), binary 61 | end 62 | 63 | def init_recipe 64 | MiniPortileCMake.new("test-cmake", "1.0").tap do |recipe| 65 | recipe.logger = @logger 66 | recipe.files << "http://localhost:#{HTTP_PORT}/#{ERB::Util.url_encode(File.basename(@tar_path))}" 67 | recipe.patch_files << File.join(@assets_path, "patch 1.diff") 68 | end 69 | end 70 | end 71 | 72 | class TestCMakeConfig < TestCMake 73 | def test_make_command_configuration 74 | MiniPortile.stub(:mswin?, false) do 75 | without_env("MAKE") do 76 | assert_equal("make", MiniPortileCMake.new("test", "1.0.0").make_cmd) 77 | assert_equal("xyzzy", MiniPortileCMake.new("test", "1.0.0", make_command: "xyzzy").make_cmd) 78 | end 79 | with_env("MAKE"=>"asdf") do 80 | assert_equal("asdf", MiniPortileCMake.new("test", "1.0.0").make_cmd) 81 | assert_equal("asdf", MiniPortileCMake.new("test", "1.0.0", make_command: "xyzzy").make_cmd) 82 | end 83 | end 84 | 85 | MiniPortile.stub(:mswin?, true) do 86 | assert_equal("nmake", MiniPortileCMake.new("test", "1.0.0").make_cmd) 87 | end 88 | end 89 | 90 | def test_configure_defaults_with_macos 91 | with_env({ "CC" => nil, "CXX" => nil }) do 92 | MiniPortile.stub(:darwin?, true) do 93 | with_stubbed_target(os: 'darwin22', cpu: 'arm64') do 94 | with_compilers(c_compiler: 'clang', cxx_compiler: 'clang++') do 95 | Open3.stub(:capture2, cmake_help_mock('Unix')) do 96 | assert_equal( 97 | [ 98 | "-DCMAKE_SYSTEM_NAME=Darwin", 99 | "-DCMAKE_SYSTEM_PROCESSOR=arm64", 100 | "-DCMAKE_C_COMPILER=clang", 101 | "-DCMAKE_CXX_COMPILER=clang++", 102 | "-DCMAKE_BUILD_TYPE=Release" 103 | ], 104 | @recipe.configure_defaults) 105 | end 106 | end 107 | end 108 | end 109 | end 110 | end 111 | 112 | def test_configure_defaults_with_freebsd 113 | with_env({ "CC" => nil, "CXX" => nil }) do 114 | with_stubbed_target(os: 'freebsd14') do 115 | with_compilers(c_compiler: 'cc', cxx_compiler: 'c++') do 116 | Open3.stub(:capture2, cmake_help_mock('Unix')) do 117 | assert_equal( 118 | [ 119 | "-DCMAKE_SYSTEM_NAME=FreeBSD", 120 | "-DCMAKE_SYSTEM_PROCESSOR=x86_64", 121 | "-DCMAKE_C_COMPILER=cc", 122 | "-DCMAKE_CXX_COMPILER=c++", 123 | "-DCMAKE_BUILD_TYPE=Release" 124 | ], 125 | @recipe.configure_defaults) 126 | end 127 | end 128 | end 129 | end 130 | end 131 | 132 | def test_configure_defaults_with_manual_system_name 133 | MiniPortile.stub(:darwin?, false) do 134 | with_stubbed_target do 135 | with_compilers do 136 | Open3.stub(:capture2, cmake_help_mock('Unix')) do 137 | @recipe.stub(:system_name, 'Custom') do 138 | assert_equal( 139 | [ 140 | "-DCMAKE_SYSTEM_NAME=Custom", 141 | "-DCMAKE_SYSTEM_PROCESSOR=x86_64", 142 | "-DCMAKE_C_COMPILER=gcc", 143 | "-DCMAKE_CXX_COMPILER=g++", 144 | "-DCMAKE_BUILD_TYPE=Release" 145 | ], 146 | @recipe.configure_defaults) 147 | end 148 | end 149 | end 150 | end 151 | end 152 | end 153 | 154 | def test_configure_defaults_with_unix_makefiles 155 | MiniPortile.stub(:linux?, true) do 156 | MiniPortile.stub(:darwin?, false) do 157 | with_stubbed_target do 158 | with_compilers do 159 | Open3.stub(:capture2, cmake_help_mock('Unix')) do 160 | MiniPortile.stub(:mingw?, true) do 161 | assert_equal(default_x86_compile_flags, 162 | @recipe.configure_defaults) 163 | end 164 | end 165 | end 166 | end 167 | end 168 | end 169 | end 170 | 171 | def test_configure_defaults_with_msys_makefiles 172 | MiniPortile.stub(:linux?, true) do 173 | MiniPortile.stub(:darwin?, false) do 174 | with_stubbed_target do 175 | with_compilers do 176 | Open3.stub(:capture2, cmake_help_mock('MSYS')) do 177 | MiniPortile.stub(:mingw?, true) do 178 | assert_equal(['-G', 'MSYS Makefiles'] + default_x86_compile_flags, @recipe.configure_defaults) 179 | end 180 | end 181 | end 182 | end 183 | end 184 | end 185 | end 186 | 187 | def test_configure_defaults_with_nmake_makefiles 188 | MiniPortile.stub(:linux?, true) do 189 | MiniPortile.stub(:darwin?, false) do 190 | with_stubbed_target do 191 | with_compilers do 192 | Open3.stub(:capture2, cmake_help_mock('NMake')) do 193 | MiniPortile.stub(:mswin?, true) do 194 | assert_equal(['-G', 'NMake Makefiles'] + default_x86_compile_flags, @recipe.configure_defaults) 195 | end 196 | end 197 | end 198 | end 199 | end 200 | end 201 | end 202 | 203 | def test_cmake_command_configuration 204 | without_env("CMAKE") do 205 | assert_equal("cmake", MiniPortileCMake.new("test", "1.0.0").cmake_cmd) 206 | assert_equal("xyzzy", MiniPortileCMake.new("test", "1.0.0", cmake_command: "xyzzy").cmake_cmd) 207 | end 208 | with_env("CMAKE"=>"asdf") do 209 | assert_equal("asdf", MiniPortileCMake.new("test", "1.0.0").cmake_cmd) 210 | assert_equal("asdf", MiniPortileCMake.new("test", "1.0.0", cmake_command: "xyzzy").cmake_cmd) 211 | end 212 | end 213 | 214 | def test_cmake_build_type_configuration 215 | without_env("CMAKE_BUILD_TYPE") do 216 | assert_equal("Release", MiniPortileCMake.new("test", "1.0.0").cmake_build_type) 217 | assert_equal("xyzzy", MiniPortileCMake.new("test", "1.0.0", cmake_build_type: "xyzzy").cmake_build_type) 218 | end 219 | with_env("CMAKE_BUILD_TYPE"=>"Debug") do 220 | assert_equal("Debug", MiniPortileCMake.new("test", "1.0.0").cmake_build_type) 221 | assert_equal("Debug", MiniPortileCMake.new("test", "1.0.0", cmake_build_type: "xyzzy").cmake_build_type) 222 | end 223 | end 224 | 225 | private 226 | 227 | def with_stubbed_target(os: 'linux', cpu: 'x86_64') 228 | MiniPortile.stub(:target_os, os) do 229 | MiniPortile.stub(:target_cpu, cpu) do 230 | yield 231 | end 232 | end 233 | end 234 | 235 | def with_compilers(c_compiler: 'gcc', cxx_compiler: 'g++') 236 | @recipe.stub(:cc_cmd, c_compiler) do 237 | @recipe.stub(:cxx_cmd, cxx_compiler) do 238 | yield 239 | end 240 | end 241 | end 242 | 243 | def default_x86_compile_flags 244 | [ 245 | "-DCMAKE_SYSTEM_NAME=Linux", 246 | "-DCMAKE_SYSTEM_PROCESSOR=x86_64", 247 | "-DCMAKE_C_COMPILER=gcc", 248 | "-DCMAKE_CXX_COMPILER=g++", 249 | "-DCMAKE_BUILD_TYPE=Release" 250 | ] 251 | end 252 | 253 | def cmake_help_mock(generator_type) 254 | open3_mock = MiniTest::Mock.new 255 | cmake_script = <<~SCRIPT 256 | echo "The following generators are available on this platform (* marks default):" 257 | echo "* #{generator_type} Makefiles = Generates standard #{generator_type.upcase} makefiles." 258 | SCRIPT 259 | 260 | exit_status = MiniTest::Mock.new 261 | exit_status.expect(:success?, true) 262 | expected_output = [cmake_script, exit_status] 263 | open3_mock.expect(:call, expected_output, ['cmake --help']) 264 | open3_mock 265 | end 266 | end 267 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiniPortile 2 | 3 | This documents versions 2 and up, for which the require file was 4 | renamed to `mini_portile2`. For mini_portile versions 0.6.x and 5 | previous, please visit 6 | [the v0.6.x branch](https://github.com/flavorjones/mini_portile/tree/v0.6.x). 7 | 8 | [![Continuous Integration](https://github.com/flavorjones/mini_portile/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/flavorjones/mini_portile/actions/workflows/ci.yml) 9 | [![Tidelift dependencies](https://tidelift.com/badges/package/rubygems/mini_portile2)](https://tidelift.com/subscription/pkg/rubygems-mini.portile2?utm_source=undefined&utm_medium=referral&utm_campaign=readme) 10 | 11 | * Documentation: http://www.rubydoc.info/github/flavorjones/mini_portile 12 | * Source Code: https://github.com/flavorjones/mini_portile 13 | * Bug Reports: https://github.com/flavorjones/mini_portile/issues 14 | 15 | This project is a minimalistic implementation of a port/recipe system 16 | **for developers**. 17 | 18 | Because _"Works on my machine"_ is unacceptable for a library maintainer. 19 | 20 | 21 | ## Not Another Package Management System 22 | 23 | `mini_portile2` is not a general package management system. It is not 24 | aimed to replace apt, macports or homebrew. 25 | 26 | It's intended primarily to make sure that you, as the developer of a 27 | library, can reproduce a user's dependencies and environment by 28 | specifying a specific version of an underlying dependency that you'd 29 | like to use. 30 | 31 | So, if a user says, "This bug happens on my system that uses libiconv 32 | 1.13.1", `mini_portile2` should make it easy for you to download, 33 | compile and link against libiconv 1.13.1; and run your test suite 34 | against it. 35 | 36 | This scenario might be simplified with something like this: 37 | 38 | ``` 39 | rake compile LIBICONV_VERSION=1.13.1 40 | ``` 41 | 42 | (For your homework, you can make libiconv version be taken from the 43 | appropriate `ENV` variables.) 44 | 45 | 46 | 47 | ## Sounds easy, but where's the catch? 48 | 49 | At this time `mini_portile2` only supports **autoconf**- or 50 | **configure**-based projects. (That is, it assumes the library you 51 | want to build contains a `configure` script, which all the 52 | autoconf-based libraries do.) 53 | 54 | As of v2.2.0, there is experimental support for **CMake**-based 55 | projects. We welcome your feedback on this, particularly for Windows 56 | platforms. 57 | 58 | 59 | ### How to use (for autoconf projects) 60 | 61 | Now that you know the catch, and you're still reading this, here is a 62 | quick example: 63 | 64 | ```ruby 65 | gem "mini_portile2", "~> 2.0.0" # NECESSARY if used in extconf.rb. see below. 66 | require "mini_portile2" 67 | recipe = MiniPortile.new("libiconv", "1.13.1") 68 | recipe.files = ["http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.13.1.tar.gz"] 69 | recipe.cook 70 | recipe.activate 71 | ``` 72 | 73 | The gem version constraint makes sure that your extconf.rb is 74 | protected against possible backwards-incompatible changes to 75 | `mini_portile2`. This constraint is REQUIRED if you're using 76 | `mini_portile2` within a gem installation process (e.g., extconf.rb), 77 | because Bundler doesn't enforce gem version constraints at 78 | install-time (only at run-time. 79 | 80 | `#cook` will download, extract, patch, configure and compile the 81 | library into a namespaced structure. 82 | 83 | `#activate` ensures GCC will find this library and prefer it over a 84 | system-wide installation. 85 | 86 | Some keyword arguments can be passed to the constructor to configure the commands used: 87 | 88 | #### `cc_command` and `cxx_command` 89 | 90 | The C compiler command that is used is configurable, and in order of preference will use: 91 | 92 | - the `CC` environment variable (if present) 93 | - the `:cc_command` keyword argument passed in to the constructor 94 | - `RbConfig::CONFIG["CC"]` 95 | - `"gcc"` 96 | 97 | The C++ compiler is similarly configuratble, and in order of preference will use: 98 | 99 | - the `CXX` environment variable (if present) 100 | - the `:cxx_command` keyword argument passed in to the constructor 101 | - `RbConfig::CONFIG["CXX"]` 102 | - `"g++"` 103 | 104 | You can pass your compiler commands to the MiniPortile constructor: 105 | 106 | ``` ruby 107 | MiniPortile.new("libiconv", "1.13.1", cc_command: "clang", cxx_command: "clang++") 108 | ``` 109 | 110 | (For backwards compatibility, the constructor also supports a keyword argument `:gcc_command` for the C compiler.) 111 | 112 | #### `make_command` 113 | 114 | The configuration/make command that is used is configurable, and in order of preference will use: 115 | 116 | - the `MAKE` environment variable (if present) 117 | - the `make_command` value passed in to the constructor 118 | - the `make` environment variable (if present) 119 | - `"make"` 120 | 121 | You can pass it in like so: 122 | 123 | ``` ruby 124 | MiniPortile.new("libiconv", "1.13.1", make_command: "nmake") 125 | ``` 126 | 127 | #### `open_timeout`, `read_timeout` 128 | 129 | By default, when downloading source archives, MiniPortile will use a timeout value of 10 130 | seconds. This can be overridden by passing a different value (in seconds): 131 | 132 | ``` ruby 133 | MiniPortile.new("libiconv", "1.13.1", open_timeout: 99, read_timeout: 2) 134 | ``` 135 | 136 | 137 | ### How to use (for cmake projects) 138 | 139 | Same as above, but instead of `MiniPortile.new`, call `MiniPortileCMake.new`. 140 | 141 | #### `make_command` 142 | 143 | This is configurable as above, except for Windows systems where it's hardcoded to `"nmake"`. 144 | 145 | #### `cmake_command` 146 | 147 | The cmake command used is configurable, and in order of preference will use: 148 | 149 | - the `CMAKE` environment variable (if present) 150 | - the `:cmake_command` keyword argument passed into the constructor 151 | - `"cmake"` (the default) 152 | 153 | You can pass it in like so: 154 | 155 | ``` ruby 156 | MiniPortileCMake.new("libfoobar", "1.3.5", cmake_command: "cmake3") 157 | ``` 158 | 159 | #### `cmake_build_type` 160 | 161 | The cmake build type is configurable as of v2.8.5, and in order of preference will use: 162 | 163 | - the `CMAKE_BUILD_TYPE` environment variable (if present) 164 | - the `:cmake_build_type` keyword argument passed into the constructor 165 | - `"Release"` (the default) 166 | 167 | You can pass it in like so: 168 | 169 | ``` ruby 170 | MiniPortileCMake.new("libfoobar", "1.3.5", cmake_build_type: "Debug") 171 | ``` 172 | 173 | ### Local source directories 174 | 175 | Instead of downloading a remote file, you can also point mini_portile2 at a local source 176 | directory. In particular, this may be useful for testing or debugging: 177 | 178 | ``` ruby 179 | gem "mini_portile2", "~> 2.0.0" # NECESSARY if used in extconf.rb. see below. 180 | require "mini_portile2" 181 | recipe = MiniPortile.new("libiconv", "1.13.1") 182 | recipe.source_directory = "/path/to/local/source/for/library-1.2.3" 183 | ``` 184 | 185 | ### Directory Structure Conventions 186 | 187 | `mini_portile2` follows the principle of **convention over configuration** and 188 | established a folder structure where is going to place files and perform work. 189 | 190 | Take the above example, and let's draw some picture: 191 | 192 | ``` 193 | mylib 194 | |-- ports 195 | | |-- archives 196 | | | `-- libiconv-1.13.1.tar.gz 197 | | `-- 198 | | `-- libiconv 199 | | `-- 1.13.1 200 | | |-- bin 201 | | |-- include 202 | | `-- lib 203 | `-- tmp 204 | `-- 205 | `-- ports 206 | ``` 207 | 208 | In above structure, `` refers to the architecture that 209 | represents the operating system you're using (e.g. i686-linux, 210 | i386-mingw32, etc). 211 | 212 | Inside the platform folder, `mini_portile2` will store the artifacts 213 | that result from the compilation process. The library is versioned so 214 | you can keep multiple versions around on disk without clobbering 215 | anything. 216 | 217 | `archives` is where downloaded source files are cached. It is 218 | recommended you avoid trashing that folder to avoid downloading the 219 | same file multiple times (save bandwidth, save the world). 220 | 221 | `tmp` is where compilation is performed and can be safely discarded. 222 | 223 | Use the recipe's `#path` to obtain the full path to the installation 224 | directory: 225 | 226 | ```ruby 227 | recipe.cook 228 | recipe.path # => /home/luis/projects/myapp/ports/i686-linux/libiconv/1.13.1 229 | ``` 230 | 231 | ### How can I combine this with my compilation task? 232 | 233 | In the simplest case, your rake `compile` task will depend on 234 | `mini_portile2` compilation and most important, activation. 235 | 236 | Example: 237 | 238 | ```ruby 239 | task :libiconv do 240 | recipe = MiniPortile.new("libiconv", "1.13.1") 241 | recipe.files << { 242 | url: "http://ftp.gnu.org/pub/gnu/libiconv/libiconv-1.13.1.tar.gz"], 243 | sha256: "55a36168306089009d054ccdd9d013041bfc3ab26be7033d107821f1c4949a49" 244 | } 245 | checkpoint = ".#{recipe.name}-#{recipe.version}.installed" 246 | 247 | unless File.exist?(checkpoint) 248 | recipe.cook 249 | touch checkpoint 250 | end 251 | 252 | recipe.activate 253 | end 254 | 255 | task :compile => [:libiconv] do 256 | # ... your library's compilation task ... 257 | end 258 | ``` 259 | 260 | The above example will: 261 | 262 | * **download** and verify integrity the sources only once 263 | * **compile** the library only once (using a timestamp file) 264 | * ensure compiled library is **activated** 265 | * make the compile task depend upon compiled library activation 266 | 267 | As an exercise for the reader, you could specify the libiconv version 268 | in an environment variable or a configuration file. 269 | 270 | ### Download verification 271 | MiniPortile supports HTTPS, HTTP, FTP and FILE sources for download. 272 | The integrity of the downloaded file can be verified per hash value or PGP signature. 273 | This is particular important for untrusted sources (non-HTTPS). 274 | 275 | #### Hash digest verification 276 | MiniPortile can verify the integrity of the downloaded file per SHA256, SHA1 or MD5 hash digest. 277 | 278 | ```ruby 279 | recipe.files << { 280 | url: "http://your.host/file.tar.bz2", 281 | sha256: "<32 byte hex value>", 282 | } 283 | ``` 284 | 285 | #### PGP signature verification 286 | MiniPortile can also verify the integrity of the downloaded file per PGP signature. 287 | 288 | ```ruby 289 | public_key = <<-EOT 290 | -----BEGIN PGP PUBLIC KEY BLOCK----- 291 | Version: GnuPG v1 292 | 293 | mQENBE7SKu8BCADQo6x4ZQfAcPlJMLmL8zBEBUS6GyKMMMDtrTh3Yaq481HB54oR 294 | [...] 295 | -----END PGP PUBLIC KEY BLOCK----- 296 | EOT 297 | 298 | recipe.files << { 299 | url: "http://your.host/file.tar.bz2", 300 | gpg: { 301 | key: public_key, 302 | signature_url: "http://your.host/file.tar.bz2.sig" 303 | } 304 | } 305 | ``` 306 | 307 | Please note, that the `gpg` executable is required to verify the signature. 308 | It is therefore recommended to use the hash verification method instead of PGP, when used in `extconf.rb` while `gem install`. 309 | 310 | ### Native and/or Cross Compilation 311 | 312 | The above example covers the normal use case: compiling dependencies 313 | natively. 314 | 315 | `MiniPortile` also covers another use case, which is the 316 | cross-compilation of the dependencies to be used as part of a binary 317 | gem compilation. 318 | 319 | It is the perfect complementary tool for 320 | [`rake-compiler`](https://github.com/rake-compiler/rake-compiler) and 321 | its `cross` rake task. 322 | 323 | Depending on your usage of `rake-compiler`, you will need to use 324 | `host` to match the installed cross-compiler toolchain. 325 | 326 | Please refer to the examples directory for simplified and practical usage. 327 | 328 | 329 | ### Supported Scenarios 330 | 331 | As mentioned before, `MiniPortile` requires a GCC compiler 332 | toolchain. This has been tested against Ubuntu, OSX and even Windows 333 | (RubyInstaller with DevKit) 334 | 335 | 336 | ## Support 337 | 338 | The bug tracker is available here: 339 | 340 | * https://github.com/flavorjones/mini_portile/issues 341 | 342 | Consider subscribing to [Tidelift][tidelift] which provides license assurances and timely security notifications for your open source dependencies, including Loofah. [Tidelift][tidelift] subscriptions also help the Loofah maintainers fund our [automated testing](https://ci.nokogiri.org) which in turn allows us to ship releases, bugfixes, and security updates more often. 343 | 344 | [tidelift]: https://tidelift.com/subscription/pkg/rubygems-mini.portile2?utm_source=rubygems-mini.portile2&utm_medium=referral&utm_campaign=enterprise 345 | 346 | 347 | ## Security 348 | 349 | See [`SECURITY.md`](SECURITY.md) for vulnerability reporting details. 350 | 351 | 352 | ## License 353 | 354 | This library is licensed under MIT license. Please see LICENSE.txt for details. 355 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## mini_portile changelog 2 | 3 | ### 2.8.9 / 2025-05-12 4 | 5 | ### Ruby support 6 | 7 | * Import only what's needed from `cgi`, for supporting Ruby 3.5. #160 @Earlopain 8 | 9 | 10 | ### 2.8.8 / 2024-11-14 11 | 12 | #### Improved 13 | 14 | - Raise an exception with a clear error message when `xzcat` is needed but is not installed. (#152) @flavorjones 15 | 16 | 17 | ### 2.8.7 / 2024-05-31 18 | 19 | #### Added 20 | 21 | - When setting the C compiler through the `MiniPortile` constructor, the preferred keyword argument is now `:cc_command`. The original `:gcc_command` is still supported. (#144 by @flavorjones) 22 | - Add support for extracting xz-compressed tarballs on OpenBSD. (#141 by @postmodern) 23 | - Add OpenBSD support to the experimental method `MakeMakefile#mkmf_config`. (#141 by @flavorjones) 24 | 25 | 26 | #### Changed 27 | 28 | - `MiniPortileCMake` now detects the C and C++ compiler the same way `MiniPortile` does: by examining environment variables, then using kwargs, then looking in RbConfig (in that order). (#144 by @flavorjones) 29 | - GPG file verification error messages are captured in the raised exception. Previously these errors went to `stderr`. (#145 by @flavorjones) 30 | 31 | 32 | ### 2.8.6 / 2024-04-14 33 | 34 | #### Added 35 | 36 | - When using CMake on FreeBSD, default to clang's "cc" and "c++" compilers. (#139 by @mudge) 37 | 38 | 39 | ### 2.8.5 / 2023-10-22 40 | 41 | #### Added 42 | 43 | - New methods `#lib_path` and `#include_path` which point at the installed directories under `ports`. (by @flavorjones) 44 | - Add config param for CMAKE_BUILD_TYPE, which now defaults to `Release`. (#136 by @Watson1978) 45 | 46 | #### Experimental 47 | 48 | Introduce experimental support for `MiniPortile#mkmf_config` which sets up MakeMakefile variables to properly link against the recipe. This should make it easier for C extensions to package third-party libraries. (by @flavorjones) 49 | 50 | - With no arguments, will set up just `$INCFLAGS`, `$libs`, and `$LIBPATH`. 51 | - Optionally, if provided a pkg-config file, will use that config to more precisely set `$INCFLAGS`, `$libs`, `$LIBPATH`, and `$CFLAGS`/`$CXXFLAGS`. 52 | - Optionally, if provided the name of a static archive, will rewrite linker flags to ensure correct linkage. 53 | 54 | Note that the behavior may change slightly before official support is announced. Please comment on [#118](https://github.com/flavorjones/mini_portile/issues/118) if you have feedback. 55 | 56 | 57 | ### 2.8.4 / 2023-07-18 58 | 59 | - cmake: set CMAKE compile flags to configure cross-compilation similarly to `autotools` `--host` flag: `SYSTEM_NAME`, `SYSTEM_PROCESSOR`, `C_COMPILER`, and `CXX_COMPILER`. [#130] (Thanks, @stanhu!) 60 | 61 | 62 | ### 2.8.3 / 2023-07-18 63 | 64 | #### Fixed 65 | 66 | - cmake: only use MSYS/NMake generators when available. [#129] (Thanks, @stanhu!) 67 | 68 | 69 | ### 2.8.2 / 2023-04-30 70 | 71 | #### Fixed 72 | 73 | - Ensure that the `source_directory` option will work when given a Windows path to an autoconf directory. [#126] 74 | 75 | 76 | ### 2.8.1 / 2022-12-24 77 | 78 | #### Fixed 79 | 80 | - Support applying patches via `git apply` even when the working directory resembles a git directory. [#119] (Thanks, @h0tw1r3!) 81 | 82 | 83 | ### 2.8.0 / 2022-02-20 84 | 85 | #### Added 86 | 87 | - Support xz-compressed archives (recognized by an `.xz` file extension). 88 | - When downloading a source archive, default open_timeout and read_timeout to 10 seconds, but allow configuration via open_timeout and read_timeout config parameters. 89 | 90 | 91 | ### 2.7.1 / 2021-10-20 92 | 93 | #### Packaging 94 | 95 | A test artifact that has been included in the gem was being flagged by some users' security scanners because it wasn't a real tarball. That artifact has been updated to be a real tarball. [#108] 96 | 97 | 98 | ### 2.7.0 / 2021-08-31 99 | 100 | #### Added 101 | 102 | The commands used for "make", "compile", and "cmake" are configurable via keyword arguments. [#107] (Thanks, @cosmo0920!) 103 | 104 | 105 | ### 2.6.1 / 2021-05-31 106 | 107 | #### Dependencies 108 | 109 | Make `net-ftp` an optional dependency, since requiring it as a hard dependency in v2.5.2 caused warnings to be emitted by Ruby 2.7 and earlier. A warning message is emitted if FTP functionality is called and `net-ftp` isn't available; this should only happen in Ruby 3.1 and later. 110 | 111 | 112 | ### 2.5.3 / 2021-05-31 113 | 114 | #### Dependencies 115 | 116 | Make `net-ftp` an optional dependency, since requiring it as a hard dependency in v2.5.2 caused warnings to be emitted by Ruby 2.7 and earlier. A warning message is emitted if FTP functionality is called and `net-ftp` isn't available; this should only happen in Ruby 3.1 and later. 117 | 118 | 119 | ### 2.6.0 / 2021-05-31 120 | 121 | ### Added 122 | 123 | Recipes may build against a local directory by specifying `source_directory` instead of `files`. In 124 | particular, this may be useful for debugging problems with the upstream dependency (e.g., use `git 125 | bisect` in a local clone) or for continuous integration with upstream HEAD. 126 | 127 | 128 | ### 2.5.2 / 2021-05-28 129 | 130 | #### Dependencies 131 | 132 | Add `net-ftp` as an explicit dependency to accommodate the upcoming Ruby 3.1 changes that move this and other gems out of the "default" gem set and into the "bundled" gem set. See https://bugs.ruby-lang.org/issues/17873 [#101] 133 | 134 | 135 | ### 2.5.1 / 2021-04-28 136 | 137 | #### Dependencies 138 | 139 | This release ends support for ruby < 2.3.0. If you're on 2.2.x or earlier, we strongly suggest that you find the time to upgrade, because [official support for Ruby 2.2 ended on 2018-03-31](https://www.ruby-lang.org/en/news/2018/06/20/support-of-ruby-2-2-has-ended/). 140 | 141 | #### Enhancements 142 | 143 | * `MiniPortile.execute` now takes an optional `:env` hash, which is merged into the environment variables for the subprocess. Likely this is only useful for specialized use cases. [#99] 144 | * Experimental support for cmake-based projects extended to Windows. (Thanks, @larskanis!) 145 | 146 | 147 | ### 2.5.0 / 2020-02-24 148 | 149 | #### Enhancements 150 | 151 | * When verifying GPG signatures, remove all imported pubkeys from keyring [#90] (Thanks, @hanazuki!) 152 | 153 | 154 | ### 2.4.0 / 2018-12-02 155 | 156 | #### Enhancements 157 | 158 | * Skip progress report when Content-Length is unavailable. [#85] (Thanks, @eagletmt!) 159 | 160 | 161 | ### 2.3.0 / 2017-09-13 162 | 163 | #### Enhancements 164 | 165 | * Verify checksums of files at extraction time (in addition to at download time). (#56) 166 | * Clarify error message if a `tar` command can't be found. (#81) 167 | 168 | 169 | ### 2.2.0 / 2017-06-04 170 | 171 | #### Enhancements 172 | 173 | * Remove MD5 hashing of configure options, not avialbale in FIPS mode. (#78) 174 | * Add experimental support for cmake-based projects. 175 | * Retry on HTTP failures during downloads. [#63] (Thanks, @jtarchie and @jvshahid!) 176 | * Support Ruby 2.4 frozen string literals. 177 | * Support applying patches for users with misconfigured git worktree. [#69] 178 | * Support gpg signature verification of download resources. 179 | 180 | 181 | ### 2.1.0 / 2016-01-06 182 | 183 | #### Enhancements 184 | 185 | * Add support for `file:` protocol for tarballs 186 | 187 | 188 | #### Bugfixes 189 | 190 | * Raise exception on unsupported URI protocols 191 | * Ignore git whitespace config when patching (Thanks, @e2!) (#67) 192 | 193 | 194 | ### 2.0.0 / 2015-11-30 195 | 196 | Many thanks to @larskanis, @knu, and @kirikak2, who all contributed 197 | code, ideas, or both to this release. 198 | 199 | Note that the 0.7.0.rc* series was not released as 0.7.0 final, and 200 | instead became 2.0.0 due to backwards-incompatible behavioral changes 201 | which can appear because rubygems doesn't enforce loading the declared 202 | dependency version at installation-time (only run-time). 203 | 204 | If you use MiniPortile in an `extconf.rb` file, please make sure you're 205 | setting a gem version constraint before `require "mini_portile2"` . 206 | 207 | Note also that 2.0.0 doesn't include the backwards-compatible "escaped 208 | string" behavior from 0.7.0.rc3. 209 | 210 | 211 | #### Enhancements 212 | 213 | * In patch task, use git(1) or patch(1), whichever is available. 214 | * Append outputs to patch.log instead of clobbering it for every patch command. 215 | * Take `configure_options` literally without running a subshell. 216 | This changes allows for embedded spaces in a path, among other things. 217 | Please unescape `configure_options` where you have been doing it yourself. 218 | * Print last 20 lines of the given log file, for convenience. 219 | * Allow SHA1, SHA256 and MD5 hash verification of downloads 220 | 221 | 222 | #### Bugfixes 223 | 224 | * Fix issue when proxy username/password use escaped characters. 225 | * Fix use of https and ftp proxy. 226 | 227 | 228 | ### 0.7.0.rc4 / 2015-08-24 229 | 230 | * Updated tests for Windows. No functional change. Final release candidate? 231 | 232 | 233 | ### 0.7.0.rc3 / 2015-08-24 234 | 235 | * Restore backwards-compatible behavior with respect to escaped strings. 236 | 237 | 238 | ### 0.7.0.rc2 / 2015-08-24 239 | 240 | * Restore support for Ruby 1.9.2 241 | * Add Ruby 2.0.0 and Ruby 2.1.x to Appveyor suite 242 | 243 | 244 | ### 0.7.0.rc1 / 2015-08-24 245 | 246 | Many thanks to @larskanis, @knu, and @kirikak2, who all contributed 247 | code, ideas, or both to this release. 248 | 249 | #### Enhancements 250 | 251 | * In patch task, use git(1) or patch(1), whichever is available. 252 | * Append outputs to patch.log instead of clobbering it for every patch command. 253 | * Take `configure_options` literally without running a subshell. 254 | This changes allows for embedded spaces in a path, among other things. 255 | Please unescape `configure_options` where you have been doing it yourself. 256 | * Print last 20 lines of the given log file, for convenience. 257 | * Allow SHA1, SHA256 and MD5 hash verification of downloads 258 | 259 | 260 | #### Bugfixes 261 | 262 | * Fix issue when proxy username/password use escaped characters. 263 | * Fix use of https and ftp proxy. 264 | 265 | 266 | ### 0.6.2 / 2014-12-30 267 | 268 | * Updated gemspec, license and README to reflect new maintainer. 269 | 270 | 271 | ### 0.6.1 / 2014-08-03 272 | 273 | * Enhancements: 274 | * Expand path to logfile to easier debugging on failures. 275 | Pull #33 [marvin2k] 276 | 277 | ### 0.6.0 / 2014-04-18 278 | 279 | * Enhancements: 280 | * Add default cert store and custom certs from `SSL_CERT_FILE` if present. 281 | This increases compatibility with Ruby 1.8.7. 282 | 283 | * Bugfixes: 284 | * Specify Accept-Encoding to make sure a raw file content is downloaded. 285 | Pull #30. [knu] 286 | 287 | * Internal: 288 | * Improve examples and use them as test harness. 289 | 290 | ### 0.5.3 / 2014-03-24 291 | 292 | * Bugfixes: 293 | * Shell escape paths in tar command. Pull #29. [quickshiftin] 294 | * Support older versions of tar that cannot auto-detect 295 | the compression type. Pull #27. Closes #21. [b-dean] 296 | * Try RbConfig's CC before fall back to 'gcc'. Ref #28. 297 | 298 | ### 0.5.2 / 2013-10-23 299 | 300 | * Bugfixes: 301 | * Change tar detection order to support NetBSD 'gtar'. Closes #24 302 | * Trick 'git-apply' when applying patches on nested Git checkouts. [larskanis] 303 | * Respect ENV's MAKE before fallback to 'make'. [larskanis] 304 | * Respect ENV's CC variable before fallback to 'gcc'. 305 | * Avoid non-ASCII output of GCC cause host detection issues. Closes #22 306 | 307 | ### 0.5.1 / 2013-07-07 308 | 309 | * Bugfixes: 310 | * Detect tar executable without shelling out. [jtimberman] 311 | 312 | ### 0.5.0 / 2012-11-17 313 | 314 | * Enhancements: 315 | * Allow patching extracted files using `git apply`. [metaskills] 316 | 317 | ### 0.4.1 / 2012-10-24 318 | 319 | * Bugfixes: 320 | * Syntax to process FTp binary chunks differs between Ruby 1.8.7 and 1.9.x 321 | 322 | ### 0.4.0 / 2012-10-24 323 | 324 | * Enhancements: 325 | * Allow fetching of FTP URLs along HTTP ones. [metaskills] 326 | 327 | ### 0.3.0 / 2012-03-23 328 | 329 | * Enhancements: 330 | * Use `gcc -v` to determine original host (platform) instead of Ruby one. 331 | 332 | * Deprecations: 333 | * Dropped support for Rubies older than 1.8.7 334 | 335 | ### 0.2.2 / 2011-04-11 336 | 337 | * Minor enhancements: 338 | * Use LDFLAGS when activating recipes for cross-compilation. Closes #6 339 | 340 | * Bugfixes: 341 | * Remove memoization of *_path helpers. Closes #7 342 | 343 | ### 0.2.1 / 2011-04-06 344 | 345 | * Minor enhancements: 346 | * Provide MiniPortile#path to obtain full path to installation directory. Closes GH-5 347 | 348 | ### 0.2.0 / 2011-04-05 349 | 350 | * Enhancements: 351 | * Improve tar detection 352 | * Improve and refactor configure_options 353 | * Detect configure_options changes. Closes GH-1 354 | * Add recipe examples 355 | 356 | * Bugfixes: 357 | * MiniPortile#target can be changed now. Closes GH-2 358 | * Always redirect tar output properly 359 | 360 | ### 0.1.0 / 2011-03-08 361 | 362 | * Initial release. Welcome to this world! 363 | -------------------------------------------------------------------------------- /lib/mini_portile2/mini_portile.rb: -------------------------------------------------------------------------------- 1 | require 'rbconfig' 2 | require 'net/http' 3 | require 'net/https' 4 | require 'fileutils' 5 | require 'tempfile' 6 | require 'digest' 7 | require 'open-uri' 8 | require "cgi/escape" 9 | require "cgi/util" if RUBY_VERSION < "3.5" 10 | require 'rbconfig' 11 | require 'shellwords' 12 | require 'open3' 13 | 14 | # Monkey patch for Net::HTTP by ruby open-uri fix: 15 | # https://github.com/ruby/ruby/commit/58835a9 16 | class Net::HTTP 17 | private 18 | remove_method(:edit_path) 19 | def edit_path(path) 20 | if proxy? 21 | if path.start_with?("ftp://") || use_ssl? 22 | path 23 | else 24 | "http://#{addr_port}#{path}" 25 | end 26 | else 27 | path 28 | end 29 | end 30 | end 31 | 32 | $MINI_PORTILE_STATIC_LIBS = {} 33 | 34 | class MiniPortile 35 | DEFAULT_TIMEOUT = 10 36 | 37 | attr_reader :name, :version, :original_host, :source_directory 38 | attr_writer :configure_options 39 | attr_accessor :host, :files, :patch_files, :target, :logger 40 | 41 | def self.windows? 42 | target_os =~ /mswin|mingw/ 43 | end 44 | 45 | # GNU MinGW compiled Ruby? 46 | def self.mingw? 47 | target_os =~ /mingw/ 48 | end 49 | 50 | # MS Visual-C compiled Ruby? 51 | def self.mswin? 52 | target_os =~ /mswin/ 53 | end 54 | 55 | def self.darwin? 56 | target_os =~ /darwin/ 57 | end 58 | 59 | def self.freebsd? 60 | target_os =~ /freebsd/ 61 | end 62 | 63 | def self.openbsd? 64 | target_os =~ /openbsd/ 65 | end 66 | 67 | def self.linux? 68 | target_os =~ /linux/ 69 | end 70 | 71 | def self.solaris? 72 | target_os =~ /solaris/ 73 | end 74 | 75 | def self.target_os 76 | RbConfig::CONFIG['target_os'] 77 | end 78 | 79 | def self.target_cpu 80 | RbConfig::CONFIG['target_cpu'] 81 | end 82 | 83 | def self.native_path(path) 84 | path = File.expand_path(path) 85 | if File::ALT_SEPARATOR 86 | path.tr(File::SEPARATOR, File::ALT_SEPARATOR) 87 | else 88 | path 89 | end 90 | end 91 | 92 | def self.posix_path(path) 93 | path = File.expand_path(path) 94 | if File::ALT_SEPARATOR 95 | "/" + path.tr(File::ALT_SEPARATOR, File::SEPARATOR).tr(":", File::SEPARATOR) 96 | else 97 | path 98 | end 99 | end 100 | 101 | def initialize(name, version, **kwargs) 102 | @name = name 103 | @version = version 104 | @target = 'ports' 105 | @files = [] 106 | @patch_files = [] 107 | @log_files = {} 108 | @logger = kwargs[:logger] || STDOUT 109 | @source_directory = nil 110 | 111 | @cc_command = kwargs[:cc_command] || kwargs[:gcc_command] 112 | @cxx_command = kwargs[:cxx_command] 113 | @make_command = kwargs[:make_command] 114 | @open_timeout = kwargs[:open_timeout] || DEFAULT_TIMEOUT 115 | @read_timeout = kwargs[:read_timeout] || DEFAULT_TIMEOUT 116 | 117 | @original_host = @host = detect_host 118 | end 119 | 120 | def source_directory=(path) 121 | @source_directory = posix_path(path) 122 | end 123 | 124 | def prepare_build_directory 125 | raise "source_directory is not set" if source_directory.nil? 126 | output "Building #{@name} from source at '#{source_directory}'" 127 | FileUtils.mkdir_p(File.join(tmp_path, [name, version].join("-"))) 128 | FileUtils.rm_rf(port_path) # make sure we always re-install 129 | end 130 | 131 | def download 132 | files_hashs.each do |file| 133 | download_file(file[:url], file[:local_path]) 134 | verify_file(file) 135 | end 136 | end 137 | 138 | def extract 139 | files_hashs.each do |file| 140 | verify_file(file) 141 | extract_file(file[:local_path], tmp_path) 142 | end 143 | end 144 | 145 | def apply_patch(patch_file) 146 | ( 147 | # Not a class variable because closures will capture self. 148 | @apply_patch ||= 149 | case 150 | when which('git') 151 | lambda { |file| 152 | message "Running git apply with #{file}... " 153 | Dir.mktmpdir do |tmp_git_dir| 154 | execute('patch', ["git", "--git-dir=#{tmp_git_dir}", "--work-tree=.", "apply", "--whitespace=warn", file], :initial_message => false) 155 | end 156 | } 157 | when which('patch') 158 | lambda { |file| 159 | message "Running patch with #{file}... " 160 | execute('patch', ["patch", "-p1", "-i", file], :initial_message => false) 161 | } 162 | else 163 | raise "Failed to complete patch task; patch(1) or git(1) is required." 164 | end 165 | ).call(patch_file) 166 | end 167 | 168 | def patch 169 | @patch_files.each do |full_path| 170 | next unless File.exist?(full_path) 171 | apply_patch(full_path) 172 | end 173 | end 174 | 175 | def configure_options 176 | @configure_options ||= configure_defaults 177 | end 178 | 179 | def configure 180 | return if configured? 181 | 182 | FileUtils.mkdir_p(tmp_path) 183 | cache_file = File.join(tmp_path, 'configure.options_cache') 184 | File.open(cache_file, "w") { |f| f.write computed_options.to_s } 185 | 186 | command = Array(File.join((source_directory || "."), "configure")) 187 | if RUBY_PLATFORM=~/mingw|mswin/ 188 | # Windows doesn't recognize the shebang. 189 | command.unshift("sh") 190 | end 191 | execute('configure', command + computed_options, altlog: "config.log") 192 | end 193 | 194 | def compile 195 | execute('compile', make_cmd) 196 | end 197 | 198 | def install 199 | return if installed? 200 | execute('install', %Q(#{make_cmd} install)) 201 | end 202 | 203 | def downloaded? 204 | missing = files_hashs.detect do |file| 205 | !File.exist?(file[:local_path]) 206 | end 207 | 208 | missing ? false : true 209 | end 210 | 211 | def configured? 212 | configure = File.join((source_directory || work_path), 'configure') 213 | makefile = File.join(work_path, 'Makefile') 214 | cache_file = File.join(tmp_path, 'configure.options_cache') 215 | 216 | stored_options = File.exist?(cache_file) ? File.read(cache_file) : "" 217 | current_options = computed_options.to_s 218 | 219 | (current_options == stored_options) && newer?(makefile, configure) 220 | end 221 | 222 | def installed? 223 | makefile = File.join(work_path, 'Makefile') 224 | target_dir = Dir.glob("#{port_path}/*").find { |d| File.directory?(d) } 225 | 226 | newer?(target_dir, makefile) 227 | end 228 | 229 | def cook 230 | if source_directory 231 | prepare_build_directory 232 | else 233 | download unless downloaded? 234 | extract 235 | patch 236 | end 237 | configure unless configured? 238 | compile 239 | install unless installed? 240 | 241 | return true 242 | end 243 | 244 | def activate 245 | vars = { 246 | 'PATH' => File.join(port_path, 'bin'), 247 | 'CPATH' => include_path, 248 | 'LIBRARY_PATH' => lib_path, 249 | }.reject { |env, path| !File.directory?(path) } 250 | 251 | output "Activating #{@name} #{@version} (from #{port_path})..." 252 | vars.each do |var, path| 253 | full_path = native_path(path) 254 | 255 | # save current variable value 256 | old_value = ENV[var] || '' 257 | 258 | unless old_value.include?(full_path) 259 | ENV[var] = "#{full_path}#{File::PATH_SEPARATOR}#{old_value}" 260 | end 261 | end 262 | 263 | # rely on LDFLAGS when cross-compiling 264 | if File.exist?(lib_path) && (@host != @original_host) 265 | full_path = native_path(lib_path) 266 | 267 | old_value = ENV.fetch("LDFLAGS", "") 268 | 269 | unless old_value.include?(full_path) 270 | ENV["LDFLAGS"] = "-L#{full_path} #{old_value}".strip 271 | end 272 | end 273 | end 274 | 275 | # pkg: the pkg-config file name (without the .pc extension) 276 | # dir: inject the directory path for the pkg-config file (probably only useful for tests) 277 | # static: the name of the static library archive (without the "lib" prefix or the file extension), or nil for dynamic linking 278 | # 279 | # we might be able to be terribly clever and infer the name of the static archive file, but 280 | # unfortunately projects have so much freedom in what they can report (for name, for libs, etc.) 281 | # that it feels unreliable to try to do so, so I'm preferring to just have the developer make it 282 | # explicit. 283 | def mkmf_config(pkg: nil, dir: nil, static: nil) 284 | require "mkmf" 285 | 286 | if pkg 287 | dir ||= File.join(lib_path, "pkgconfig") 288 | pcfile = File.join(dir, "#{pkg}.pc") 289 | unless File.exist?(pcfile) 290 | raise ArgumentError, "pkg-config file '#{pcfile}' does not exist" 291 | end 292 | 293 | output "Configuring MakeMakefile for #{File.basename(pcfile)} (in #{File.dirname(pcfile)})\n" 294 | 295 | # on macos, pkg-config will not return --cflags without this 296 | ENV["PKG_CONFIG_ALLOW_SYSTEM_CFLAGS"] = "t" 297 | 298 | # append to PKG_CONFIG_PATH as we go, so later pkg-config files can depend on earlier ones 299 | ENV["PKG_CONFIG_PATH"] = [ENV["PKG_CONFIG_PATH"], dir].compact.join(File::PATH_SEPARATOR) 300 | 301 | incflags = minimal_pkg_config(pcfile, "cflags-only-I") 302 | cflags = minimal_pkg_config(pcfile, "cflags-only-other") 303 | if static 304 | ldflags = minimal_pkg_config(pcfile, "libs-only-L", "static") 305 | libflags = minimal_pkg_config(pcfile, "libs-only-l", "static") 306 | else 307 | ldflags = minimal_pkg_config(pcfile, "libs-only-L") 308 | libflags = minimal_pkg_config(pcfile, "libs-only-l") 309 | end 310 | else 311 | output "Configuring MakeMakefile for #{@name} #{@version} (from #{path})\n" 312 | 313 | lib_name = name.sub(/\Alib/, "") # TODO: use delete_prefix when we no longer support ruby 2.4 314 | 315 | incflags = Dir.exist?(include_path) ? "-I#{include_path}" : "" 316 | cflags = "" 317 | ldflags = Dir.exist?(lib_path) ? "-L#{lib_path}" : "" 318 | libflags = Dir.exist?(lib_path) ? "-l#{lib_name}" : "" 319 | end 320 | 321 | if static 322 | libdir = lib_path 323 | if pcfile 324 | pcfile_libdir = minimal_pkg_config(pcfile, "variable=libdir").strip 325 | libdir = pcfile_libdir unless pcfile_libdir.empty? 326 | end 327 | 328 | # 329 | # keep track of the libraries we're statically linking against, and fix up ldflags and 330 | # libflags to make sure we link statically against the recipe's libaries. 331 | # 332 | # this avoids the unintentionally dynamically linking against system libraries, and makes sure 333 | # that if multiple pkg-config files reference each other that we are able to intercept flags 334 | # from dependent packages that reference the static archive. 335 | # 336 | $MINI_PORTILE_STATIC_LIBS[static] = libdir 337 | static_ldflags = $MINI_PORTILE_STATIC_LIBS.values.map { |v| "-L#{v}" } 338 | static_libflags = $MINI_PORTILE_STATIC_LIBS.keys.map { |v| "-l#{v}" } 339 | 340 | # remove `-L#{libdir}` and `-lfoo`. we don't need them since we link against the static 341 | # archive using the full path. 342 | ldflags = ldflags.shellsplit.reject { |f| static_ldflags.include?(f) }.shelljoin 343 | libflags = libflags.shellsplit.reject { |f| static_libflags.include?(f) }.shelljoin 344 | 345 | # prepend the full path to the static archive to the linker flags 346 | static_archive = File.join(libdir, "lib#{static}.#{$LIBEXT}") 347 | libflags = [static_archive, libflags].join(" ").strip 348 | end 349 | 350 | # prefer this package by prepending to search paths and library flags 351 | # 352 | # convert the ldflags into a list of directories and append to $LIBPATH (instead of just using 353 | # $LDFLAGS) to ensure we get the `-Wl,-rpath` linker flag for re-finding shared libraries. 354 | $INCFLAGS = [incflags, $INCFLAGS].join(" ").strip 355 | libpaths = ldflags.shellsplit.map { |f| f.sub(/\A-L/, "") } 356 | $LIBPATH = libpaths | $LIBPATH 357 | $libs = [libflags, $libs].join(" ").strip 358 | 359 | # prefer this package's compiler flags by appending them to the command line 360 | $CFLAGS = [$CFLAGS, cflags].join(" ").strip 361 | $CXXFLAGS = [$CXXFLAGS, cflags].join(" ").strip 362 | end 363 | 364 | def path 365 | File.expand_path(port_path) 366 | end 367 | 368 | def include_path 369 | File.join(path, "include") 370 | end 371 | 372 | def lib_path 373 | File.join(path, "lib") 374 | end 375 | 376 | def cc_cmd 377 | (ENV["CC"] || @cc_command || RbConfig::CONFIG["CC"] || "gcc").dup 378 | end 379 | alias :gcc_cmd :cc_cmd 380 | 381 | def cxx_cmd 382 | (ENV["CXX"] || @cxx_command || RbConfig::CONFIG["CXX"] || "g++").dup 383 | end 384 | 385 | def make_cmd 386 | (ENV["MAKE"] || @make_command || ENV["make"] || "make").dup 387 | end 388 | 389 | private 390 | 391 | def native_path(path) 392 | MiniPortile.native_path(path) 393 | end 394 | 395 | def posix_path(path) 396 | MiniPortile.posix_path(path) 397 | end 398 | 399 | def tmp_path 400 | "tmp/#{@host}/ports/#{@name}/#{@version}" 401 | end 402 | 403 | def port_path 404 | "#{@target}/#{@host}/#{@name}/#{@version}" 405 | end 406 | 407 | def archives_path 408 | "#{@target}/archives" 409 | end 410 | 411 | def work_path 412 | Dir.glob("#{tmp_path}/*").find { |d| File.directory?(d) } 413 | end 414 | 415 | def configure_defaults 416 | [ 417 | "--host=#{@host}", # build for specific target (host) 418 | "--enable-static", # build static library 419 | "--disable-shared" # disable generation of shared object 420 | ] 421 | end 422 | 423 | def configure_prefix 424 | "--prefix=#{File.expand_path(port_path)}" 425 | end 426 | 427 | def computed_options 428 | [ 429 | configure_options, # customized or default options 430 | configure_prefix, # installation target 431 | ].flatten 432 | end 433 | 434 | def files_hashs 435 | @files.map do |file| 436 | hash = case file 437 | when String 438 | { :url => file } 439 | when Hash 440 | file.dup 441 | else 442 | raise ArgumentError, "files must be an Array of Stings or Hashs" 443 | end 444 | 445 | url = hash.fetch(:url){ raise ArgumentError, "no url given" } 446 | filename = File.basename(url) 447 | hash[:local_path] = File.join(archives_path, filename) 448 | hash 449 | end 450 | end 451 | 452 | KEYRING_NAME = "mini_portile_keyring.gpg" 453 | 454 | def verify_file(file) 455 | if file.has_key?(:gpg) 456 | gpg = file[:gpg] 457 | 458 | signature_url = gpg[:signature_url] || "#{file[:url]}.asc" 459 | signature_file = file[:local_path] + ".asc" 460 | # download the signature file 461 | download_file(signature_url, signature_file) 462 | 463 | gpg_exe = which('gpg2') || which('gpg') || raise("Neither GPG nor GPG2 is installed") 464 | 465 | # import the key into our own keyring 466 | gpg_error = nil 467 | gpg_status = Open3.popen3(gpg_exe, "--status-fd", "1", "--no-default-keyring", "--keyring", KEYRING_NAME, "--import") do |gpg_in, gpg_out, gpg_err, _thread| 468 | gpg_in.write gpg[:key] 469 | gpg_in.close 470 | gpg_error = gpg_err.read 471 | gpg_out.read 472 | end 473 | key_ids = gpg_status.scan(/\[GNUPG:\] IMPORT_OK \d+ (?[0-9a-f]+)/i).map(&:first) 474 | raise "invalid gpg key provided:\n#{gpg_error}" if key_ids.empty? 475 | 476 | begin 477 | # verify the signature against our keyring 478 | gpg_status, gpg_error, _status = Open3.capture3(gpg_exe, "--status-fd", "1", "--no-default-keyring", "--keyring", KEYRING_NAME, "--verify", signature_file, file[:local_path]) 479 | 480 | raise "signature mismatch:\n#{gpg_error}" unless gpg_status.match(/^\[GNUPG:\] VALIDSIG/) 481 | ensure 482 | # remove the key from our keyring 483 | key_ids.each do |key_id| 484 | IO.popen([gpg_exe, "--batch", "--yes", "--no-default-keyring", "--keyring", KEYRING_NAME, "--delete-keys", key_id], &:read) 485 | raise "unable to delete the imported key" unless $?.exitstatus==0 486 | end 487 | end 488 | 489 | 490 | else 491 | digest = case 492 | when exp=file[:sha256] then Digest::SHA256 493 | when exp=file[:sha1] then Digest::SHA1 494 | when exp=file[:md5] then Digest::MD5 495 | end 496 | if digest 497 | is = digest.file(file[:local_path]).hexdigest 498 | unless is == exp.downcase 499 | raise "Downloaded file '#{file[:local_path]}' has wrong hash: expected: #{exp} is: #{is}" 500 | end 501 | end 502 | end 503 | end 504 | 505 | def log_file(action) 506 | @log_files[action] ||= 507 | File.expand_path("#{action}.log", tmp_path).tap { |file| 508 | File.unlink(file) if File.exist?(file) 509 | } 510 | end 511 | 512 | # From: http://stackoverflow.com/a/5471032/7672 513 | # Thanks, Mislav! 514 | # 515 | # Cross-platform way of finding an executable in the $PATH. 516 | # 517 | # which('ruby') #=> /usr/bin/ruby 518 | def which(cmd) 519 | exts = [''] + (ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : []) 520 | ENV['PATH'].split(File::PATH_SEPARATOR).each do |path| 521 | exts.each { |ext| 522 | exe = File.join(path, "#{cmd}#{ext}") 523 | return exe if File.executable? exe 524 | } 525 | end 526 | return nil 527 | end 528 | 529 | def detect_host 530 | return @detect_host if defined?(@detect_host) 531 | 532 | begin 533 | ENV["LC_ALL"], old_lc_all = "C", ENV["LC_ALL"] 534 | 535 | output = `#{gcc_cmd} -v 2>&1` 536 | if m = output.match(/^Target\: (.*)$/) 537 | @detect_host = m[1] 538 | else 539 | @detect_host = nil 540 | end 541 | 542 | @detect_host 543 | ensure 544 | ENV["LC_ALL"] = old_lc_all 545 | end 546 | end 547 | 548 | TAR_EXECUTABLES = %w[gtar bsdtar tar basic-bsdtar] 549 | def tar_exe 550 | @@tar_exe ||= begin 551 | TAR_EXECUTABLES.find { |c| 552 | which(c) 553 | } or raise("tar not found - please make sure that one of the following commands is in the PATH: #{TAR_EXECUTABLES.join(", ")}") 554 | end 555 | end 556 | 557 | def xzcat_exe 558 | @@xzcat_exe ||= which("xzcat") ? "xzcat" : raise("xzcat not found") 559 | end 560 | 561 | def tar_command(file, target) 562 | case File.extname(file) 563 | when '.gz', '.tgz' 564 | [tar_exe, 'xzf', file, '-C', target] 565 | when '.bz2', '.tbz2' 566 | [tar_exe, 'xjf', file, '-C', target] 567 | when '.xz' 568 | # NOTE: OpenBSD's tar command does not support the -J option 569 | "#{xzcat_exe.shellescape} #{file.shellescape} | #{tar_exe.shellescape} xf - -C #{target.shellescape}" 570 | else 571 | [tar_exe, 'xf', file, '-C', target] 572 | end 573 | end 574 | 575 | def extract_file(file, target) 576 | filename = File.basename(file) 577 | FileUtils.mkdir_p target 578 | 579 | message "Extracting #{filename} into #{target}... " 580 | execute('extract', tar_command(file, target) , {:cd => Dir.pwd, :initial_message => false}) 581 | end 582 | 583 | # command could be an array of args, or one string containing a command passed to the shell. See 584 | # Process.spawn for more information. 585 | def execute(action, command, command_opts={}) 586 | opt_message = command_opts.fetch(:initial_message, true) 587 | opt_debug = command_opts.fetch(:debug, false) 588 | opt_cd = command_opts.fetch(:cd) { work_path } 589 | opt_env = command_opts.fetch(:env) { Hash.new } 590 | opt_altlog = command_opts.fetch(:altlog, nil) 591 | 592 | log_out = log_file(action) 593 | 594 | Dir.chdir(opt_cd) do 595 | output "DEBUG: env is #{opt_env.inspect}" if opt_debug 596 | output "DEBUG: command is #{command.inspect}" if opt_debug 597 | message "Running '#{action}' for #{@name} #{@version}... " if opt_message 598 | 599 | if Process.respond_to?(:spawn) && ! RbConfig.respond_to?(:java) 600 | options = {[:out, :err]=>[log_out, "a"]} 601 | output "DEBUG: options are #{options.inspect}" if opt_debug 602 | args = [opt_env, command, options].flatten 603 | pid = spawn(*args) 604 | Process.wait(pid) 605 | else 606 | env_args = opt_env.map { |k,v| "#{k}=#{v}".shellescape }.join(" ") 607 | c = if command.kind_of?(Array) 608 | command.map(&:shellescape).join(" ") 609 | else 610 | command 611 | end 612 | redirected = %Q{env #{env_args} #{c} > #{log_out.shellescape} 2>&1} 613 | output "DEBUG: final command is #{redirected.inspect}" if opt_debug 614 | system redirected 615 | end 616 | 617 | if $?.success? 618 | output "OK" 619 | return true 620 | else 621 | output "ERROR. Please review logs to see what happened:\n" 622 | [log_out, opt_altlog].compact.each do |log| 623 | next unless File.exist?(log) 624 | output("----- contents of '#{log}' -----") 625 | output(File.read(log)) 626 | output("----- end of file -----") 627 | end 628 | raise "Failed to complete #{action} task" 629 | end 630 | end 631 | end 632 | 633 | def newer?(target, checkpoint) 634 | if (target && File.exist?(target)) && (checkpoint && File.exist?(checkpoint)) 635 | File.mtime(target) > File.mtime(checkpoint) 636 | else 637 | false 638 | end 639 | end 640 | 641 | # print out a message with the logger 642 | def message(text) 643 | @logger.print text 644 | @logger.flush 645 | end 646 | 647 | # print out a message using the logger but return to a new line 648 | def output(text = "") 649 | @logger.puts text 650 | @logger.flush 651 | end 652 | 653 | # Slighly modified from RubyInstaller uri_ext, Rubinius configure 654 | # and adaptations of Wayne's RailsInstaller 655 | def download_file(url, full_path, count = 3) 656 | return if File.exist?(full_path) 657 | uri = URI.parse(url) 658 | 659 | case uri.scheme.downcase 660 | when /ftp/ 661 | download_file_ftp(uri, full_path) 662 | when /http|https/ 663 | download_file_http(url, full_path, count) 664 | when /file/ 665 | download_file_file(uri, full_path) 666 | else 667 | raise ArgumentError.new("Unsupported protocol for #{url}") 668 | end 669 | rescue Exception => e 670 | File.unlink full_path if File.exist?(full_path) 671 | raise e 672 | end 673 | 674 | def download_file_http(url, full_path, count = 3) 675 | filename = File.basename(full_path) 676 | with_tempfile(filename, full_path) do |temp_file| 677 | total = 0 678 | params = { 679 | "Accept-Encoding" => 'identity', 680 | :content_length_proc => lambda{|length| total = length }, 681 | :progress_proc => lambda{|bytes| 682 | if total 683 | new_progress = (bytes * 100) / total 684 | message "\rDownloading %s (%3d%%) " % [filename, new_progress] 685 | else 686 | # Content-Length is unavailable because Transfer-Encoding is chunked 687 | message "\rDownloading %s " % [filename] 688 | end 689 | }, 690 | :open_timeout => @open_timeout, 691 | :read_timeout => @read_timeout, 692 | } 693 | proxy_uri = URI.parse(url).scheme.downcase == 'https' ? 694 | ENV["https_proxy"] : 695 | ENV["http_proxy"] 696 | if proxy_uri 697 | _, userinfo, _p_host, _p_port = URI.split(proxy_uri) 698 | if userinfo 699 | proxy_user, proxy_pass = userinfo.split(/:/).map{|s| CGI.unescape(s) } 700 | params[:proxy_http_basic_authentication] = 701 | [proxy_uri, proxy_user, proxy_pass] 702 | end 703 | end 704 | 705 | begin 706 | OpenURI.open_uri(url, 'rb', params) do |io| 707 | temp_file << io.read 708 | end 709 | output 710 | rescue OpenURI::HTTPRedirect => redirect 711 | raise "Too many redirections for the original URL, halting." if count <= 0 712 | count = count - 1 713 | return download_file(redirect.url, full_path, count-1) 714 | rescue => e 715 | count = count - 1 716 | @logger.puts "#{count} retrie(s) left for #{filename} (#{e.message})" 717 | if count > 0 718 | sleep 1 719 | return download_file_http(url, full_path, count) 720 | end 721 | 722 | output e.message 723 | return false 724 | end 725 | end 726 | end 727 | 728 | def download_file_file(uri, full_path) 729 | FileUtils.mkdir_p File.dirname(full_path) 730 | FileUtils.cp uri.path, full_path 731 | end 732 | 733 | def download_file_ftp(uri, full_path) 734 | require "net/ftp" 735 | filename = File.basename(uri.path) 736 | with_tempfile(filename, full_path) do |temp_file| 737 | total = 0 738 | params = { 739 | :content_length_proc => lambda{|length| total = length }, 740 | :progress_proc => lambda{|bytes| 741 | new_progress = (bytes * 100) / total 742 | message "\rDownloading %s (%3d%%) " % [filename, new_progress] 743 | }, 744 | :open_timeout => @open_timeout, 745 | :read_timeout => @read_timeout, 746 | } 747 | if ENV["ftp_proxy"] 748 | _, userinfo, _p_host, _p_port = URI.split(ENV['ftp_proxy']) 749 | if userinfo 750 | proxy_user, proxy_pass = userinfo.split(/:/).map{|s| CGI.unescape(s) } 751 | params[:proxy_http_basic_authentication] = 752 | [ENV['ftp_proxy'], proxy_user, proxy_pass] 753 | end 754 | end 755 | OpenURI.open_uri(uri, 'rb', params) do |io| 756 | temp_file << io.read 757 | end 758 | output 759 | end 760 | rescue LoadError 761 | raise LoadError, "Ruby #{RUBY_VERSION} does not provide the net-ftp gem, please add it as a dependency if you need to use FTP" 762 | rescue Net::FTPError 763 | return false 764 | end 765 | 766 | def with_tempfile(filename, full_path) 767 | temp_file = Tempfile.new("download-#{filename}") 768 | temp_file.binmode 769 | yield temp_file 770 | temp_file.close 771 | File.unlink full_path if File.exist?(full_path) 772 | FileUtils.mkdir_p File.dirname(full_path) 773 | FileUtils.mv temp_file.path, full_path, :force => true 774 | end 775 | 776 | # 777 | # this minimal version of pkg_config is based on ruby 29dc9378 (2023-01-09) 778 | # 779 | # specifically with the fix from b90e56e6 to support multiple pkg-config options, and removing 780 | # code paths that aren't helpful for mini-portile's use case of parsing pc files. 781 | # 782 | def minimal_pkg_config(pkg, *pcoptions) 783 | if pcoptions.empty? 784 | raise ArgumentError, "no pkg-config options are given" 785 | end 786 | 787 | if ($PKGCONFIG ||= 788 | (pkgconfig = MakeMakefile.with_config("pkg-config") {MakeMakefile.config_string("PKG_CONFIG") || "pkg-config"}) && 789 | MakeMakefile.find_executable0(pkgconfig) && pkgconfig) 790 | pkgconfig = $PKGCONFIG 791 | else 792 | raise RuntimeError, "pkg-config is not found" 793 | end 794 | 795 | pcoptions = Array(pcoptions).map { |o| "--#{o}" } 796 | response = IO.popen([pkgconfig, *pcoptions, pkg], err:[:child, :out], &:read) 797 | raise RuntimeError, response unless $?.success? 798 | response.strip 799 | end 800 | end 801 | --------------------------------------------------------------------------------