rcee_precompiled 0.1.0-x64-mingw32 → 0.4.0-x64-mingw32

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e12ef62653c5915c0d72a743c49899ae029682943297ea443f13cfc7cb789ad4
4
- data.tar.gz: 5fc721a6b4bce84bf2bedfc3aed6b275c2bf945c5524d5a7d005e90b9e57b8c8
3
+ metadata.gz: ea6f3cd835a5bed241848335027c8a8cf022833ae7026da43029743ad5f91880
4
+ data.tar.gz: a766319f7e9bccd4ec17ece36b32d6fb7e4b66bb7ab0d507ac152895d5d3d5ba
5
5
  SHA512:
6
- metadata.gz: 94212df664d8204add58e579455d67814142ae83dc9a8c98e7bc0db924212753b1dc54033c55f97cdf048be2c0fe8234bbdeaa8f44fdb6fc8d5ff981af704948
7
- data.tar.gz: 748b518e55b7dcf53f7b2c99c044d81cfe56e309c48daeb4807c79ba7122ef64da62d4012c2ee687cd5d55b5e67c51e5b93847417395f6bf5569ae674d23a2f9
6
+ metadata.gz: 75ee4cf1972cbba08b4a0907bb2e0dbfe1161a6a94d9be354fc430823c8600b45e42a460c996a820e665b5c6deaadb7f223dd096ebbcba46f4c24642932c7ccb
7
+ data.tar.gz: e70418704048aa9f987acb84d84181798fd133bec1335ef3846a78310f0521438a6a84c2f8de81dc477dcace06715b4967c1acba6484b0d2225c13296f149ec6
data/Gemfile CHANGED
@@ -8,6 +8,6 @@ gemspec
8
8
  gem "rake", "~> 13.0"
9
9
 
10
10
  gem "rake-compiler"
11
- gem "rake-compiler-dock"
11
+ gem "rake-compiler-dock", "~> 1.2.1"
12
12
 
13
13
  gem "minitest", "~> 5.0"
data/README.md CHANGED
@@ -1,2 +1,191 @@
1
+ # RCEE::Precompiled
1
2
 
2
3
  This gem is part of the Ruby C Extensions Explained project at https://github.com/flavorjones/ruby-c-extensions-explained
