dockhand 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fb3565b6fe684a13404f48ae012095c671d1edd8b343cdae561b577dfec059e0
4
+ data.tar.gz: a848ca844f4e11abde42e535ba457a74469a86d4036c1a118a4607e7eb3e7b7c
5
+ SHA512:
6
+ metadata.gz: cc42d59510ccfd9e21b85cbdbcd448b907928e869b12dfa7a29eef7d04c0905ac9a711c9f5f1a6e4e21458f9e3efef86ac3f4c72674e9c18a2855b674d729d41
7
+ data.tar.gz: 34e9c0b08b8fe38a5b1c9b27ccbf74dc792466623922da5d5bae7a1e60afd362a1fb7b5b9cf69dd6beae61bb4a7957dea115253bc7312e8d3c7bb0b92d9a90ed
data/CHANGELOG.md ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in dockhand.gemspec
6
+ gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "minitest", "~> 5.0"
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Jonathan Hefner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # dockhand
2
+
3
+ Toolkit for building Rails app Docker images.
4
+
5
+
6
+ ## Installation
7
+
8
+ Install the [`dockhand` gem](https://rubygems.org/gems/dockhand).
9
+
10
+
11
+ ## Contributing
12
+
13
+ Run `rake test` to run the tests.
14
+
15
+
16
+ ## License
17
+
18
+ [MIT License](LICENSE.txt)
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
data/exe/dockhand ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "dockhand"
5
+ Dockhand::Command.start
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler"
4
+ require "fileutils"
5
+ require "json"
6
+ require "pathname"
7
+ require "shellwords"
8
+ require "tmpdir"
9
+ require "thor"
10
+
11
+ class Dockhand::Command < Thor
12
+ include Thor::Actions
13
+
14
+ desc "install-packages [PACKAGES...]",
15
+ "Install apt packages."
16
+ method_option :buildtime, type: :boolean,
17
+ desc: "Include buildtime packages (e.g. `build-essential`)."
18
+ method_option :gem_buildtime, type: :boolean,
19
+ desc: "Include gem-related buildtime packages (e.g. `libsqlite3-dev` if using SQLite)."
20
+ method_option :gem_runtime, type: :boolean,
21
+ desc: "Include gem-related runtime packages (e.g. `libsqlite3-0` if using SQLite)."
22
+ method_option :clean, type: :boolean,
23
+ desc: "Clean apt cache directories after installing packages."
24
+ def install_packages(*packages)
25
+ packages.concat(essential_buildtime_packages) if options[:buildtime]
26
+ packages.concat(gem_buildtime_packages) if options[:gem_buildtime]
27
+ packages.concat(gem_runtime_packages) if options[:gem_runtime]
28
+
29
+ unless packages.empty?
30
+ run "apt-get update -qq"
31
+ run "apt-get install --no-install-recommends --yes", *packages
32
+ end
33
+
34
+ FileUtils.rm_rf(["/var/cache/apt", "/var/lib/apt"]) if options[:clean]
35
+ end
36
+
37
+
38
+ desc "transmute-to-artifacts [PATHS...]",
39
+ "Move files and directories to an artifacts directory, and replace the originals with symlinks."
40
+ method_option :artifacts_dir, default: "/artifacts",
41
+ desc: "The artifacts directory."
42
+ def transmute_to_artifacts(*paths)
43
+ paths.each do |path|
44
+ path = File.expand_path(path)
45
+ artifacts_dir = File.expand_path(options[:artifacts_dir])
46
+ artifacts_subpath = "#{artifacts_dir}/#{path}"
47
+
48
+ FileUtils.mkdir_p(File.dirname(artifacts_subpath))
49
+ FileUtils.mv(path, artifacts_subpath)
50
+ FileUtils.ln_s(artifacts_subpath, path)
51
+ end
52
+ end
53
+
54
+
55
+ desc "install-gems",
56
+ "Install gems with Bundler."
57
+ method_option :clean, type: :boolean,
58
+ desc: "Clean Bundler cache after installing gems."
59
+ def install_gems
60
+ # Support for `BUNDLE_ONLY` was recently added in Bundler 2.4.0, but we can
61
+ # support `BUNDLE_ONLY` for older Bundler versions by converting the value
62
+ # to `BUNDLE_WITHOUT` (and updating `BUNDLE_WITH`).
63
+ #
64
+ # TODO Remove this hack when Bundler >= 2.4.0 is more widespread.
65
+ settings = [:without, :with].to_h { |key| [key, Bundler.settings[key].map(&:to_s)] }
66
+ only = Array(Bundler.settings[:only]).join(":").split(/\W/)
67
+ unless only.empty?
68
+ settings[:without] |= Bundler.definition.groups.map(&:to_s) - only
69
+ settings[:with] &= only
70
+ end
71
+
72
+ settings.each do |key, values|
73
+ run "bundle config set --local #{key} #{values.join(":").inspect}"
74
+ end
75
+ run "bundle install", env: { "BUNDLE_FROZEN" => "1" }
76
+ FileUtils.rm_rf("#{Bundler.bundle_path}/cache") if options[:clean]
77
+ end
78
+
79
+
80
+ desc "install-node",
81
+ "Install Node.js."
82
+ method_option :optional, type: :boolean,
83
+ desc: "Skips install if a .node-version or package.json file is not present."
84
+ method_option :prefix, default: "/usr/local",
85
+ desc: "The destination superdirectory. Files will be installed in `bin/`, `lib/`, etc."
86
+ def install_node
87
+ version_file = Dir["{.node-version,.nvmrc}"].first
88
+
89
+ if !version_file && !package_json.dig("engines", "node")
90
+ return if options[:optional] && !package_json_path
91
+ raise <<~ERROR
92
+ Missing Node.js version from `.node-version`, `.nvmrc`, or `package.json`.
93
+
94
+ You can create a version file by running the following command in the same directory as `package.json`:
95
+
96
+ $ node --version > .node-version
97
+ ERROR
98
+ end
99
+
100
+ Dir.mktmpdir do |tmp|
101
+ installer = "#{tmp}/n"
102
+ get "https://raw.githubusercontent.com/tj/n/HEAD/bin/n", installer
103
+ FileUtils.chmod("a+x", installer)
104
+ run installer, "lts" if !version_file && !which("node")
105
+ run installer, "auto", env: { "N_PREFIX" => options[:prefix] }
106
+ end
107
+ end
108
+
109
+
110
+ desc "install-node-modules",
111
+ "Install Node.js modules using Yarn, NPM, or PNPM."
112
+ method_option :optional, type: :boolean,
113
+ desc: "Skips install if a `yarn.lock`, `package-lock.json`, or `pnpm-lock.yaml` file is not present."
114
+ def install_node_modules
115
+ lock_file = Dir["{yarn.lock,package-lock.json,pnpm-lock.yaml}"].first
116
+
117
+ if !lock_file
118
+ return if options[:optional] && !package_json_path
119
+ raise "Missing Node.js modules lock file (`yarn.lock`, `package-lock.json`, or `pnpm-lock.yaml`)"
120
+ end
121
+
122
+ run "npm install --global corepack" unless which("corepack")
123
+ run "corepack enable"
124
+
125
+ case lock_file
126
+ when "yarn.lock"
127
+ run "yarn install --frozen-lockfile"
128
+ when "package-lock.json"
129
+ run "npm ci"
130
+ when "pnpm-lock.yaml"
131
+ run "pnpm install --frozen-lockfile"
132
+ end
133
+ end
134
+
135
+
136
+ desc "prepare-rails-app",
137
+ "Precompile assets, precompile code with Bootsnap, and normalize binstubs."
138
+ method_option :clean, type: :boolean,
139
+ desc: "Clean asset precompilation cache after precompiling."
140
+ def prepare_rails_app
141
+ Pathname.glob("bin/**/*") { |path| normalize_binstub(path) if path.file? }
142
+ run "bundle exec bootsnap precompile --gemfile app/ lib/" if gem?("bootsnap")
143
+ run "bin/rails assets:precompile", env: secret_key_base_dummy if rake_task?("assets:precompile")
144
+ FileUtils.rm_rf("tmp/cache/assets") if options[:clean]
145
+ end
146
+
147
+
148
+ desc "rails-entrypoint",
149
+ "Entrypoint for a Rails application."
150
+ def rails_entrypoint(*args)
151
+ if File.exist?("bin/docker-entrypoint")
152
+ exec("bin/docker-entrypoint", *args)
153
+ else
154
+ run "bin/rails db:prepare" if /\brails s(erver)?$/.match?(args[0..1].join(" "))
155
+ exec(*args)
156
+ end
157
+ end
158
+
159
+ private
160
+ GEM_RUNTIME_PACKAGES = {
161
+ "mysql2" => %w[default-mysql-client],
162
+ "pg" => %w[postgresql-client],
163
+ "ruby-vips" => %w[libvips],
164
+ "sqlite3" => %w[libsqlite3-0],
165
+ }
166
+
167
+ GEM_BUILDTIME_PACKAGES = GEM_RUNTIME_PACKAGES.merge(
168
+ "mysql2" => %w[default-libmysqlclient-dev],
169
+ "pg" => %w[libpq-dev],
170
+ "sqlite3" => %w[libsqlite3-dev],
171
+ )
172
+
173
+ def essential_buildtime_packages
174
+ %w[build-essential pkg-config git python-is-python3 curl]
175
+ end
176
+
177
+ def gem_buildtime_packages
178
+ GEM_BUILDTIME_PACKAGES.slice(*gems).values.flatten
179
+ end
180
+
181
+ def gem_runtime_packages
182
+ GEM_RUNTIME_PACKAGES.slice(*gems).values.flatten
183
+ end
184
+
185
+ def gems
186
+ @gems ||= Bundler.settings.temporary(frozen: true) do
187
+ Bundler.definition.resolve.for(Bundler.definition.requested_dependencies).map(&:name)
188
+ end
189
+ end
190
+
191
+ def gem?(name)
192
+ gems.include?(name)
193
+ end
194
+
195
+ def rails_version
196
+ Bundler.definition.specs.find { |spec| spec.name == "rails" }.version.to_s
197
+ end
198
+
199
+ def secret_key_base_dummy
200
+ if !ENV["SECRET_KEY_BASE"] && !ENV["RAILS_MASTER_KEY"] && !File.exist?("config/master.key")
201
+ rails_version < "7.1" ? { "SECRET_KEY_BASE" => "1" } : { "SECRET_KEY_BASE_DUMMY" => "1" }
202
+ end
203
+ end
204
+
205
+ def rake_task?(name)
206
+ !`rake --tasks '^#{name}$'`.empty?
207
+ end
208
+
209
+ def package_json_path
210
+ (@package_json_paths ||= Pathname.glob("{.,*}/package.json")).first
211
+ end
212
+
213
+ def package_json
214
+ @package_json ||= package_json_path ? JSON.load_file(package_json_path) : {}
215
+ end
216
+
217
+ def normalize_binstub(path)
218
+ path.open("r+") do |file|
219
+ shebang = file.read(2)
220
+
221
+ if shebang == "#!"
222
+ shebang << file.gets.chomp!
223
+ if shebang.include?("ruby")
224
+ shebang = "#!/usr/bin/env #{File.basename Thor::Util.ruby_command}"
225
+ end
226
+
227
+ content = file.read
228
+ content.delete!("\r")
229
+
230
+ file.rewind
231
+ file.truncate(file.write(shebang, "\n", content))
232
+ path.chmod(0755 & ~File.umask)
233
+ end
234
+ end
235
+ end
236
+
237
+ def which(bin)
238
+ path = `sh -c 'command -v #{bin}'`
239
+ path unless path.empty?
240
+ end
241
+
242
+ def run(*cmd, env: nil)
243
+ cmd[0] = Shellwords.split(cmd[0]) if cmd[0].include?(" ")
244
+ cmd.flatten!
245
+ cmd.compact!
246
+ cmd.unshift(env) if env
247
+ system(*cmd, exception: true)
248
+ end
249
+
250
+ def self.exit_on_failure?
251
+ true
252
+ end
253
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dockhand
4
+ VERSION = "0.1.0"
5
+ end
data/lib/dockhand.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "dockhand/version"
4
+ require_relative "dockhand/command"
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dockhand
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Hefner
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-03-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.2'
27
+ description:
28
+ email:
29
+ - jonathan@hefner.pro
30
+ executables:
31
+ - dockhand
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - CHANGELOG.md
36
+ - Gemfile
37
+ - LICENSE.txt
38
+ - README.md
39
+ - Rakefile
40
+ - exe/dockhand
41
+ - lib/dockhand.rb
42
+ - lib/dockhand/command.rb
43
+ - lib/dockhand/version.rb
44
+ homepage: https://github.com/jonathanhefner/dockhand
45
+ licenses:
46
+ - MIT
47
+ metadata:
48
+ homepage_uri: https://github.com/jonathanhefner/dockhand
49
+ source_code_uri: https://github.com/jonathanhefner/dockhand
50
+ changelog_uri: https://github.com/jonathanhefner/dockhand/blob/HEAD/CHANGELOG.md
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '2.7'
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: '0'
65
+ requirements: []
66
+ rubygems_version: 3.4.1
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: Toolkit for building Rails app Docker images
70
+ test_files: []