├── .yardopts ├── .gitignore ├── test ├── fixtures │ ├── config │ │ └── Cargo.toml │ └── github │ │ └── releases.atom ├── test_helper.rb └── lib │ └── thermite │ ├── util_test.rb │ ├── custom_binary_test.rb │ ├── semver_test.rb │ ├── cargo_test.rb │ ├── package_test.rb │ ├── github_release_binary_test.rb │ └── config_test.rb ├── Gemfile ├── .rubocop.yml ├── .appveyor.yml ├── thermite.gemspec ├── LICENSE ├── Rakefile ├── ci └── after_success.py ├── lib └── thermite │ ├── semver.rb │ ├── fiddle.rb │ ├── util.rb │ ├── custom_binary.rb │ ├── package.rb │ ├── cargo.rb │ ├── github_release_binary.rb │ ├── tasks.rb │ └── config.rb ├── .travis.yml ├── CONTRIBUTING.md ├── NEWS.md └── README.md /.yardopts: -------------------------------------------------------------------------------- 1 | --charset utf-8 2 | --markup markdown 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .yardoc 3 | Gemfile.lock 4 | coverage 5 | doc 6 | -------------------------------------------------------------------------------- /test/fixtures/config/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fixture" 3 | 4 | [package.metadata.thermite] 5 | github_releases = true 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | group :test do 6 | gem 'simplecov', require: nil 7 | end 8 | 9 | gemspec 10 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | Exclude: 3 | - 'vendor/**/*' 4 | TargetRubyVersion: 2.1 5 | 6 | Layout/EmptyLineAfterMagicComment: 7 | Enabled: false 8 | 9 | # When support for Ruby < 2.3 is dropped, re-enable 10 | Layout/IndentHeredoc: 11 | Enabled: false 12 | 13 | Lint/EndAlignment: 14 | Enabled: true 15 | EnforcedStyleAlignWith: variable 16 | 17 | Metrics/AbcSize: 18 | Max: 20 19 | 20 | Metrics/ClassLength: 21 | Exclude: 22 | - 'test/**/*' 23 | Max: 150 24 | 25 | Metrics/LineLength: 26 | Max: 100 27 | AllowURI: true 28 | URISchemes: 29 | - http 30 | - https 31 | 32 | Metrics/MethodLength: 33 | Max: 20 34 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | platform: 2 | - x86 3 | - x64 4 | environment: 5 | matrix: 6 | - RUBY_VERSION: 21 7 | - RUBY_VERSION: 22 8 | - RUBY_VERSION: 23 9 | - RUBY_VERSION: 24 10 | - RUBY_VERSION: 25 11 | cache: 12 | - vendor\bundle 13 | install: 14 | - ps: | 15 | if ($env:platform -eq 'x86') { 16 | $env:WIN_RUBY_BIN = "C:\Ruby${env:RUBY_VERSION}\bin"; 17 | } else { 18 | $env:WIN_RUBY_BIN = "C:\Ruby${env:RUBY_VERSION}-x64\bin"; 19 | } 20 | $env:PATH = "${env:WIN_RUBY_BIN};${env:PATH}"; 21 | - ruby --version 22 | - gem --version 23 | - rake --version 24 | - bundle --version 25 | - bundle config --local path vendor/bundle 26 | - bundle install 27 | build: false 28 | test_script: 29 | - bundle exec rake test 30 | -------------------------------------------------------------------------------- /thermite.gemspec: -------------------------------------------------------------------------------- 1 | require 'English' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = 'thermite' 5 | s.version = '0.13.0' 6 | s.summary = 'Rake helpers for Rust+Ruby' 7 | s.description = 'A Rake-based helper for building and distributing Rust-based Ruby extensions' 8 | 9 | s.authors = ['Mark Lee'] 10 | s.email = 'malept@users.noreply.github.com' 11 | s.homepage = 'https://github.com/malept/thermite' 12 | s.license = 'MIT' 13 | 14 | s.files = `git ls-files`.split($OUTPUT_RECORD_SEPARATOR) 15 | s.require_paths = %w[lib] 16 | 17 | s.required_ruby_version = '>= 2.1.0' 18 | 19 | s.add_runtime_dependency 'minitar', '~> 0.6' 20 | s.add_runtime_dependency 'rake', '>= 10' 21 | s.add_runtime_dependency 'tomlrb', '~> 1.2' 22 | s.add_development_dependency 'minitest', '~> 5.9' 23 | s.add_development_dependency 'mocha', '~> 1.1' 24 | s.add_development_dependency 'rubocop', '~> 0.49' 25 | s.add_development_dependency 'yard', '~> 0.9' 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Mark Lee and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /test/fixtures/github/releases.atom: -------------------------------------------------------------------------------- 1 | 2 | 3 | tag:github.com,2008:https://github.com/ghost/project/releases 4 | 5 | 6 | Release notes from project 7 | 2016-07-10T01:57:28Z 8 | 9 | tag:github.com,2008:Repository/12345678/v0.1.12_rust 10 | 2016-07-10T01:59:24Z 11 | 12 | v0.1.12_rust 13 | No content. 14 | 15 | ghost 16 | 17 | 18 | 19 | 20 | tag:github.com,2008:Repository/12345678/v0.1.11_rust 21 | 2016-07-09T22:47:09Z 22 | 23 | v0.1.11_rust 24 | No content. 25 | 26 | ghost 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'bundler/gem_tasks' 21 | require 'rake/testtask' 22 | require 'rubocop/rake_task' 23 | require 'yard' 24 | 25 | Rake::TestTask.new do |t| 26 | t.libs << 'test' 27 | t.test_files = FileList['test/**/*_test.rb'] 28 | end 29 | YARD::Rake::YardocTask.new 30 | RuboCop::RakeTask.new 31 | 32 | task default: %w[rubocop yard test] 33 | -------------------------------------------------------------------------------- /ci/after_success.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import print_function 4 | 5 | import httplib 6 | import json 7 | import os 8 | 9 | JSON = 'application/json' 10 | 11 | def trigger_appveyor_build(): 12 | payload = { 13 | 'accountName': 'malept', 14 | 'projectSlug': 'rusty-blank', 15 | 'branch': 'master', 16 | } 17 | 18 | headers = { 19 | 'Accept': JSON, 20 | 'Authorization': 'Bearer {}'.format(os.environ['APPVEYOR_TOKEN']), 21 | 'Content-Type': JSON, 22 | } 23 | 24 | http = httplib.HTTPSConnection('ci.appveyor.com') 25 | http.request('POST', '/api/builds', json.dumps(payload), headers) 26 | print(http.getresponse().read()) 27 | 28 | def trigger_travis_build(): 29 | msg = "Triggered by {}@{} ({})".format(os.environ['TRAVIS_REPO_SLUG'], 30 | os.environ['TRAVIS_BRANCH'], 31 | os.environ['TRAVIS_COMMIT']) 32 | payload = { 33 | "request": { 34 | "branch": "master", 35 | "message": msg, 36 | } 37 | } 38 | 39 | headers = { 40 | 'Accept': JSON, 41 | 'Authorization': 'token {}'.format(os.environ['TRAVIS_TOKEN']), 42 | 'Content-Type': JSON, 43 | 'Travis-Api-Version': '3', 44 | } 45 | 46 | http = httplib.HTTPSConnection('api.travis-ci.org') 47 | http.request('POST', '/repo/malept%2Frusty_blank/requests', json.dumps(payload), headers) 48 | print(http.getresponse().read()) 49 | 50 | if __name__ == '__main__': 51 | if (os.environ['TRAVIS_OS_NAME'] == 'linux' and 52 | os.environ['TRAVIS_RUBY_VERSION'].startswith('2.4.')): 53 | trigger_appveyor_build() 54 | trigger_travis_build() 55 | -------------------------------------------------------------------------------- /lib/thermite/semver.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2018 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | module Thermite 21 | # 22 | # [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (2.0.0) regular expression. 23 | # 24 | module SemVer 25 | # 26 | # Valid version number part (major/minor/patch). 27 | # 28 | NUMERIC = '(?:0|[1-9]\d*)'.freeze 29 | 30 | # 31 | # Valid identifier for pre-release versions or build metadata. 32 | # 33 | IDENTIFIER = '[-0-9A-Za-z][-0-9A-Za-z.]*'.freeze 34 | 35 | # 36 | # Version pre-release section, including the hyphen. 37 | # 38 | PRERELEASE = "-#{IDENTIFIER}".freeze 39 | 40 | # 41 | # Version build metadata section, including the plus sign. 42 | # 43 | BUILD_METADATA = "\\+#{IDENTIFIER}".freeze 44 | 45 | # 46 | # Semantic version-compliant regular expression. 47 | # 48 | VERSION = "v?#{NUMERIC}\.#{NUMERIC}\.#{NUMERIC}(?:#{PRERELEASE})?(?:#{BUILD_METADATA})?".freeze 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016, 2017 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'simplecov' 21 | SimpleCov.start do 22 | load_profile 'test_frameworks' 23 | add_filter 'lib/thermite/fiddle.rb' 24 | add_filter 'lib/thermite/tasks.rb' 25 | track_files 'lib/**/*.rb' 26 | end 27 | 28 | ENV['THERMITE_TEST'] = '1' 29 | 30 | require 'minitest/autorun' 31 | require 'mocha/mini_test' 32 | require 'thermite/config' 33 | 34 | module Minitest 35 | class Test 36 | def fixtures_path(*components) 37 | File.join(File.dirname(__FILE__), 'fixtures', *components) 38 | end 39 | end 40 | end 41 | 42 | module Thermite 43 | module ModuleTester 44 | def mock_module(options = {}) 45 | @mock_module ||= described_class.new(options) 46 | end 47 | end 48 | 49 | module TestHelper 50 | attr_reader :config, :options 51 | def initialize(options = {}) 52 | @options = options 53 | @config = Thermite::Config.new(@options) 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /test/lib/thermite/util_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016, 2017 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'tempfile' 21 | require 'test_helper' 22 | require 'thermite/util' 23 | 24 | module Thermite 25 | class UtilTest < Minitest::Test 26 | include Thermite::ModuleTester 27 | 28 | class Tester 29 | include Thermite::TestHelper 30 | include Thermite::Util 31 | end 32 | 33 | def test_debug 34 | stub_debug_filename(nil) 35 | mock_module.debug('will not exist') 36 | debug_file = Tempfile.new('thermite_test') 37 | stub_debug_filename(debug_file.path) 38 | mock_module.debug('some message') 39 | mock_module.instance_variable_get('@debug').flush 40 | debug_file.rewind 41 | assert_equal "some message\n", debug_file.read 42 | ensure 43 | debug_file.close 44 | debug_file.unlink 45 | end 46 | 47 | def stub_debug_filename(value) 48 | mock_module.config.stubs(:debug_filename).returns(value) 49 | end 50 | 51 | def described_class 52 | Tester 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/thermite/fiddle.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'fiddle' 21 | require 'thermite/config' 22 | 23 | module Thermite 24 | # 25 | # Fiddle helper functions. 26 | # 27 | module Fiddle 28 | # 29 | # Loads a native extension using {Thermite::Config} and the builtin `Fiddle` extension. 30 | # 31 | # @param init_function_name [String] the name of the native function that initializes 32 | # the extension 33 | # @param config_options [Hash] {Thermite::Tasks#options options} passed to {Thermite::Config}. 34 | # Options likely needed to be set: 35 | # `cargo_project_path`, `ruby_project_path` 36 | # 37 | def self.load_module(init_function_name, config_options) 38 | config = Thermite::Config.new(config_options) 39 | library = ::Fiddle.dlopen(config.ruby_extension_path) 40 | func = ::Fiddle::Function.new(library[init_function_name], 41 | [], ::Fiddle::TYPE_VOIDP) 42 | func.call 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/thermite/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'net/http' 21 | 22 | module Thermite 23 | # 24 | # Utility methods 25 | # 26 | module Util 27 | # 28 | # Logs a debug message to the specified `config.debug_filename`, if set. 29 | # 30 | def debug(msg) 31 | # Should probably replace with a Logger 32 | return unless config.debug_filename 33 | 34 | @debug ||= File.open(config.debug_filename, 'w') 35 | @debug.write("#{msg}\n") 36 | @debug.flush 37 | end 38 | 39 | # 40 | # Wrapper for a Net::HTTP GET request that handles redirects. 41 | # 42 | # :nocov: 43 | def http_get(uri, retries_left = 10) 44 | raise RedirectError, 'Too many redirects' if retries_left.zero? 45 | 46 | case (response = Net::HTTP.get_response(URI(uri))) 47 | when Net::HTTPClientError 48 | nil 49 | when Net::HTTPServerError 50 | raise Net::HTTPServerException.new(response.message, response) 51 | when Net::HTTPFound, Net::HTTPPermanentRedirect 52 | http_get(response['location'], retries_left - 1) 53 | else 54 | StringIO.new(response.body) 55 | end 56 | end 57 | # :nocov: 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /test/lib/thermite/custom_binary_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016, 2017 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'tmpdir' 21 | require 'test_helper' 22 | require 'thermite/custom_binary' 23 | require 'thermite/util' 24 | 25 | module Thermite 26 | class CustomBinaryTest < Minitest::Test 27 | include Thermite::ModuleTester 28 | 29 | class Tester 30 | include Thermite::CustomBinary 31 | include Thermite::TestHelper 32 | include Thermite::Util 33 | end 34 | 35 | def test_no_downloading_when_binary_uri_is_falsey 36 | mock_module(binary_uri_format: false) 37 | mock_module.expects(:http_get).never 38 | 39 | assert !mock_module.download_binary_from_custom_uri 40 | end 41 | 42 | def test_download_binary_from_custom_uri 43 | mock_module(binary_uri_format: 'http://example.com/download/%s/%s') 44 | mock_module.config.stubs(:toml).returns(package: { version: '4.5.6' }) 45 | Net::HTTP.stubs(:get_response).returns('location' => 'redirect') 46 | mock_module.stubs(:http_get).returns('tarball') 47 | mock_module.expects(:unpack_tarball).once 48 | mock_module.expects(:prepare_downloaded_library).once 49 | 50 | assert mock_module.download_binary_from_custom_uri 51 | end 52 | 53 | private 54 | 55 | def described_class 56 | Tester 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /lib/thermite/custom_binary.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'net/http' 21 | require 'uri' 22 | 23 | module Thermite 24 | # 25 | # Custom binary URI helpers. 26 | # 27 | module CustomBinary 28 | # 29 | # Downloads a Rust binary using a custom URI format, given the target OS and architecture. 30 | # 31 | # Requires the `binary_uri_format` option to be set. The version of the binary is determined by 32 | # the crate version given in `Cargo.toml`. 33 | # 34 | # Returns whether a binary was found and unpacked. 35 | # 36 | def download_binary_from_custom_uri 37 | return false unless config.binary_uri_format 38 | 39 | version = config.crate_version 40 | uri ||= format( 41 | config.binary_uri_format, 42 | filename: config.tarball_filename(version), 43 | version: version 44 | ) 45 | 46 | return false unless (tgz = download_versioned_binary(uri, version)) 47 | 48 | debug "Unpacking binary from Cargo version: #{File.basename(uri)}" 49 | unpack_tarball(tgz) 50 | prepare_downloaded_library 51 | true 52 | end 53 | 54 | private 55 | 56 | def download_versioned_binary(uri, version) 57 | unless ENV.key?('THERMITE_TEST') 58 | # :nocov: 59 | puts "Downloading compiled version (#{version})" 60 | # :nocov: 61 | end 62 | 63 | http_get(uri) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | os: 3 | - linux 4 | - osx 5 | dist: trusty 6 | sudo: false 7 | osx_image: xcode8.3 8 | rvm: 9 | - '2.1.10' 10 | - '2.2.5' 11 | - '2.3.3' 12 | - '2.4.0' 13 | - '2.5.0' 14 | cache: bundler 15 | before_script: 16 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-$(uname -s | tr '[:upper:]' '[:lower:]')-amd64 > ./cc-test-reporter && chmod +x ./cc-test-reporter 17 | - ./cc-test-reporter before-build 18 | after_success: 19 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 20 | - ci/after_success.py 21 | env: 22 | global: 23 | # TRAVIS_TOKEN 24 | - secure: Ia4faKtCVBGN6z5jFrnBsCJX/Hg3Hs+pGll+lczDE+UFM0Io7LT/upQdre5JUftM0IiynwCw/XS4dz6h/ZZWS28pSFm12NvHHmO22NAaX/xQjKvBhWz8OblnNjybPNSdt0bBa4XnssW7SiPSH5moO3PEnRfbaVrAppHWw+VL0449RnFwu33J9663Gvsf48cE9f3ZeX/NUowqe9Gr3Y1edKsE5btK2vgW+VrwCCbf3PIF60SveClcivB823IrjHsBN76n8C6/Hx6zngZXYN8GfBN5c9JGMP9UP0fSE81jqmDo2poX1MdVch3kf3ylllOrOP/Z5hzZ3aYv+sOTDR68si+sLx1mlrY8ImX6sK/uDbGx0CYOGvmZO9OqREOBEBLg98KQAOsdxIXe2CTfUSQbj7pVppba72tTVqPrM8mroEIl+Dk6rPB5x0makMj1xQ8UyyyCgsiEcfkRrUjefPBIw5aczxXGoqg+SEnRdqQu94z5V0UeGtbZ2BqBuP6E/XINr90nm9fmpI2nDgMde8MCjPfHz8nbXM/l/1nkQ4l7d0Dob+vUZtTwr4CBW1oWp4w46Iv0mBSr5js0b7fhh95rMN1/CYZaaNsraXnqACsP70WRMBuHSDZ6mk4DQG0D7BjIckt/GplUoo3I7Z5tPjBgejk+VhoLdQGAu9CGutSGkTk= 25 | # APPVEYOR_TOKEN 26 | - secure: Qvw49LusnruOfYYL0SpcV3PhK1Fm15y9wtZK+YEscvkNw6LZ+xziD+1+f2nuR+zQkHmG/izqpA+NMoGqJvB8JsI+8N6S7YORJJ5e/TVjVq7voQXohNRMldUPn38IIVfBSR8Wzmqw7t+g7Eg6WWnbiRHO7RGlHuGwLeG4Cgl9gPNYt9gZpuditAIk/jmuOaTMbp4gRB02CERIMInrH8+52fcAWJCTQzLecD116c33xa3LUCC1hS49Wu6ku0O8OsuOAcpWTUuxAhScdCSYf8iy7WP83mNkbuIPN8OsOz6wS0pO1cJ5opdIvDWNPnYinhVOXbyP1FEXj5GtlDzt+5eIslp3BBAf90czc/TpYQS+9HBXZTD81l0xslneC/eAc0/bPlF98qLdhPByAZpjJFHM1tiijFVyIzqHLNXL4c2NUcOZONquCwKkroyL/Ze2w7pwttP8hC5TUEoRexMgE3ULAPKITKSmkrq2UxRdRB4Ln6uXaK5zfxLfJOgwqmcD7W/34pfQ/X+f/wymEYG2FwORewGVH//UtYklN6J8ivQDmJ1eyECSJmfHWgKWX729pfAuKLkc+iVOvZZ736plcN2GbmznyplevhjScaBpZHzjF1Iz/A7JEJHYoQIdecn67iIY2p5ULKklBJP8vqrI24YqtNl4Ly/8/kRn0DjyPVauQ1o= 27 | # CC_TEST_REPORTER_ID 28 | - secure: BR1CpaI3GS38jpi6H6Ymcc5F5VDzdhHfRE8CmyK4LzWPAUe1/HRu8VNhzNXRSQlo2UhgjEIAEkNF7Y2IFU58Vkxpn4yet5NZrnrjn/NhLSQf6JPNLBEAGq3ioDa0naFqVeH62zi/qmJ+4Ckz7BccaYABZWAXps8oD5pBMWu3mo6uJRctKdQIBvWQOCKaRCKwaRXRG0XjbpAZmgUsgsM4tM70sQhevL123gU1bw65WJ3vxilg8oLnwdHomfI454qLsXZjbzZSQ541nkTi+PwlDMhYPxw5G4t80FvgUTbDfhp6uZi6J/pEMZeTVYKqwP3ohavUjH/7+2/0x4PNwZb4L2ufZ5GzwoYZMC+/v55d/pQN0V0F1iK99g+mBhw6aUxEz6tFzmwzCSTU/0XN2KkoWusl5gnV/QV4D3g0oSPbcebA8CTQ9qf0he1MP5gZ3bV1PAV3VHlA7nElSlJ/tSBFFu9rYzooLVOzZ13nBRdx7sOLg+u4TiUUVMnVY4jkXZoOIq8odoJsjFcJt5kdNxBJHNfkj/mUEM26ZXnFgNeTxWsZBVss+CHIo/4WGXnDkVYl/iyLXDIsnt4sOZ/ulrM7VubHFcNJZxVb1tgXGoeEq0ZXzRUsT5b6z1M5l5wI5DeHB8MlMqq8rc7ocWYaxYZy29wqb7BfIaDxNU1APkCmQ4s= 29 | -------------------------------------------------------------------------------- /test/lib/thermite/semver_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2018 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'thermite/semver' 21 | 22 | module Thermite 23 | class SemVerTest < Minitest::Test 24 | def test_valid_semantic_versions 25 | # From https://github.com/malept/thermite/pull/45 26 | assert semantic_version_regexp.match('0.3.2') 27 | assert semantic_version_regexp.match('v0.3.2') 28 | assert semantic_version_regexp.match('0.3.0-rc1') 29 | assert semantic_version_regexp.match('0.3.0-rc.1') 30 | end 31 | 32 | def test_invalid_semantic_versions 33 | # From https://github.com/malept/thermite/pull/45 34 | assert_nil semantic_version_regexp.match('v0.3.2.beta13') 35 | assert_nil semantic_version_regexp.match('0.5.3.alpha1') 36 | assert_nil semantic_version_regexp.match('0.5.3.1') 37 | end 38 | 39 | def test_valid_semantic_prerelease_versions 40 | # From https://semver.org/spec/v2.0.0.html#spec-item-9 41 | assert semantic_version_regexp.match('1.0.0-alpha') 42 | assert semantic_version_regexp.match('1.0.0-alpha.1') 43 | assert semantic_version_regexp.match('1.0.0-0.3.7') 44 | assert semantic_version_regexp.match('1.0.0-x.7.z.92') 45 | end 46 | 47 | def test_valid_semantic_build_metadata_versions 48 | # From https://semver.org/spec/v2.0.0.html#spec-item-10 49 | assert semantic_version_regexp.match('1.0.0-alpha+001') 50 | assert semantic_version_regexp.match('1.0.0+20130313144700') 51 | assert semantic_version_regexp.match('1.0.0-beta+exp.sha.5114f85') 52 | end 53 | 54 | def semantic_version_regexp 55 | @semantic_version_regexp ||= /^#{Thermite::SemVer::VERSION}$/ 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Thermite 2 | 3 | Thermite is a part of the Rust ecosystem. As such, all contributions to this project follow the 4 | [Rust language's code of conduct](https://www.rust-lang.org/conduct.html) where appropriate. 5 | 6 | This project is hosted at [GitHub](https://github.com/malept/thermite). Both pull requests and 7 | issues of many different kinds are accepted. 8 | 9 | ## Filing Issues 10 | 11 | Issues include bugs, questions, feedback, and feature requests. Before you file a new issue, please 12 | make sure that your issue has not already been filed by someone else. 13 | 14 | ### Filing Bugs 15 | 16 | When filing a bug, please include the following information: 17 | 18 | * Operating system and version. If on Linux, please also include the distribution name. 19 | * System architecture. Examples include: x86-64, x86, and ARMv7. 20 | * Ruby and Rake versions that run Thermite. 21 | * The version (and/or git revision) of Thermite. 22 | * If it's an error related to Rust, the version of Rust, Cargo, and how you installed it. 23 | * A detailed list of steps to reproduce the bug. A minimal testcase would be very helpful, 24 | if possible. 25 | * If there are any error messages in the console, copying them in the bug summary will be 26 | very helpful. 27 | 28 | ## Filing Pull Requests 29 | 30 | Here are some things to keep in mind as you file a pull request to fix a bug, add a new feature, 31 | etc.: 32 | 33 | * Travis CI is used to make sure that the project conforms to the coding standards. 34 | * If your PR changes the behavior of an existing feature, or adds a new feature, please add/edit 35 | the RDoc inline documentation (using the Markdown format). You can see what it looks like in the 36 | rendered documentation by running `bundle exec rake rdoc`. 37 | * Please ensure that your changes follow the Rubocop-enforced coding standard, by running 38 | `bundle exec rake rubocop`. 39 | * If you are contributing a nontrivial change, please add an entry to `NEWS.md`. The format is 40 | similar to the one described at [Keep a Changelog](http://keepachangelog.com/). 41 | * Please make sure your commits are rebased onto the latest commit in the master branch, and that 42 | you limit/squash the number of commits created to a "feature"-level. For instance: 43 | 44 | bad: 45 | 46 | ``` 47 | commit 1: add foo 48 | commit 2: run rubocop 49 | commit 3: add test 50 | commit 4: add docs 51 | commit 5: add bar 52 | commit 6: add test + docs 53 | ``` 54 | 55 | good: 56 | 57 | ``` 58 | commit 1: add foo 59 | commit 2: add bar 60 | ``` 61 | 62 | Squashing commits during discussion of the pull request is almost always unnecessary, and makes it 63 | more difficult for both the submitters and reviewers to understand what changed in between comments. 64 | However, rebasing is encouraged when practical, particularly when there's a merge conflict. 65 | 66 | If you are continuing the work of another person's PR and need to rebase/squash, please retain the 67 | attribution of the original author(s) and continue the work in subsequent commits. 68 | -------------------------------------------------------------------------------- /lib/thermite/package.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'archive/tar/minitar' 21 | require 'rubygems/package' 22 | require 'zlib' 23 | 24 | module Thermite 25 | # 26 | # Helpers to package the Rust library into a gzipped tarball. 27 | # 28 | module Package 29 | # 30 | # Builds a tarball of the Rust-compiled shared library. 31 | # 32 | def build_package 33 | filename = config.tarball_filename(config.toml[:package][:version]) 34 | relative_library_path = config.ruby_extension_path.sub("#{config.ruby_toplevel_dir}/", '') 35 | prepare_built_library 36 | Zlib::GzipWriter.open(filename) do |tgz| 37 | Dir.chdir(config.ruby_toplevel_dir) do 38 | Archive::Tar::Minitar.pack(relative_library_path, tgz) 39 | end 40 | end 41 | end 42 | 43 | # 44 | # Unpack a gzipped tarball stream (specified by `tgz`) into the current 45 | # working directory. 46 | # 47 | def unpack_tarball(tgz) 48 | Dir.chdir(config.ruby_toplevel_dir) do 49 | each_compressed_file(tgz) do |path, entry| 50 | debug "Unpacking file: #{path}" 51 | File.open(path, 'wb') do |f| 52 | f.write(entry.read) 53 | end 54 | end 55 | end 56 | end 57 | 58 | # :nocov: 59 | 60 | def prepare_downloaded_library 61 | return unless config.target_os.start_with?('darwin') 62 | 63 | libruby_path = Shellwords.escape(config.libruby_path) 64 | library_path = Shellwords.escape(config.ruby_extension_path) 65 | `install_name_tool -id #{library_path} #{library_path}` 66 | `install_name_tool -change @libruby_path@ #{libruby_path} #{library_path}` 67 | end 68 | 69 | # :nocov: 70 | 71 | private 72 | 73 | def each_compressed_file(tgz) 74 | Zlib::GzipReader.wrap(tgz) do |gz| 75 | Gem::Package::TarReader.new(gz) do |tar| 76 | tar.each do |entry| 77 | path = entry.header.name 78 | next if path.end_with?('/') 79 | yield path, entry 80 | end 81 | end 82 | end 83 | end 84 | 85 | # :nocov: 86 | 87 | def prepare_built_library 88 | return unless config.target_os.start_with?('darwin') 89 | 90 | libruby_path = Shellwords.escape(config.libruby_path) 91 | library_path = Shellwords.escape(config.ruby_extension_path) 92 | `install_name_tool -change #{libruby_path} @libruby_path@ #{library_path}` 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /test/lib/thermite/cargo_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016, 2017 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'test_helper' 21 | require 'thermite/cargo' 22 | 23 | module Thermite 24 | class CargoTest < Minitest::Test 25 | include Thermite::ModuleTester 26 | 27 | class Tester 28 | include Thermite::Cargo 29 | include Thermite::TestHelper 30 | end 31 | 32 | def test_run_cargo_if_exists 33 | mock_module.stubs(:find_executable).returns('/opt/cargo-test/bin/cargo') 34 | mock_module.expects(:sh).with('/opt/cargo-test/bin/cargo', 'foo', 'bar').once 35 | mock_module.run_cargo_if_exists('foo', 'bar') 36 | end 37 | 38 | def test_run_cargo_if_exists_sans_cargo 39 | mock_module.stubs(:find_executable).returns(nil) 40 | mock_module.expects(:sh).never 41 | mock_module.run_cargo_if_exists('foo', 'bar') 42 | end 43 | 44 | def test_run_cargo_debug_rustc 45 | mock_module.config.stubs(:dynamic_linker_flags).returns('') 46 | mock_module.expects(:run_cargo).with('rustc').once 47 | mock_module.run_cargo_rustc('debug') 48 | end 49 | 50 | def test_run_cargo_release_rustc 51 | mock_module.config.stubs(:dynamic_linker_flags).returns('') 52 | mock_module.expects(:run_cargo).with('rustc', '--release').once 53 | mock_module.run_cargo_rustc('release') 54 | end 55 | 56 | def test_run_cargo_rustc_with_workspace_member 57 | mock_module.config.stubs(:dynamic_linker_flags).returns('') 58 | mock_module.config.stubs(:cargo_workspace_member).returns('foo/bar') 59 | mock_module.expects(:run_cargo).with('rustc', '--manifest-path', 'foo/bar/Cargo.toml').once 60 | mock_module.run_cargo_rustc('debug') 61 | end 62 | 63 | def test_run_cargo_rustc_with_dynamic_linker_flags 64 | mock_module.config.stubs(:dynamic_linker_flags).returns('foo bar') 65 | if RbConfig::CONFIG['target_os'] == 'mingw32' 66 | mock_module.expects(:run_cargo).with('rustc').once 67 | else 68 | mock_module.expects(:run_cargo).with('rustc', '--lib', '--', '-C', 'link-args=foo bar').once 69 | end 70 | mock_module.run_cargo_rustc('debug') 71 | end 72 | 73 | def test_inform_user_about_cargo_exception 74 | _, err = capture_io do 75 | assert_raises RuntimeError do 76 | mock_module(optional_rust_extension: false).inform_user_about_cargo 77 | end 78 | end 79 | 80 | assert_equal '', err 81 | end 82 | 83 | def test_inform_user_about_cargo_warning 84 | _, err = capture_io do 85 | mock_module(optional_rust_extension: true).inform_user_about_cargo 86 | end 87 | 88 | assert_equal mock_module.cargo_recommended_msg, err 89 | end 90 | 91 | def described_class 92 | Tester 93 | end 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/thermite/cargo.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'mkmf' 21 | 22 | module Thermite 23 | # 24 | # Cargo helpers 25 | # 26 | module Cargo 27 | # 28 | # Path to `cargo`. Can be overwritten by using the `CARGO` environment variable. 29 | # 30 | def cargo 31 | @cargo ||= find_executable(ENV.fetch('CARGO', 'cargo')) 32 | end 33 | 34 | # 35 | # Run `cargo` with the given `args` and return `STDOUT`. 36 | # 37 | def run_cargo(*args) 38 | Dir.chdir(config.rust_toplevel_dir) do 39 | sh cargo, *args 40 | end 41 | end 42 | 43 | # 44 | # Only `run_cargo` if it is found in the executable paths. 45 | # 46 | def run_cargo_if_exists(*args) 47 | run_cargo(*args) if cargo 48 | end 49 | 50 | # 51 | # Run `cargo rustc`, given a target (i.e., `release` [default] or `debug`). 52 | # 53 | def run_cargo_rustc(target) 54 | cargo_args = %w[rustc] 55 | cargo_args.push(*cargo_manifest_path_args) 56 | cargo_args << '--release' if target == 'release' 57 | cargo_args.push(*cargo_rustc_args) 58 | run_cargo(*cargo_args) 59 | end 60 | 61 | # 62 | # If the `cargo_workspace_member` option is set, the `--manifest-path` argument to `cargo`. 63 | # 64 | def cargo_manifest_path_args 65 | return [] unless config.cargo_workspace_member 66 | 67 | manifest = File.join(config.cargo_workspace_member, 'Cargo.toml') 68 | ['--manifest-path', manifest] 69 | end 70 | 71 | # 72 | # Inform the user about cargo if it doesn't exist. 73 | # 74 | # If `optional_rust_extension` is true, print message to STDERR. Otherwise, raise an exception. 75 | # 76 | def inform_user_about_cargo 77 | raise cargo_required_msg unless options[:optional_rust_extension] 78 | 79 | $stderr.write(cargo_recommended_msg) 80 | end 81 | 82 | # 83 | # Message used when cargo is not found. 84 | # 85 | # `require_severity` is the verb that indicates how important Rust is to the library. 86 | # 87 | def cargo_msg(require_severity) 88 | < 'redirect') 56 | mock_module.stubs(:http_get).returns('tarball') 57 | mock_module.expects(:unpack_tarball).once 58 | mock_module.expects(:prepare_downloaded_library).once 59 | 60 | assert mock_module.download_binary_from_github_release 61 | end 62 | 63 | def test_download_cargo_version_from_github_release_with_custom_git_tag_format 64 | mock_module(github_releases: true, git_tag_format: 'VER_%s') 65 | mock_module.config.stubs(:toml).returns(package: { version: '4.5.6' }) 66 | stub_github_download_uri('VER_4.5.6') 67 | Net::HTTP.stubs(:get_response).returns('location' => 'redirect') 68 | mock_module.stubs(:http_get).returns('tarball') 69 | mock_module.expects(:unpack_tarball).once 70 | mock_module.expects(:prepare_downloaded_library).once 71 | 72 | assert mock_module.download_binary_from_github_release 73 | end 74 | 75 | def test_download_cargo_version_from_github_release_with_no_repository 76 | mock_module(github_releases: true) 77 | mock_module.config.stubs(:toml).returns(package: { version: '4.5.6' }) 78 | 79 | assert_raises KeyError do 80 | mock_module.download_binary_from_github_release 81 | end 82 | end 83 | 84 | def test_download_cargo_version_from_github_release_with_client_error 85 | mock_module(github_releases: true) 86 | mock_module.config.stubs(:toml).returns( 87 | package: { 88 | repository: 'test/test', 89 | version: '4.5.6' 90 | } 91 | ) 92 | Net::HTTP.stubs(:get_response).returns(Net::HTTPClientError.new('1.1', 403, 'Forbidden')) 93 | 94 | assert !mock_module.download_binary_from_github_release 95 | end 96 | 97 | def test_download_cargo_version_from_github_release_with_server_error 98 | mock_module(github_releases: true) 99 | mock_module.config.stubs(:toml).returns( 100 | package: { 101 | repository: 'test/test', 102 | version: '4.5.6' 103 | } 104 | ) 105 | server_error = Net::HTTPServerError.new('1.1', 500, 'Internal Server Error') 106 | Net::HTTP.stubs(:get_response).returns(server_error) 107 | 108 | assert_raises Net::HTTPServerException do 109 | mock_module.download_binary_from_github_release 110 | end 111 | end 112 | 113 | def test_download_latest_binary_from_github_release 114 | mock_module(github_releases: true, github_release_type: 'latest', git_tag_regex: 'v(.*)_rust') 115 | stub_releases_atom 116 | mock_module.stubs(:download_versioned_github_release_binary).returns(StringIO.new('tarball')) 117 | mock_module.expects(:unpack_tarball).once 118 | mock_module.expects(:prepare_downloaded_library).once 119 | 120 | assert mock_module.download_binary_from_github_release 121 | end 122 | 123 | def test_download_latest_binary_from_github_release_no_releases_match_regex 124 | mock_module(github_releases: true, github_release_type: 'latest') 125 | stub_releases_atom 126 | mock_module.expects(:github_download_uri).never 127 | 128 | assert !mock_module.download_binary_from_github_release 129 | end 130 | 131 | def test_download_latest_binary_from_github_release_no_tarball_found 132 | mock_module(github_releases: true, github_release_type: 'latest', git_tag_regex: 'v(.*)_rust') 133 | stub_releases_atom 134 | mock_module.stubs(:download_versioned_github_release_binary).returns(nil) 135 | mock_module.expects(:unpack_tarball).never 136 | mock_module.expects(:prepare_downloaded_library).never 137 | 138 | assert !mock_module.download_binary_from_github_release 139 | end 140 | 141 | private 142 | 143 | def described_class 144 | Tester 145 | end 146 | 147 | def stub_github_download_uri(tag) 148 | uri = 'https://github.com/user/project/downloads/project-4.5.6.tar.gz' 149 | mock_module.expects(:github_download_uri).with(tag, '4.5.6').returns(uri) 150 | end 151 | 152 | def stub_releases_atom 153 | atom = File.read(fixtures_path('github', 'releases.atom')) 154 | project_uri = 'https://github.com/user/project' 155 | releases_uri = "#{project_uri}/releases.atom" 156 | mock_module.config.stubs(:toml).returns(package: { repository: project_uri }) 157 | mock_module.expects(:http_get).with(releases_uri).returns(atom) 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thermite 2 | 3 | [![Linux/OSX build status](https://travis-ci.org/malept/thermite.svg?branch=master)](https://travis-ci.org/malept/thermite) 4 | [![Windows build status](https://ci.appveyor.com/api/projects/status/kneo890m3ypoxril?svg=true)](https://ci.appveyor.com/project/malept/thermite) 5 | [![Code Climate](https://codeclimate.com/github/malept/thermite/badges/gpa.svg)](https://codeclimate.com/github/malept/thermite) 6 | [![Test coverage](https://codeclimate.com/github/malept/thermite/badges/coverage.svg)](https://codeclimate.com/github/malept/thermite/coverage) 7 | [![Inline docs](http://inch-ci.org/github/malept/thermite.svg?branch=master)](http://inch-ci.org/github/malept/thermite) 8 | [![Gem](https://img.shields.io/gem/v/thermite.svg?maxAge=30000)](https://rubygems.org/gems/thermite) 9 | 10 | Thermite is a Rake-based helper for building and distributing Rust-based Ruby extensions. 11 | 12 | ## Features 13 | 14 | * Provides wrappers for `cargo` commands. 15 | * Handles non-standard `cargo` installations via the `CARGO` environment variable. 16 | * Opt-in to allow users to install pre-compiled Rust extensions hosted on GitHub releases. 17 | * Opt-in to allow users to install pre-compiled Rust extensions hosted on a third party server. 18 | * Provides a wrapper for initializing a Rust extension via Fiddle. 19 | 20 | ## Usage 21 | 22 | 1. Add the following to your gemspec file: 23 | 24 | ```ruby 25 | spec.extensions << 'ext/Rakefile' 26 | spec.add_runtime_dependency 'thermite', '~> 0' 27 | ``` 28 | 29 | 2. Create `ext/Rakefile` with the following code, assuming that the Cargo project root is the same 30 | as the Ruby project root: 31 | 32 | ```ruby 33 | require 'thermite/tasks' 34 | 35 | project_dir = File.dirname(File.dirname(__FILE__)) 36 | Thermite::Tasks.new(cargo_project_path: project_dir, ruby_project_path: project_dir) 37 | task default: %w(thermite:build) 38 | ``` 39 | 40 | 3. In `Rakefile`, integrate Thermite into your build-test workflow: 41 | 42 | ```ruby 43 | require 'thermite/tasks' 44 | 45 | Thermite::Tasks.new 46 | 47 | desc 'Run Rust & Ruby testsuites' 48 | task test: ['thermite:build', 'thermite:test'] do 49 | # … 50 | end 51 | ``` 52 | 53 | Run `rake -T thermite` to view all of the available tasks in the `thermite` namespace. 54 | 55 | ### Configuration 56 | 57 | Task configuration for your project can be set in two ways: 58 | 59 | * passing arguments to `Thermite::Tasks.new` 60 | * adding a `package.metadata.thermite` section to `Cargo.toml`. These settings override the 61 | arguments passed to the `Tasks` class. Due to the conflict, it is infeasible for 62 | `cargo_project_path` or `cargo_workspace_member` to be set in this way. Example section: 63 | 64 | ```toml 65 | [package.metadata.thermite] 66 | 67 | github_releases = true 68 | ``` 69 | 70 | Possible options: 71 | 72 | * `binary_uri_format` - if set, the interpolation-formatted string used to construct the download 73 | URI for the pre-built native extension. If the environment variable `THERMITE_BINARY_URI_FORMAT` 74 | is set, it takes precedence over this option. Either method of setting this option overrides the 75 | `github_releases` option. 76 | Example: `https://example.com/download/%{version}/%{filename}`. Replacement variables: 77 | - `filename` - The value of `Config.tarball_filename` 78 | - `version` - the crate version from `Cargo.toml` 79 | * `cargo_project_path` - the path to the top-level Cargo project. Defaults to the current working 80 | directory. 81 | * `cargo_workspace_member` - if set, the relative path to the Cargo workspace member. Usually used 82 | when it is part of a repository containing multiple crates. 83 | * `github_releases` - whether to look for Rust binaries via GitHub releases when installing 84 | the gem, and `cargo` is not found. Defaults to `false`. 85 | * `github_release_type` - when `github_releases` is `true`, the mode to use to download the Rust 86 | binary from GitHub releases. `'cargo'` (the default) uses the version in `Cargo.toml`, along with 87 | the `git_tag_format` option (described below) to determine the download URI. `'latest'` takes the 88 | latest release matching the `git_tag_regex` option (described below) to determine the download 89 | URI. 90 | * `git_tag_format` - when `github_release_type` is `'cargo'` (the default), the 91 | [format string](http://ruby-doc.org/core/String.html#method-i-25) used to determine the tag used 92 | in the GitHub download URI. Defaults to `v%s`, where `%s` is the version in `Cargo.toml`. 93 | * `git_tag_regex` - when `github_releases` is enabled and `github_release_type` is `'latest'`, a 94 | regular expression (expressed as a `String`) that determines which tagged releases to look for 95 | precompiled Rust tarballs. One group must be specified that indicates the version number to be 96 | used in the tarball filename. Defaults to the [semantic versioning 2.0.0 97 | format](https://semver.org/spec/v2.0.0.html). In this case, the group is around the entire 98 | expression. 99 | * `optional_rust_extension` - prints a warning to STDERR instead of raising an exception, if Cargo 100 | is unavailable and `github_releases` is either disabled or unavailable. Useful for projects where 101 | either fallback code exists, or a native extension is desirable but not required. Defaults 102 | to `false`. 103 | * `ruby_project_path` - the top-level directory of the Ruby gem's project. Defaults to the 104 | current working directory. 105 | * `ruby_extension_dir` - the directory relative to `ruby_project_path` where the extension is 106 | located. Defaults to `lib`. 107 | 108 | ### Example 109 | 110 | Using the cliché Rust+Ruby example, the [`rusty_blank`](https://github.com/malept/rusty_blank) 111 | repository contains an example of using Thermite with [ruru](https://github.com/d-unseductable/ruru) 112 | to provide a `String.blank?` speedup extension. While the example uses ruru, this gem should be 113 | usable with any method of integrating Rust and Ruby that you choose. 114 | 115 | ### Debug / release build 116 | 117 | By default Thermite will do a release build of your Rust code. To do a debug build instead, 118 | set the `CARGO_PROFILE` environment variable to `debug`. 119 | 120 | For example, you can run `CARGO_PROFILE=debug rake thermite:build`. 121 | 122 | ### Troubleshooting 123 | 124 | Debug statements can be written to a file specified by the `THERMITE_DEBUG_FILENAME` environment 125 | variable. 126 | 127 | ## FAQ 128 | 129 | ### Why is it named Thermite? 130 | 131 | According to Wikipedia: 132 | 133 | * The chemical formula for ruby includes Al2O3, or aluminum oxide. 134 | * Rust is iron oxide, or Fe2O3. 135 | * A common thermite reaction uses iron oxide and aluminum to produce iron and aluminum oxide: 136 | Fe2O3 + 2Al → 2Fe + Al2O3 137 | 138 | ## [Release Notes](https://github.com/malept/thermite/blob/master/NEWS.md) 139 | 140 | ## [Contributing](https://github.com/malept/thermite/blob/master/CONTRIBUTING.md) 141 | 142 | ## Legal 143 | 144 | This gem is licensed under the MIT license. 145 | -------------------------------------------------------------------------------- /lib/thermite/tasks.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'fileutils' 21 | require 'rake/tasklib' 22 | require 'thermite/cargo' 23 | require 'thermite/config' 24 | require 'thermite/custom_binary' 25 | require 'thermite/github_release_binary' 26 | require 'thermite/package' 27 | require 'thermite/util' 28 | 29 | # 30 | # Helpers for Rust-based Ruby extensions. 31 | # 32 | module Thermite 33 | # 34 | # Create the following rake tasks: 35 | # 36 | # * `thermite:build` 37 | # * `thermite:clean` 38 | # * `thermite:test` 39 | # * `thermite:tarball` 40 | # 41 | class Tasks < Rake::TaskLib 42 | include Thermite::Cargo 43 | include Thermite::CustomBinary 44 | include Thermite::GithubReleaseBinary 45 | include Thermite::Package 46 | include Thermite::Util 47 | 48 | # 49 | # The configuration used for the Rake tasks. See: {Thermite::Config} 50 | # 51 | attr_reader :config 52 | 53 | # 54 | # Possible configuration options for Thermite tasks: 55 | # 56 | # * `binary_uri_format` - if set, the interpolation-formatted string used to construct the 57 | # download URI for the pre-built native extension. If the environment variable 58 | # `THERMITE_BINARY_URI_FORMAT` is set, it takes precedence over this option. Either method of 59 | # setting this option overrides the `github_releases` option. 60 | # Example: `https://example.com/download/%{version}/%{filename}`. Replacement variables: 61 | # - `filename` - The value of {Config#tarball_filename} 62 | # - `version` - the crate version from the `Cargo.toml` file 63 | # * `cargo_project_path` - the path to the Cargo project. Defaults to the current 64 | # working directory. 65 | # * `cargo_workspace_member` - if set, the relative path to the Cargo workspace member. Usually 66 | # used when it is part of a repository containing multiple crates. 67 | # * `github_releases` - whether to look for rust binaries via GitHub releases when installing 68 | # the gem, and `cargo` is not found. Defaults to `false`. 69 | # * `github_release_type` - when `github_releases` is `true`, the mode to use to download the 70 | # Rust binary from GitHub releases. `'cargo'` (the default) uses the version in `Cargo.toml`, 71 | # along with the `git_tag_format` option (described below) to determine the download URI. 72 | # `'latest'` takes the latest release matching the `git_tag_regex` option (described below) to 73 | # determine the download URI. 74 | # * `git_tag_format` - when `github_release_type` is `'cargo'` (the default), the 75 | # [format string](http://ruby-doc.org/core/String.html#method-i-25) used to determine the 76 | # tag used in the GitHub download URI. Defaults to `v%s`, where `%s` is the version in 77 | # `Cargo.toml`. 78 | # * `git_tag_regex` - when `github_releases` is enabled and `github_release_type` is 79 | # `'latest'`, a regular expression (expressed as a `String`) that determines which tagged 80 | # releases to look for precompiled Rust tarballs. One group must be specified that indicates 81 | # the version number to be used in the tarball filename. Defaults to the [semantic versioning 82 | # 2.0.0 format](https://semver.org/spec/v2.0.0.html). In this case, the group is around the 83 | # entire expression. 84 | # * `optional_rust_extension` - prints a warning to STDERR instead of raising an exception, if 85 | # Cargo is unavailable and `github_releases` is either disabled or unavailable. Useful for 86 | # projects where either fallback code exists, or a native extension is desirable but not 87 | # required. Defaults to `false`. 88 | # * `ruby_project_path` - the toplevel directory of the Ruby gem's project. Defaults to the 89 | # current working directory. 90 | # * `ruby_extension_dir` - the directory relative to `ruby_project_path` where the extension is 91 | # located. Defaults to `lib`. 92 | # 93 | # These values can be overridden by values with the same key name in the 94 | # `package.metadata.thermite` section of `Cargo.toml`, if that section exists. The exceptions 95 | # to this are `cargo_project_path` and `cargo_workspace_member`, since they are both used to 96 | # find the `Cargo.toml` file. 97 | # 98 | attr_reader :options 99 | 100 | # 101 | # Define the Thermite tasks with the given configuration parameters (see {#options}). 102 | # 103 | # Example: 104 | # 105 | # ```ruby 106 | # Thermite::Tasks.new(cargo_project_path: 'rust') 107 | # ``` 108 | # 109 | def initialize(options = {}) 110 | @options = options 111 | @config = Config.new(options) 112 | @options.merge!(@config.toml_config) 113 | define_build_task 114 | define_clean_task 115 | define_test_task 116 | define_package_task 117 | end 118 | 119 | private 120 | 121 | def define_build_task 122 | desc 'Build or download the Rust shared library: CARGO_PROFILE controls Cargo profile' 123 | task 'thermite:build' do 124 | # if cargo found, build. Otherwise, grab binary (when github_releases is enabled). 125 | if cargo 126 | profile = ENV.fetch('CARGO_PROFILE', 'release') 127 | run_cargo_rustc(profile) 128 | FileUtils.cp(config.cargo_target_path(profile, config.cargo_shared_library), 129 | config.ruby_extension_path) 130 | elsif !download_binary_from_custom_uri && !download_binary_from_github_release 131 | inform_user_about_cargo 132 | end 133 | end 134 | end 135 | 136 | def define_clean_task 137 | desc 'Clean up after thermite:build task' 138 | task 'thermite:clean' do 139 | FileUtils.rm(config.ruby_extension_path, force: true) 140 | run_cargo_if_exists 'clean', *cargo_manifest_path_args 141 | end 142 | end 143 | 144 | def define_test_task 145 | desc 'Run Rust testsuite' 146 | task 'thermite:test' do 147 | run_cargo_if_exists 'test', *cargo_manifest_path_args 148 | end 149 | end 150 | 151 | def define_package_task 152 | namespace :thermite do 153 | desc 'Package rust library in a tarball' 154 | task tarball: %w[thermite:build] do 155 | build_package 156 | end 157 | end 158 | end 159 | end 160 | end 161 | -------------------------------------------------------------------------------- /test/lib/thermite/config_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016, 2017 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'test_helper' 21 | 22 | module Thermite 23 | class ConfigTest < Minitest::Test 24 | def test_debug_filename 25 | assert_nil described_class.new.debug_filename 26 | ENV['THERMITE_DEBUG_FILENAME'] = 'foo' 27 | assert_equal 'foo', described_class.new.debug_filename 28 | ENV['THERMITE_DEBUG_FILENAME'] = nil 29 | end 30 | 31 | def test_shared_ext_osx 32 | config.stubs(:dlext).returns('bundle') 33 | assert_equal 'dylib', config.shared_ext 34 | end 35 | 36 | def test_shared_ext_windows 37 | config.stubs(:dlext).returns('so') 38 | Gem.stubs(:win_platform?).returns(true) 39 | assert_equal 'dll', config.shared_ext 40 | end 41 | 42 | def test_shared_ext_unix 43 | config.stubs(:dlext).returns('foobar') 44 | Gem.stubs(:win_platform?).returns(false) 45 | assert_equal 'foobar', config.shared_ext 46 | end 47 | 48 | def test_ruby_version 49 | config.stubs(:rbconfig_ruby_version).returns('3.2.0') 50 | assert_equal 'ruby32', config.ruby_version 51 | end 52 | 53 | def test_library_name_from_cargo_lib 54 | config.stubs(:toml).returns(lib: { name: 'foobar' }, package: { name: 'barbaz' }) 55 | assert_equal 'foobar', config.library_name 56 | end 57 | 58 | def test_library_name_from_cargo_package 59 | config.stubs(:toml).returns(lib: {}, package: { name: 'barbaz' }) 60 | assert_equal 'barbaz', config.library_name 61 | end 62 | 63 | def test_library_name_from_cargo_lib_has_no_hyphens 64 | config.stubs(:toml).returns(lib: { name: 'foo-bar' }, package: { name: 'bar-baz' }) 65 | assert_equal 'foo_bar', config.library_name 66 | end 67 | 68 | def test_library_name_from_cargo_package_has_no_hyphens 69 | config.stubs(:toml).returns(lib: {}, package: { name: 'bar-baz' }) 70 | assert_equal 'bar_baz', config.library_name 71 | end 72 | 73 | def test_shared_library 74 | config.stubs(:library_name).returns('foobar') 75 | config.stubs(:shared_ext).returns('ext') 76 | Gem.stubs(:win_platform?).returns(false) 77 | assert_equal 'foobar.so', config.shared_library 78 | end 79 | 80 | def test_shared_library_windows 81 | config.stubs(:library_name).returns('foobar') 82 | config.stubs(:shared_ext).returns('ext') 83 | Gem.stubs(:win_platform?).returns(true) 84 | assert_equal 'foobar.so', config.shared_library 85 | end 86 | 87 | def test_cargo_shared_library 88 | config.stubs(:library_name).returns('foobar') 89 | config.stubs(:shared_ext).returns('ext') 90 | Gem.stubs(:win_platform?).returns(false) 91 | assert_equal 'libfoobar.ext', config.cargo_shared_library 92 | end 93 | 94 | def test_cargo_shared_library_windows 95 | config.stubs(:library_name).returns('foobar') 96 | config.stubs(:shared_ext).returns('ext') 97 | Gem.stubs(:win_platform?).returns(true) 98 | assert_equal 'foobar.ext', config.cargo_shared_library 99 | end 100 | 101 | def test_tarball_filename 102 | stub_tarball_filename_params(false) 103 | assert_equal 'foobar-0.1.2-ruby12-c64-z80.tar.gz', config.tarball_filename('0.1.2') 104 | end 105 | 106 | def test_tarball_filename_with_static_extension 107 | stub_tarball_filename_params(true) 108 | assert_equal 'foobar-0.1.2-ruby12-c64-z80-static.tar.gz', config.tarball_filename('0.1.2') 109 | end 110 | 111 | def test_default_ruby_toplevel_dir 112 | FileUtils.stubs(:pwd).returns('/tmp/foobar') 113 | assert_equal '/tmp/foobar', config.ruby_toplevel_dir 114 | end 115 | 116 | def test_ruby_toplevel_dir 117 | FileUtils.stubs(:pwd).returns('/tmp/foobar') 118 | assert_equal '/tmp/barbaz', config(ruby_project_path: '/tmp/barbaz').ruby_toplevel_dir 119 | end 120 | 121 | def test_ruby_path 122 | FileUtils.stubs(:pwd).returns('/tmp/foobar') 123 | assert_equal '/tmp/foobar/baz/quux', config.ruby_path('baz', 'quux') 124 | end 125 | 126 | def test_ruby_extension_path 127 | FileUtils.stubs(:pwd).returns('/tmp/foobar') 128 | config.stubs(:shared_library).returns('libfoo.ext') 129 | assert_equal '/tmp/foobar/lib/libfoo.ext', config.ruby_extension_path 130 | end 131 | 132 | def test_ruby_extension_path_with_custom_extension_dir 133 | FileUtils.stubs(:pwd).returns('/tmp/foobar') 134 | config.stubs(:ruby_extension_dir).returns('lib/ext') 135 | config.stubs(:shared_library).returns('libfoo.ext') 136 | assert_equal '/tmp/foobar/lib/ext/libfoo.ext', config.ruby_extension_path 137 | end 138 | 139 | def test_default_rust_toplevel_dir 140 | FileUtils.stubs(:pwd).returns('/tmp/foobar') 141 | assert_equal '/tmp/foobar', config.rust_toplevel_dir 142 | end 143 | 144 | def test_rust_toplevel_dir 145 | FileUtils.stubs(:pwd).returns('/tmp/foobar') 146 | assert_equal '/tmp/barbaz', config(cargo_project_path: '/tmp/barbaz').rust_toplevel_dir 147 | end 148 | 149 | def test_rust_path 150 | FileUtils.stubs(:pwd).returns('/tmp/foobar') 151 | assert_equal '/tmp/foobar/baz/quux', config.rust_path('baz', 'quux') 152 | end 153 | 154 | def test_cargo_target_path_with_env_var 155 | FileUtils.stubs(:pwd).returns('/tmp/foobar') 156 | ENV['CARGO_TARGET_DIR'] = 'foo' 157 | assert_equal File.join('foo', 'debug', 'bar'), config.cargo_target_path('debug', 'bar') 158 | ENV['CARGO_TARGET_DIR'] = nil 159 | end 160 | 161 | def test_cargo_target_path_without_env_var 162 | FileUtils.stubs(:pwd).returns('/tmp/foobar') 163 | ENV['CARGO_TARGET_DIR'] = nil 164 | assert_equal File.join('/tmp/foobar', 'target', 'debug', 'bar'), 165 | config.cargo_target_path('debug', 'bar') 166 | end 167 | 168 | def test_cargo_toml_path_with_workspace_member 169 | FileUtils.stubs(:pwd).returns('/tmp/foobar') 170 | config(cargo_workspace_member: 'baz') 171 | assert_equal '/tmp/foobar/baz/Cargo.toml', config.cargo_toml_path 172 | end 173 | 174 | def test_default_git_tag_regex 175 | assert_equal described_class::DEFAULT_TAG_REGEX, config.git_tag_regex 176 | end 177 | 178 | def test_git_tag_regex 179 | assert_equal(/abc(\d)/, config(git_tag_regex: 'abc(\d)').git_tag_regex) 180 | end 181 | 182 | def test_toml 183 | expected = { 184 | package: { 185 | name: 'fixture', 186 | metadata: { 187 | thermite: { 188 | github_releases: true 189 | } 190 | } 191 | } 192 | } 193 | assert_equal expected, config(cargo_project_path: fixtures_path('config')).toml 194 | end 195 | 196 | def test_default_toml_config 197 | config.stubs(:toml).returns({}) 198 | assert_equal({}, config.toml_config) 199 | end 200 | 201 | def test_toml_config 202 | expected = { github_releases: true } 203 | assert_equal expected, config(cargo_project_path: fixtures_path('config')).toml_config 204 | end 205 | 206 | def test_static_extension_sans_env_var 207 | ENV.stubs(:key?).with('RUBY_STATIC').returns(false) 208 | RbConfig::CONFIG.stubs(:[]).with('ENABLE_SHARED').returns('yes') 209 | refute config.static_extension? 210 | 211 | RbConfig::CONFIG.stubs(:[]).with('ENABLE_SHARED').returns('no') 212 | assert config.static_extension? 213 | end 214 | 215 | def test_static_extension_with_env_var 216 | ENV.stubs(:key?).with('RUBY_STATIC').returns(true) 217 | RbConfig::CONFIG.stubs(:[]).with('ENABLE_SHARED').returns('yes') 218 | assert config.static_extension? 219 | end 220 | 221 | private 222 | 223 | def config(options = {}) 224 | @config ||= described_class.new(options) 225 | end 226 | 227 | def described_class 228 | Thermite::Config 229 | end 230 | 231 | def stub_tarball_filename_params(static_extension) 232 | config.stubs(:library_name).returns('foobar') 233 | config.stubs(:ruby_version).returns('ruby12') 234 | config.stubs(:target_os).returns('c64') 235 | config.stubs(:target_arch).returns('z80') 236 | config.stubs(:static_extension?).returns(static_extension) 237 | end 238 | end 239 | end 240 | -------------------------------------------------------------------------------- /lib/thermite/config.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # 3 | # Copyright (c) 2016 Mark Lee and contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | # associated documentation files (the "Software"), to deal in the Software without restriction, 7 | # including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | # sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all copies or 12 | # substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 17 | # DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 18 | # OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | 20 | require 'fileutils' 21 | require 'rbconfig' 22 | require 'thermite/semver' 23 | require 'tomlrb' 24 | 25 | module Thermite 26 | # 27 | # Configuration helper 28 | # 29 | class Config 30 | # 31 | # Creates a new configuration object. 32 | # 33 | # `options` is the same as the {Thermite::Tasks#initialize} parameter. 34 | # 35 | def initialize(options = {}) 36 | @options = options 37 | end 38 | 39 | # 40 | # Location to emit debug output, if not `nil`. Defaults to `nil`. 41 | # 42 | def debug_filename 43 | @debug_filename ||= ENV['THERMITE_DEBUG_FILENAME'] 44 | end 45 | 46 | # 47 | # The file extension of the compiled shared Rust library. 48 | # 49 | def shared_ext 50 | @shared_ext ||= begin 51 | if dlext == 'bundle' 52 | 'dylib' 53 | elsif Gem.win_platform? 54 | 'dll' 55 | else 56 | dlext 57 | end 58 | end 59 | end 60 | 61 | # 62 | # The interpolation-formatted string used to construct the download URI for the pre-built 63 | # native extension. Can be set via the `THERMITE_BINARY_URI_FORMAT` environment variable, or a 64 | # `binary_uri_format` option. 65 | # 66 | def binary_uri_format 67 | @binary_uri_format ||= ENV['THERMITE_BINARY_URI_FORMAT'] || 68 | @options[:binary_uri_format] || 69 | false 70 | end 71 | 72 | # 73 | # The major and minor version of the Ruby interpreter that's currently running. 74 | # 75 | def ruby_version 76 | @ruby_version ||= begin 77 | version_info = rbconfig_ruby_version.split('.') 78 | "ruby#{version_info[0]}#{version_info[1]}" 79 | end 80 | end 81 | 82 | # :nocov: 83 | 84 | # 85 | # Alias for `RbConfig::CONFIG['target_cpu']`. 86 | # 87 | def target_arch 88 | @target_arch ||= RbConfig::CONFIG['target_cpu'] 89 | end 90 | 91 | # 92 | # Alias for `RbConfig::CONFIG['target_os']`. 93 | # 94 | def target_os 95 | @target_os ||= RbConfig::CONFIG['target_os'] 96 | end 97 | # :nocov: 98 | 99 | # 100 | # The name of the library compiled by Rust. 101 | # 102 | # Due to the way that Cargo works, all hyphens in library names are replaced with underscores. 103 | # 104 | def library_name 105 | @library_name ||= begin 106 | base = toml[:lib] && toml[:lib][:name] ? toml[:lib] : toml[:package] 107 | base[:name].tr('-', '_') if base[:name] 108 | end 109 | end 110 | 111 | # 112 | # The basename of the shared library built by Cargo. 113 | # 114 | def cargo_shared_library 115 | @cargo_shared_library ||= begin 116 | filename = "#{library_name}.#{shared_ext}" 117 | filename = "lib#{filename}" unless Gem.win_platform? 118 | filename 119 | end 120 | end 121 | 122 | # 123 | # The basename of the Rust shared library, as installed in the {#ruby_extension_path}. 124 | # 125 | def shared_library 126 | @shared_library ||= "#{library_name}.so" 127 | end 128 | 129 | # 130 | # Return the basename of the tarball generated by the `thermite:tarball` Rake task, given a 131 | # package `version`. 132 | # 133 | def tarball_filename(version) 134 | static = static_extension? ? '-static' : '' 135 | 136 | "#{library_name}-#{version}-#{ruby_version}-#{target_os}-#{target_arch}#{static}.tar.gz" 137 | end 138 | 139 | # 140 | # The top-level directory of the Ruby project. Defaults to the current working directory. 141 | # 142 | def ruby_toplevel_dir 143 | @ruby_toplevel_dir ||= @options.fetch(:ruby_project_path, FileUtils.pwd) 144 | end 145 | 146 | # 147 | # Generate a path relative to {#ruby_toplevel_dir}, given the `path_components` that are passed 148 | # to `File.join`. 149 | # 150 | def ruby_path(*path_components) 151 | File.join(ruby_toplevel_dir, *path_components) 152 | end 153 | 154 | # :nocov: 155 | 156 | # 157 | # Absolute path to the shared libruby. 158 | # 159 | def libruby_path 160 | @libruby_path ||= File.join(RbConfig::CONFIG['libdir'], RbConfig::CONFIG['LIBRUBY_SO']) 161 | end 162 | 163 | # :nocov: 164 | 165 | # 166 | # The top-level directory of the Cargo project. Defaults to the current working directory. 167 | # 168 | def rust_toplevel_dir 169 | @rust_toplevel_dir ||= @options.fetch(:cargo_project_path, FileUtils.pwd) 170 | end 171 | 172 | # 173 | # Generate a path relative to {#rust_toplevel_dir}, given the `path_components` that are 174 | # passed to `File.join`. 175 | # 176 | def rust_path(*path_components) 177 | File.join(rust_toplevel_dir, *path_components) 178 | end 179 | 180 | # 181 | # Generate a path relative to the `CARGO_TARGET_DIR` environment variable, or 182 | # {#rust_toplevel_dir}/target if that is not set. 183 | # 184 | def cargo_target_path(target, *path_components) 185 | target_base = ENV.fetch('CARGO_TARGET_DIR', File.join(rust_toplevel_dir, 'target')) 186 | File.join(target_base, target, *path_components) 187 | end 188 | 189 | # 190 | # If run in a multi-crate environment, the Cargo workspace member that contains the 191 | # Ruby extension. 192 | # 193 | def cargo_workspace_member 194 | @cargo_workspace_member ||= @options[:cargo_workspace_member] 195 | end 196 | 197 | # 198 | # The absolute path to the `Cargo.toml` file. The path depends on the existence of the 199 | # {#cargo_workspace_member} configuration option. 200 | # 201 | def cargo_toml_path 202 | @cargo_toml_path ||= begin 203 | components = ['Cargo.toml'] 204 | components.unshift(cargo_workspace_member) if cargo_workspace_member 205 | 206 | rust_path(*components) 207 | end 208 | end 209 | 210 | # 211 | # The relative directory where the Rust shared library resides, in the context of the Ruby 212 | # project. 213 | # 214 | def ruby_extension_dir 215 | @ruby_extension_dir ||= @options.fetch(:ruby_extension_dir, 'lib') 216 | end 217 | 218 | # 219 | # Path to the Rust shared library in the context of the Ruby project. 220 | # 221 | def ruby_extension_path 222 | ruby_path(ruby_extension_dir, shared_library) 223 | end 224 | 225 | # 226 | # The default git tag regular expression (semantic versioning format). 227 | # 228 | DEFAULT_TAG_REGEX = /^(#{Thermite::SemVer::VERSION})$/ 229 | 230 | # 231 | # The format (as a regular expression) that git tags containing Rust binary 232 | # tarballs are supposed to match. Defaults to `DEFAULT_TAG_REGEX`. 233 | # 234 | def git_tag_regex 235 | @git_tag_regex ||= begin 236 | if @options[:git_tag_regex] 237 | Regexp.new(@options[:git_tag_regex]) 238 | else 239 | DEFAULT_TAG_REGEX 240 | end 241 | end 242 | end 243 | 244 | # 245 | # Parsed TOML object (courtesy of `tomlrb`). 246 | # 247 | def toml 248 | @toml ||= Tomlrb.load_file(cargo_toml_path, symbolize_keys: true) 249 | end 250 | 251 | # 252 | # Alias to the crate version specified in the TOML file. 253 | # 254 | def crate_version 255 | toml[:package][:version] 256 | end 257 | 258 | # 259 | # The Thermite-specific config from the TOML file. 260 | # 261 | def toml_config 262 | @toml_config ||= begin 263 | # Not using .dig to be Ruby < 2.3 compatible 264 | if toml && toml[:package] && toml[:package][:metadata] && 265 | toml[:package][:metadata][:thermite] 266 | toml[:package][:metadata][:thermite] 267 | else 268 | {} 269 | end 270 | end 271 | end 272 | 273 | # :nocov: 274 | 275 | # 276 | # Linker flags for libruby. 277 | # 278 | def dynamic_linker_flags 279 | @dynamic_linker_flags ||= RbConfig::CONFIG['DLDFLAGS'].strip 280 | end 281 | 282 | # 283 | # Whether to use a statically linked extension. 284 | # 285 | def static_extension? 286 | ENV.key?('RUBY_STATIC') || RbConfig::CONFIG['ENABLE_SHARED'] == 'no' 287 | end 288 | 289 | private 290 | 291 | def dlext 292 | RbConfig::CONFIG['DLEXT'] 293 | end 294 | 295 | def rbconfig_ruby_version 296 | RbConfig::CONFIG['ruby_version'] 297 | end 298 | end 299 | end 300 | --------------------------------------------------------------------------------