4
+
5
+ ## Summary
6
+
7
+ Installation time is an important aspect of developer happiness, and the Packaged strategies can make your users very unhappy.
8
+
9
+ One way to speed up installation time is to precompile the extension, so that during `gem install` files are simply being unpacked, and the entire configure/compile/link process is avoided completely. This can be tricky to set up, and requires that you test across all the target platforms; but makes for much happier users.
10
+
11
+ Note, however, that it's necessary to _also_ ship the vanilla (platform=ruby) gem as a fallback for platforms you haven't pre-compiled for.
12
+
13
+ In the nine months since Nokogiri v1.11 started shipping precompiled native gems, we've seen over 45 million gem installations, and essentially zero support issues have been opened. Twitter complaints dropped to near zero. Page views of the Nokogiri installation tutorial dropped by half.
14
+
15
+ Precompiled Nokogiri gems have been an unmitigated success and have made both users and maintainers happy.
16
+
17
+
18
+ ## Details
19
+
20
+ An important tool we rely on for precompiling is [`rake-compiler/rake-compiler-dock`](https://github.com/rake-compiler/rake-compiler-dock), maintained by Lars Kanis. `rake-compiler-dock` is a Docker-based build environment that uses `rake-compiler` to _cross compile_ for all of these major platforms.
21
+
22
+ What this means is, I can run my normal gem build process in a docker container on my Linux machine, and it will build gems that I run on Windows and MacOS.
23
+
24
+ This is really powerful stuff, and once we assume that we can cross-compile reliably, the remaining problems boil down to modifying how we build the gemfile and making sure we test gems adequately on the target platforms.
25
+
26
+ First, we need to add some features to our `Rake::ExtensionTask` in `Rakefile`:
27
+
28
+ ``` ruby
29
+ cross_rubies = ["3.1.0", "3.0.0", "2.7.0", "2.6.0"]
30
+ cross_platforms = [
31
+ "x64-mingw32",
32
+ "x64-mingw-ucrt",
33
+ "x86-linux",
34
+ "x86_64-linux",
35
+ "aarch64-linux",
36
+ "x86_64-darwin",
37
+ "arm64-darwin",
38
+ ]
39
+ ENV["RUBY_CC_VERSION"] = cross_rubies.join(":")
40
+
41
+ Rake::ExtensionTask.new("precompiled", rcee_precompiled_spec) do |ext|
42
+ ext.lib_dir = "lib/rcee/precompiled"
43
+ ext.cross_compile = true
44
+ ext.cross_platform = cross_platforms
45
+ ext.cross_config_options << "--enable-cross-build" # so extconf.rb knows we're cross-compiling
46
+ ext.cross_compiling do |spec|
47
+ # remove things not needed for precompiled gems
48
+ spec.dependencies.reject! { |dep| dep.name == "mini_portile2" }
49
+ spec.files.reject! { |file| File.fnmatch?("*.tar.gz", file) }
50
+ end
51
+ end
52
+ ```
53
+
54
+ This does the following:
55
+
56
+ - set up some local variables to indicate what ruby versions and which platforms we will build for
57
+ - set an environment variable to let rake-compiler know which ruby versions we'll build
58
+ - tell the extension task to turn on cross-compiling features, including additional rake tasks
59
+ - signal to our `extconf.rb` when we're cross-compiling (in case its behavior needs to change)
60
+ - finally, in a block that is only run when cross-compiling, we modify the gemspec to remove things we don't need in native gems:
61
+ - the tarball
62
+ - the dependency on `mini_portile`
63
+
64
+ Next we need some new rake tasks:
65
+
66
+ ``` ruby
67
+ namespace "gem" do
68
+ cross_platforms.each do |platform|
69
+ desc "build native gem for #{platform}"
70
+ task platform do
71
+ RakeCompilerDock.sh(<<~EOF, platform: platform)
72
+ gem install bundler --no-document &&
73
+ bundle &&
74
+ bundle exec rake gem:#{platform}:buildit
75
+ EOF
76
+ end
77
+
78
+ namespace platform do
79
+ # this runs in the rake-compiler-dock docker container
80
+ task "buildit" do
81
+ # use Task#invoke because the pkg/*gem task is defined at runtime
82
+ Rake::Task["native:#{platform}"].invoke
83
+ Rake::Task["pkg/#{rcee_precompiled_spec.full_name}-#{Gem::Platform.new(platform)}.gem"].invoke
84
+ end
85
+ end
86
+ end
87
+ end
88
+ ```
89
+
90
+ The top task (or, really, _set_ of tasks) runs on the host system, and invokes the lower task within the appropriate docker container to cross-compile for that platform. So the bottom task is doing most of the work, and it's doing it inside a guest container. Please note that the worker is both building the extension *and* packaging the gem according to the gemspec that was just modified by our extensiontask cross-compiling block.
91
+
92
+ Changes to the `extconf.rb`:
93
+
94
+ ``` ruby
95
+ ENV["CC"] = RbConfig::CONFIG["CC"]
96
+ ```
97
+
98
+ This makes sure that the cross-compiler is the compiler used within the guest container (and not the native linux compiler).
99
+
100
+ ``` ruby
101
+ cross_build_p = enable_config("cross-build")
102
+ ```
103
+
104
+ The cross-compile rake task signals to `extconf.rb` that it's cross-compiling by using a commandline flag that we can inspect. We'll need this for `libyaml` to make sure that set the appropriate flags during precompilation (flags which shouldn't be set when compiling natively).
105
+
106
+ ``` ruby
107
+ MiniPortile.new("yaml", "0.2.5").tap do |recipe|
108
+ # ...
109
+ # configure the environment that MiniPortile will use for subshells
110
+ ENV.to_h.dup.tap do |env|
111
+ # -fPIC is necessary for linking into a shared library
112
+ env["CFLAGS"] = [env["CFLAGS"], "-fPIC"].join(" ")
113
+ env["SUBDIRS"] = "include src" # libyaml: skip tests
114
+
115
+ recipe.configure_options += env.map { |key, value| "#{key}=#{value.strip}" }
116
+ end
117
+ # ...
118
+ end
119
+ ```
120
+
121
+ The rest of the extconf changes are related to configuring libyaml at build time. We need to set the -fPIC option so we can mix static and shared libraries together. (This should probably always be set.)
122
+
123
+ The "SUBDIRS" environment variable is something that's very specific to libyaml, though: it tells libyaml's autoconf build system to skip running the tests. We have to do this because although we can *generate* binaries for other platforms, we can't actually *run* them.
124
+
125
+ We have one more small change we'll need to make to how the extension is required. Let's take a look at the directory structure in the packaged gem:
126
+
127
+ ``` text
128
+ lib
129
+ └── rcee
130
+ ├── precompiled
131
+ │   ├── 2.6
132
+ │   │   └── precompiled.so
133
+ │   ├── 2.7
134
+ │   │   └── precompiled.so
135
+ │   ├── 3.0
136
+ │   │   └── precompiled.so
137
+ │   ├── 3.1
138
+ │   │   └── precompiled.so
139
+ │   └── version.rb
140
+ └── precompiled.rb
141
+ ```
142
+
143
+ You can see that we have FOUR c extensions in this gem, one for each minor version of Ruby that we support. Remember that a C extension is specific to an architecture and a version of Ruby. For example, if we're running Ruby 3.0.1, then we need to load the extension in the 3.0 directory. Let's make sure we do that.
144
+
145
+ In `lib/rcee/precompiled.rb`, we'll replace the normal `require` with:
146
+
147
+ ``` ruby
148
+ begin
149
+ # load the precompiled extension file
150
+ ruby_version = /(\d+\.\d+)/.match(::RUBY_VERSION)
151
+ require_relative "precompiled/#{ruby_version}/precompiled"
152
+ rescue LoadError
153
+ # fall back to the extension compiled upon installation.
154
+ require "rcee/precompiled/precompiled"
155
+ end
156
+ ```
157
+
158
+ Go ahead and try it! `gem install rcee_precompiled`. If you're on windows, linux, or macos you should get a precompiled version that installs in under a second. Everyone else (hello FreeBSD people!) it'll take a few more seconds to build the vanilla gem's packaged tarball.
159
+
160
+
161
+ ## Testing
162
+
163
+ See [.github/workflows/precompiled.yml](../.github/workflows/precompiled.yml)
164
+
165
+ Key things to note:
166
+
167
+ - matrix across all supported Rubies and platforms (for compile-from-source installation testing)
168
+ - test native gems for a variety of platforms
169
+ - use rake-compiler-dock images to build the gems
170
+ - then install on native platforms and verify that it passes tests
171
+
172
+ Note that there's additional complexity because of how we test:
173
+
174
+ - see new script bin/test-gem-build which artificially bumps the VERSION string to double-check we're testing the packaged version of the gem (which the tests output)
175
+ - see new script bin/test-gem-install which installs the gem, deletes the local source code, and runs the tests against the installed gem
176
+ - the gemspec handles a missing version file (because we delete the local source code during testing)
177
+
178
+
179
+ ## What Can Go Wrong
180
+
181
+ This strategy isn't perfect. Remember what I said earlier, that a compiled C extension is specific to
182
+
183
+ - the minor version of ruby (e.g., 3.0)
184
+ - the machine architecture (e.g., x86_64)
185
+ - the system libraries
186
+
187
+ The precompiled strategy mostly takes care of the first two, but there are still edge cases for system libraries. The big gotcha is that linux libc is not the same as linux musl, and we've had to work around this a few times in Nokogiri.
188
+
189
+ I'm positive that there are more edge cases that will be found as users add more platforms and as more gems start precompiling. I'm willing to bet money that you can break this by setting some Ruby compile-time flags on your system. I'm honestly surprised it works as well as it has. (Worth noting: the `sassc` gem stopped shipping native gems for linux because of the musl incompatibilities.)
190
+
191
+ So the lesson here is: make sure you have an automated test pipeline that will build a gem and test it on the target platform! This takes time to set up, but it will save you time and effort in the long run.
data/Rakefile CHANGED
@@ -1,9 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
+ require "rubygems/package_task"
4
5
  require "rake/testtask"
6
+ require "rake/extensiontask"
7
+ require "rake_compiler_dock"
8
+
9
+ cross_rubies = ["3.1.0", "3.0.0", "2.7.0", "2.6.0"]
10
+ cross_platforms = [
11
+ "aarch64-linux",
12
+ "arm-linux",
13
+ "arm64-darwin",
14
+ "x64-mingw-ucrt",
15
+ "x64-mingw32",
16
+ "x86-linux",
17
+ "x86_64-darwin",
18
+ "x86_64-linux",
19
+ ]
20
+ ENV["RUBY_CC_VERSION"] = cross_rubies.join(":")
5
21
 
6
22
  rcee_precompiled_spec = Bundler.load_gemspec("rcee_precompiled.gemspec")
23
+ Gem::PackageTask.new(rcee_precompiled_spec).define #packaged_tarball version of the gem for platform=ruby
24
+ task "package" => cross_platforms.map { |p| "gem:#{p}" } # "package" task for all the native platforms
7
25
 
8
26
  Rake::TestTask.new(:test) do |t|
9
27
  t.libs << "test"
@@ -11,19 +29,6 @@ Rake::TestTask.new(:test) do |t|
11
29
  t.test_files = FileList["test/**/*_test.rb"]
12
30
  end
13
31
 
14
- # "gem" task for vanilla packaged (tarball)
15
- require "rubygems/package_task"
16
- Gem::PackageTask.new(rcee_precompiled_spec).define
17
-
18
- require "rake/extensiontask"
19
- require "rake_compiler_dock"
20
-
21
- cross_rubies = ["3.0.0", "2.7.0", "2.6.0", "2.5.0"]
22
- cross_platforms = ["x64-mingw32", "x86_64-linux", "x86_64-darwin", "arm64-darwin"]
23
-
24
- ENV["RUBY_CC_VERSION"] = cross_rubies.join(":")
25
-
26
-
27
32
  Rake::ExtensionTask.new("precompiled", rcee_precompiled_spec) do |ext|
28
33
  ext.lib_dir = "lib/rcee/precompiled"
29
34
  ext.cross_compile = true
@@ -31,7 +36,7 @@ Rake::ExtensionTask.new("precompiled", rcee_precompiled_spec) do |ext|
31
36
  ext.cross_config_options << "--enable-cross-build" # so extconf.rb knows we're cross-compiling
32
37
  ext.cross_compiling do |spec|
33
38
  # remove things not needed for precompiled gems
34
- spec.dependencies.reject! { |dep| dep.name == 'mini_portile2' }
39
+ spec.dependencies.reject! { |dep| dep.name == "mini_portile2" }
35
40
  spec.files.reject! { |file| File.fnmatch?("*.tar.gz", file) }
36
41
  end
37
42
  end
@@ -52,7 +57,7 @@ namespace "gem" do
52
57
  task "buildit" do
53
58
  # use Task#invoke because the pkg/*gem task is defined at runtime
54
59
  Rake::Task["native:#{platform}"].invoke
55
- Rake::Task["pkg/#{rcee_precompiled_spec.full_name}-#{Gem::Platform.new(platform).to_s}.gem"].invoke
60
+ Rake::Task["pkg/#{rcee_precompiled_spec.full_name}-#{Gem::Platform.new(platform)}.gem"].invoke
56
61
  end
57
62
  end
58
63
  end
@@ -61,9 +66,36 @@ namespace "gem" do
61
66
  multitask "all" => [cross_platforms, "gem"].flatten
62
67
  end
63
68
 
69
+ desc "Temporarily set VERSION to a unique timestamp"
70
+ task "set-version-to-timestamp" do
71
+ # this task is used by bin/test-gem-build
72
+ # to test building, packaging, and installing a precompiled gem
73
+ version_constant_re = /^\s*VERSION\s*=\s*["'](.*)["']$/
74
+
75
+ version_file_path = File.join(__dir__, "lib/rcee/precompiled/version.rb")
76
+ version_file_contents = File.read(version_file_path)
77
+
78
+ current_version_string = version_constant_re.match(version_file_contents)[1]
79
+ current_version = Gem::Version.new(current_version_string)
80
+
81
+ fake_version = Gem::Version.new(format("%s.test.%s", current_version.bump, Time.now.strftime("%Y.%m%d.%H%M")))
82
+
83
+ unless version_file_contents.gsub!(version_constant_re, " VERSION = \"#{fake_version}\"")
84
+ raise("Could not hack the VERSION constant")
85
+ end
86
+
87
+ File.open(version_file_path, "w") { |f| f.write(version_file_contents) }
88
+
89
+ puts "NOTE: wrote version as \"#{fake_version}\""
90
+ end
91
+
92
+ task default: [:clobber, :compile, :test]
64
93
 
94
+ CLEAN.add("{ext,lib}/**/*.{o,so}", "pkg")
65
95
  CLOBBER.add("ports")
66
- CLEAN.add("{ext,lib}/**/*.{o,so}")
67
96
 
68
- task build: :compile
69
- task default: %i[clobber compile test]
97
+ # when packaging the gem, if the tarball isn't cached, we need to fetch it. the easiest thing to do
98
+ # is to run the compile phase to invoke the extconf and have mini_portile download the file for us.
99
+ # this is wasteful and in the future I would prefer to separate mini_portile from the extconf to
100
+ # allow us to download without compiling.
101
+ Rake::Task["package"].prerequisites.prepend("compile")
@@ -6,7 +6,6 @@ package_root_dir = File.expand_path(File.join(File.dirname(__FILE__), "..", ".."
6
6
  RbConfig::CONFIG["CC"] = RbConfig::MAKEFILE_CONFIG["CC"] = ENV["CC"] if ENV["CC"]
7
7
  ENV["CC"] = RbConfig::CONFIG["CC"]
8
8
 
9
- # not needed for libyaml, but generally useful to know whether we're cross-compiling
10
9
  cross_build_p = enable_config("cross-build")
11
10
 
12
11
  MiniPortile.new("yaml", "0.2.5").tap do |recipe|
@@ -17,12 +16,14 @@ MiniPortile.new("yaml", "0.2.5").tap do |recipe|
17
16
  recipe.target = File.join(package_root_dir, "ports")
18
17
 
19
18
  # configure the environment that MiniPortile will use for subshells
20
- ENV.to_h.dup.tap do |env|
21
- # -fPIC is necessary for linking into a shared library
22
- env["CFLAGS"] = [env["CFLAGS"], "-fPIC"].join(" ")
23
- env["SUBDIRS"] = "include src" # libyaml: skip tests
24
-
25
- recipe.configure_options += env.map { |key, value| "#{key}=#{value.strip}" }
19
+ if cross_build_p
20
+ ENV.to_h.tap do |env|
21
+ # -fPIC is necessary for linking into a shared library
22
+ env["CFLAGS"] = [env["CFLAGS"], "-fPIC"].join(" ")
23
+ env["SUBDIRS"] = "include src" # libyaml: skip tests
24
+
25
+ recipe.configure_options += env.map { |key, value| "#{key}=#{value.strip}" }
26
+ end
26
27
  end
27
28
 
28
29
  unless File.exist?(File.join(recipe.target, recipe.host, recipe.name, recipe.version))
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RCEE
4
4
  module Precompiled
5
- VERSION = "0.1.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
@@ -1,17 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "lib/rcee/precompiled/version"
3
+ begin
4
+ require_relative "lib/rcee/precompiled/version"
5
+ rescue LoadError
6
+ puts "WARNING: Could not load RCEE::Precompiled::VERSION"
7
+ end
4
8
 
5
9
  Gem::Specification.new do |spec|
6
10
  spec.name = "rcee_precompiled"
7
- spec.version = RCEE::Precompiled::VERSION
11
+ spec.version = defined?(RCEE::Precompiled::VERSION) ? RCEE::Precompiled::VERSION : "0.0.0"
8
12
  spec.authors = ["Mike Dalessio"]
9
13
  spec.email = ["mike.dalessio@gmail.com"]
10
14
 
11
15
  spec.summary = "Example gem demonstrating a basic C extension."
12
16
  spec.description = "Part of a project to explain how Ruby C extensions work."
13
17
  spec.homepage = "https://github.com/flavorjones/ruby-c-extensions-explained"
14
- spec.required_ruby_version = ">= 2.5.0"
18
+ spec.required_ruby_version = ">= 2.6.0"
15
19
  spec.license = "MIT"
16
20
 
17
21
  # Specify which files should be added to the gem when it is released.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rcee_precompiled
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.4.0
5
5
  platform: x64-mingw32
6
6
  authors:
7
7
  - Mike Dalessio
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-09-02 00:00:00.000000000 Z
11
+ date: 2022-05-19 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Part of a project to explain how Ruby C extensions work.
14
14
  email:
@@ -25,7 +25,6 @@ files:
25
25
  - ext/precompiled/precompiled.c
26
26
  - ext/precompiled/precompiled.h
27
27
  - lib/rcee/precompiled.rb
28
- - lib/rcee/precompiled/2.5/precompiled.so
29
28
  - lib/rcee/precompiled/2.6/precompiled.so
30
29
  - lib/rcee/precompiled/2.7/precompiled.so
31
30
  - lib/rcee/precompiled/3.0/precompiled.so
@@ -35,7 +34,7 @@ homepage: https://github.com/flavorjones/ruby-c-extensions-explained
35
34
  licenses:
36
35
  - MIT
37
36
  metadata: {}
38
- post_install_message:
37
+ post_install_message:
39
38
  rdoc_options: []
40
39
  require_paths:
41
40
  - lib
@@ -43,7 +42,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
43
42
  requirements:
44
43
  - - ">="
45
44
  - !ruby/object:Gem::Version
46
- version: '2.5'
45
+ version: '2.6'
47
46
  - - "<"
48
47
  - !ruby/object:Gem::Version
49
48
  version: 3.1.dev
@@ -53,8 +52,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
52
  - !ruby/object:Gem::Version
54
53
  version: '0'
55
54
  requirements: []
56
- rubygems_version: 3.2.3
57
- signing_key:
55
+ rubygems_version: 3.3.4
56
+ signing_key:
58
57
  specification_version: 4
59
58
  summary: Example gem demonstrating a basic C extension.
60
59
  test_files: []