├── .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 | [](https://travis-ci.org/malept/thermite)
4 | [](https://ci.appveyor.com/project/malept/thermite)
5 | [](https://codeclimate.com/github/malept/thermite)
6 | [](https://codeclimate.com/github/malept/thermite/coverage)
7 | [](http://inch-ci.org/github/malept/thermite)
8 | [](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 |
--------------------------------------------------------------------------------