scint 0.7.0 → 0.7.1

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.
@@ -4,9 +4,16 @@ require_relative "../downloader/pool"
4
4
  require_relative "../gem/package"
5
5
  require_relative "../gem/extractor"
6
6
  require_relative "../cache/layout"
7
+ require_relative "../cache/manifest"
8
+ require_relative "../cache/validity"
7
9
  require_relative "../fs"
8
10
  require_relative "../errors"
9
11
  require_relative "../spec_utils"
12
+ require_relative "../platform"
13
+ require_relative "./promoter"
14
+ require_relative "./extension_builder"
15
+ require_relative "../source/git"
16
+ require_relative "../source/path"
10
17
 
11
18
  module Scint
12
19
  module Installer
@@ -45,21 +52,30 @@ module Scint
45
52
  already_cached = []
46
53
 
47
54
  sorted.each do |entry|
48
- inbound = @layout.inbound_path(entry.spec)
49
- extracted = @layout.extracted_path(entry.spec)
55
+ spec = entry.spec
56
+ inbound = @layout.inbound_path(spec)
57
+ assembling = @layout.assembling_path(spec)
58
+ cached = @layout.cached_path(spec)
50
59
 
51
- if File.directory?(extracted)
52
- # Already extracted -- load gemspec from cache or re-read
53
- gemspec = load_cached_spec(entry.spec) || read_gemspec_from_extracted(extracted, entry.spec)
60
+ if Cache::Validity.cached_valid?(spec, @layout)
61
+ gemspec = load_cached_spec(spec) || read_gemspec_from_extracted(cached, spec)
54
62
  already_cached << PreparedGem.new(
55
- spec: entry.spec,
56
- extracted_path: extracted,
63
+ spec: spec,
64
+ extracted_path: cached,
57
65
  gemspec: gemspec,
58
66
  from_cache: true,
59
67
  )
68
+ elsif File.directory?(assembling)
69
+ gemspec = read_gemspec_from_extracted(assembling, spec)
70
+ already_cached << PreparedGem.new(
71
+ spec: spec,
72
+ extracted_path: assembling,
73
+ gemspec: gemspec,
74
+ from_cache: false,
75
+ )
60
76
  elsif File.exist?(inbound)
61
77
  # Downloaded but not extracted
62
- already_cached << extract_gem(entry.spec, inbound)
78
+ already_cached << extract_gem(spec, inbound)
63
79
  else
64
80
  # Need to download
65
81
  to_download << entry
@@ -105,65 +121,98 @@ module Scint
105
121
 
106
122
  # Prepare a single entry (for use with scheduler).
107
123
  def prepare_one(entry)
108
- inbound = @layout.inbound_path(entry.spec)
109
- extracted = @layout.extracted_path(entry.spec)
124
+ spec = entry.spec
125
+ inbound = @layout.inbound_path(spec)
126
+ assembling = @layout.assembling_path(spec)
127
+ cached = @layout.cached_path(spec)
110
128
 
111
- if File.directory?(extracted)
112
- gemspec = load_cached_spec(entry.spec) || read_gemspec_from_extracted(extracted, entry.spec)
129
+ if Cache::Validity.cached_valid?(spec, @layout)
130
+ gemspec = load_cached_spec(spec) || read_gemspec_from_extracted(cached, spec)
113
131
  return PreparedGem.new(
114
- spec: entry.spec,
115
- extracted_path: extracted,
132
+ spec: spec,
133
+ extracted_path: cached,
116
134
  gemspec: gemspec,
117
135
  from_cache: true,
118
136
  )
119
137
  end
120
138
 
139
+ if File.directory?(assembling)
140
+ gemspec = read_gemspec_from_extracted(assembling, spec)
141
+ return PreparedGem.new(
142
+ spec: spec,
143
+ extracted_path: assembling,
144
+ gemspec: gemspec,
145
+ from_cache: false,
146
+ )
147
+ end
148
+
149
+
121
150
  unless File.exist?(inbound)
122
151
  uri = gem_download_uri(entry)
123
152
  @download_pool.download(uri, inbound)
124
153
  end
125
154
 
126
- extract_gem(entry.spec, inbound)
155
+ extract_gem(spec, inbound)
127
156
  end
128
157
 
129
158
  private
130
159
 
131
160
  def extract_gem(spec, gem_path)
132
- dest = @layout.extracted_path(spec)
161
+ cached = @layout.cached_path(spec)
162
+ assembling = @layout.assembling_path(spec)
163
+
164
+ if Cache::Validity.cached_valid?(spec, @layout)
165
+ gemspec = load_cached_spec(spec) || read_gemspec_from_extracted(cached, spec)
166
+ return PreparedGem.new(spec: spec, extracted_path: cached, gemspec: gemspec, from_cache: true)
167
+ end
133
168
 
134
- if File.directory?(dest)
135
- gemspec = load_cached_spec(spec) || read_gemspec_from_extracted(dest, spec)
136
- return PreparedGem.new(spec: spec, extracted_path: dest, gemspec: gemspec, from_cache: true)
169
+ if File.directory?(assembling)
170
+ gemspec = read_gemspec_from_extracted(assembling, spec)
171
+ return PreparedGem.new(spec: spec, extracted_path: assembling, gemspec: gemspec, from_cache: false)
137
172
  end
138
173
 
139
- # Extract to temp dir, then atomic move
140
- tmp_dest = "#{dest}.#{Process.pid}.tmp"
174
+ # Extract to temp dir in assembling, then atomic move
175
+ tmp_dest = "#{assembling}.#{Process.pid}.tmp"
141
176
  FileUtils.rm_rf(tmp_dest) if File.exist?(tmp_dest)
142
177
 
143
178
  result = @package.extract(gem_path, tmp_dest)
144
- FS.atomic_move(tmp_dest, dest)
145
-
146
- # Cache the gemspec as Marshal for fast future loads
147
- cache_spec(spec, result[:gemspec])
179
+ FS.atomic_move(tmp_dest, assembling)
148
180
 
149
- PreparedGem.new(
150
- spec: spec,
151
- extracted_path: dest,
152
- gemspec: result[:gemspec],
153
- from_cache: false,
154
- )
181
+ if ExtensionBuilder.needs_build?(spec, assembling)
182
+ PreparedGem.new(
183
+ spec: spec,
184
+ extracted_path: assembling,
185
+ gemspec: result[:gemspec],
186
+ from_cache: false,
187
+ )
188
+ else
189
+ promote_assembled(spec, assembling, result[:gemspec])
190
+ PreparedGem.new(
191
+ spec: spec,
192
+ extracted_path: @layout.cached_path(spec),
193
+ gemspec: result[:gemspec],
194
+ from_cache: false,
195
+ )
196
+ end
155
197
  end
156
198
 
157
199
  def load_cached_spec(spec)
158
- path = @layout.spec_cache_path(spec)
200
+ path = @layout.cached_spec_path(spec)
159
201
  return nil unless File.exist?(path)
160
- Marshal.load(File.binread(path))
161
- rescue ArgumentError, TypeError, EOFError
202
+
203
+ data = File.binread(path)
204
+ if data.start_with?("---")
205
+ data.force_encoding("UTF-8") if data.encoding != Encoding::UTF_8
206
+ return Gem::Specification.from_yaml(data)
207
+ end
208
+
209
+ Marshal.load(data)
210
+ rescue ArgumentError, TypeError, EOFError, StandardError
162
211
  nil
163
212
  end
164
213
 
165
214
  def cache_spec(spec, gemspec)
166
- path = @layout.spec_cache_path(spec)
215
+ path = @layout.cached_spec_path(spec)
167
216
  FS.atomic_write(path, Marshal.dump(gemspec))
168
217
  rescue StandardError
169
218
  # Non-fatal: cache miss on next load
@@ -173,10 +222,91 @@ module Scint
173
222
  pattern = File.join(extracted_dir, "*.gemspec")
174
223
  candidates = Dir.glob(pattern)
175
224
  if candidates.any?
225
+ version = spec.respond_to?(:version) ? spec.version.to_s : nil
226
+ old_version = ENV["VERSION"]
176
227
  begin
177
- ::Gem::Specification.load(candidates.first)
178
- rescue StandardError
228
+ ENV["VERSION"] = version if version && !ENV["VERSION"]
229
+ SpecUtils.load_gemspec(candidates.first, isolate: true)
230
+ rescue SystemExit, StandardError
179
231
  nil
232
+ ensure
233
+ ENV["VERSION"] = old_version
234
+ end
235
+ end
236
+ end
237
+
238
+ def promote_assembled(spec, assembling_path, gemspec)
239
+ return unless assembling_path && Dir.exist?(assembling_path)
240
+
241
+ cached_dir = @layout.cached_path(spec)
242
+ promoter = Promoter.new(root: @layout.root)
243
+ lock_key = "#{Platform.abi_key}-#{@layout.full_name(spec)}"
244
+
245
+ promoter.validate_within_root!(@layout.root, assembling_path, label: "assembling")
246
+ promoter.validate_within_root!(@layout.root, cached_dir, label: "cached")
247
+
248
+ result = nil
249
+ promoter.with_staging_dir(prefix: "cached") do |staging|
250
+ FS.clone_tree(assembling_path, staging)
251
+ manifest = build_cached_manifest(spec, staging)
252
+ spec_payload = gemspec ? Marshal.dump(gemspec) : nil
253
+ result = promoter.promote_tree(
254
+ staging_path: staging,
255
+ target_path: cached_dir,
256
+ lock_key: lock_key,
257
+ )
258
+ if result == :promoted
259
+ write_cached_metadata(spec, spec_payload, manifest)
260
+ end
261
+ end
262
+ FileUtils.rm_rf(assembling_path) if Dir.exist?(assembling_path)
263
+ result
264
+ rescue StandardError
265
+ FileUtils.rm_rf(cached_dir) if Dir.exist?(cached_dir)
266
+ raise
267
+ end
268
+
269
+ def write_cached_metadata(spec, spec_payload, manifest)
270
+ spec_path = @layout.cached_spec_path(spec)
271
+ manifest_path = @layout.cached_manifest_path(spec)
272
+ FS.mkdir_p(File.dirname(spec_path))
273
+
274
+ FS.atomic_write(spec_path, spec_payload) if spec_payload
275
+ Cache::Manifest.write(manifest_path, manifest)
276
+ end
277
+
278
+ def build_cached_manifest(spec, cached_dir)
279
+ Cache::Manifest.build(
280
+ spec: spec,
281
+ gem_dir: cached_dir,
282
+ abi_key: Platform.abi_key,
283
+ source: manifest_source_for(spec),
284
+ extensions: ExtensionBuilder.needs_build?(spec, cached_dir),
285
+ )
286
+ end
287
+
288
+ def manifest_source_for(spec)
289
+ source = spec.source
290
+ if source.is_a?(Source::Git)
291
+ {
292
+ "type" => "git",
293
+ "uri" => source.uri.to_s,
294
+ "revision" => source.revision || source.ref || source.branch || source.tag,
295
+ }.compact
296
+ elsif source.is_a?(Source::Path)
297
+ {
298
+ "type" => "path",
299
+ "path" => File.expand_path(source.path.to_s),
300
+ "uri" => source.path.to_s,
301
+ }
302
+ else
303
+ source_str = source.to_s
304
+ if source_str.start_with?("http://", "https://")
305
+ { "type" => "rubygems", "uri" => source_str }
306
+ elsif source_str.start_with?("/", ".", "~")
307
+ { "type" => "path", "path" => File.expand_path(source_str), "uri" => source_str }
308
+ else
309
+ { "type" => "rubygems", "uri" => source_str }
180
310
  end
181
311
  end
182
312
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+ require_relative "../fs"
5
+ require "fileutils"
6
+ require "securerandom"
7
+
8
+ module Scint
9
+ module Installer
10
+ class Promoter
11
+ attr_reader :root, :lock_dir, :staging_dir
12
+
13
+ def initialize(root:, lock_dir: nil, staging_dir: nil)
14
+ @root = File.expand_path(root.to_s)
15
+ @lock_dir = File.expand_path(lock_dir.to_s) if lock_dir
16
+ @staging_dir = File.expand_path(staging_dir.to_s) if staging_dir
17
+
18
+ @lock_dir ||= File.join(@root, "locks", "promotion")
19
+ @staging_dir ||= File.join(@root, "staging")
20
+ end
21
+
22
+ def with_lock(lock_key)
23
+ lock_path = lock_path_for(lock_key)
24
+ validate_within_root!(@root, lock_path, label: "lock")
25
+ FS.mkdir_p(File.dirname(lock_path))
26
+
27
+ File.open(lock_path, "w") do |file|
28
+ file.flock(File::LOCK_EX)
29
+ yield
30
+ ensure
31
+ file.flock(File::LOCK_UN) rescue nil
32
+ end
33
+ end
34
+
35
+ def with_staging_dir(prefix:)
36
+ FS.mkdir_p(@staging_dir)
37
+ staging_path = File.join(@staging_dir, staging_suffix(prefix))
38
+ validate_within_root!(@root, staging_path, label: "staging")
39
+ FileUtils.rm_rf(staging_path) if File.exist?(staging_path)
40
+
41
+ begin
42
+ FS.mkdir_p(staging_path)
43
+ yield staging_path
44
+ ensure
45
+ FileUtils.rm_rf(staging_path) if Dir.exist?(staging_path)
46
+ end
47
+ end
48
+
49
+ def promote_tree(staging_path:, target_path:, lock_key:)
50
+ validate_within_root!(@root, staging_path, label: "staging")
51
+ validate_within_root!(@root, target_path, label: "target")
52
+ raise CacheError, "Staging path does not exist: #{staging_path}" unless Dir.exist?(staging_path)
53
+
54
+ with_lock(lock_key) do
55
+ if Dir.exist?(target_path)
56
+ FileUtils.rm_rf(staging_path) if Dir.exist?(staging_path)
57
+ return :exists
58
+ end
59
+
60
+ begin
61
+ FS.atomic_move(staging_path, target_path)
62
+ rescue StandardError
63
+ FileUtils.rm_rf(staging_path) if Dir.exist?(staging_path)
64
+ raise
65
+ end
66
+
67
+ :promoted
68
+ end
69
+ end
70
+
71
+ def validate_within_root!(root_path, candidate_path, label: "path")
72
+ root_expanded = File.expand_path(root_path.to_s)
73
+ candidate_expanded = File.expand_path(candidate_path.to_s)
74
+ within_root = candidate_expanded == root_expanded ||
75
+ candidate_expanded.start_with?("#{root_expanded}/")
76
+ return if within_root
77
+
78
+ raise CacheError, "#{label.capitalize} escapes cache root: #{candidate_path}"
79
+ end
80
+
81
+ def lock_path_for(lock_key)
82
+ safe_key = sanitize_key(lock_key)
83
+ File.join(@lock_dir, "#{safe_key}.lock")
84
+ end
85
+
86
+ def sanitize_key(key)
87
+ key.to_s.gsub(/[^0-9A-Za-z._-]/, "_")
88
+ end
89
+
90
+ def staging_suffix(prefix)
91
+ safe = sanitize_key(prefix)
92
+ token = SecureRandom.hex(6)
93
+ "#{safe}.#{Process.pid}.#{Thread.current.object_id}.#{token}"
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,137 @@
1
+ #!/bin/bash
2
+ # scint-linker — bulk-hardlink cached gems into a bundle.
3
+ #
4
+ # Protocol (stdin):
5
+ # Line 1: source parent dir (e.g. ~/.cache/scint/cached/ruby-3.4.7-x86_64-linux)
6
+ # Line 2: dest parent dir (e.g. .bundle/ruby/3.4.0/gems)
7
+ # Remaining lines: gem directory basenames (e.g. "rack-3.2.4")
8
+ #
9
+ # The script probes the best copy strategy once (cpio, hardlink, reflink,
10
+ # copy) and uses it for every gem. When cpio is available it reads
11
+ # .scint-files from each cached gem so only listed files are materialized.
12
+ # Otherwise falls back to cp -al / cp --reflink / cp -R.
13
+
14
+ set -euo pipefail
15
+
16
+ read -r SRC
17
+ read -r DST
18
+ mkdir -p "$DST"
19
+
20
+ # ── detect strategy ──────────────────────────────────────────────
21
+ STRATEGY=""
22
+
23
+ # Probe needs a real file to test against. Find one quickly.
24
+ probe_src=""
25
+ for candidate in "$SRC"/*/.scint-files; do
26
+ [ -f "$candidate" ] && probe_src="$candidate" && break
27
+ done
28
+
29
+ if [ -z "$probe_src" ]; then
30
+ # No .scint-files at all — fall back to cp -al or cp -R
31
+ if cp -al --version >/dev/null 2>&1; then
32
+ STRATEGY="cp-al"
33
+ else
34
+ STRATEGY="cp"
35
+ fi
36
+ else
37
+ probe_dst="$DST/.scint-probe-$$"
38
+
39
+ # 1. cpio -pld (file-list driven hardlinks — ideal)
40
+ if command -v cpio >/dev/null 2>&1; then
41
+ if echo "$probe_src" | cpio -pld "$DST" >/dev/null 2>&1; then
42
+ rm -f "$probe_dst" 2>/dev/null
43
+ STRATEGY="cpio"
44
+ fi
45
+ fi
46
+
47
+ # 2. hardlink via ln
48
+ if [ -z "$STRATEGY" ]; then
49
+ if ln "$probe_src" "$probe_dst" 2>/dev/null; then
50
+ rm -f "$probe_dst"
51
+ STRATEGY="cp-al"
52
+ fi
53
+ fi
54
+
55
+ # 3. reflink (btrfs/xfs/APFS)
56
+ if [ -z "$STRATEGY" ]; then
57
+ if [ "$(uname)" = "Darwin" ]; then
58
+ if cp -c "$probe_src" "$probe_dst" 2>/dev/null; then
59
+ rm -f "$probe_dst"
60
+ STRATEGY="reflink"
61
+ fi
62
+ else
63
+ if cp --reflink=always "$probe_src" "$probe_dst" 2>/dev/null; then
64
+ rm -f "$probe_dst"
65
+ STRATEGY="reflink"
66
+ fi
67
+ fi
68
+ fi
69
+
70
+ rm -f "$probe_dst" 2>/dev/null
71
+ [ -z "$STRATEGY" ] && STRATEGY="cp"
72
+ fi
73
+
74
+ # ── link gems ────────────────────────────────────────────────────
75
+
76
+ link_cpio() {
77
+ # Read .scint-files, prefix each line with gem name, pipe to cpio -pld.
78
+ # One cpio call per gem (cpio needs a single source root).
79
+ local gem="$1"
80
+ local dotfiles="$SRC/$gem/.scint-files"
81
+ if [ -f "$dotfiles" ]; then
82
+ (cd "$SRC/$gem" && cpio -pld "$DST/$gem" < "$dotfiles" 2>/dev/null)
83
+ else
84
+ cp -al "$SRC/$gem" "$DST/$gem"
85
+ fi
86
+ }
87
+
88
+ # For cp-al: batch all gems into one call
89
+ BATCH=()
90
+
91
+ flush_batch() {
92
+ [ ${#BATCH[@]} -eq 0 ] && return
93
+ case "$STRATEGY" in
94
+ cp-al)
95
+ cp -al "${BATCH[@]}" "$DST/" 2>/dev/null || {
96
+ # Individual fallback on batch failure
97
+ for s in "${BATCH[@]}"; do
98
+ cp -al "$s" "$DST/" 2>/dev/null || cp -R "$s" "$DST/"
99
+ done
100
+ }
101
+ ;;
102
+ reflink)
103
+ if [ "$(uname)" = "Darwin" ]; then
104
+ cp -cR "${BATCH[@]}" "$DST/" 2>/dev/null || {
105
+ for s in "${BATCH[@]}"; do cp -R "$s" "$DST/"; done
106
+ }
107
+ else
108
+ cp --reflink=always -R "${BATCH[@]}" "$DST/" 2>/dev/null || {
109
+ for s in "${BATCH[@]}"; do cp -R "$s" "$DST/"; done
110
+ }
111
+ fi
112
+ ;;
113
+ cp)
114
+ cp -R "${BATCH[@]}" "$DST/"
115
+ ;;
116
+ esac
117
+ BATCH=()
118
+ }
119
+
120
+ while IFS= read -r gem; do
121
+ [ -z "$gem" ] && continue
122
+ [ -d "$SRC/$gem" ] || continue
123
+ [ -d "$DST/$gem" ] && continue
124
+
125
+ case "$STRATEGY" in
126
+ cpio)
127
+ link_cpio "$gem"
128
+ ;;
129
+ *)
130
+ BATCH+=("$SRC/$gem")
131
+ # Flush in chunks to stay under ARG_MAX
132
+ [ ${#BATCH[@]} -ge 200 ] && flush_batch
133
+ ;;
134
+ esac
135
+ done
136
+
137
+ flush_batch
@@ -1,9 +1,28 @@
1
1
  # frozen_string_literal: true
2
+ require "open3"
3
+ require "rbconfig"
2
4
 
3
5
  module Scint
4
6
  module SpecUtils
5
7
  module_function
6
8
 
9
+ GEMSPEC_LOAD_MUTEX = Thread::Mutex.new
10
+
11
+ GEMSPEC_SUBPROCESS_LOADER = <<~'RUBY'
12
+ path = ARGV.fetch(0)
13
+ begin
14
+ Dir.chdir(File.dirname(path)) do
15
+ spec = Gem::Specification.load(path)
16
+ exit(2) unless spec
17
+ STDOUT.binmode
18
+ STDOUT.write(Marshal.dump(spec))
19
+ end
20
+ rescue Exception => e
21
+ warn("#{e.class}: #{e.message}")
22
+ exit(1)
23
+ end
24
+ RUBY
25
+
7
26
  def name(spec)
8
27
  spec.respond_to?(:name) ? spec.name : spec[:name]
9
28
  end
@@ -54,5 +73,61 @@ module Scint
54
73
  def full_key_for(name, version, platform = "ruby")
55
74
  "#{name}-#{version}-#{platform_value(platform)}"
56
75
  end
76
+
77
+ # Load a gemspec with path-sensitive fallback.
78
+ #
79
+ # Some gemspecs depend on process cwd (e.g. `require "./lib/..."`,
80
+ # `File.read("README")`). A direct load can fail when evaluated from a
81
+ # different working directory. For those failures, evaluate in a dedicated
82
+ # subprocess that can safely chdir without affecting install worker threads.
83
+ def load_gemspec(path, isolate: false)
84
+ absolute = File.expand_path(path.to_s)
85
+ return nil unless File.file?(absolute)
86
+ return load_gemspec_in_subprocess(absolute) if isolate
87
+
88
+ spec = load_gemspec_direct(absolute)
89
+ return spec if spec
90
+
91
+ load_gemspec_in_subprocess(absolute)
92
+ rescue StandardError, ScriptError => e
93
+ return load_gemspec_in_subprocess(absolute) if gemspec_path_context_error?(e)
94
+
95
+ nil
96
+ end
97
+
98
+ def gemspec_path_context_error?(error)
99
+ return true if error.is_a?(LoadError)
100
+ return true if error.is_a?(Errno::ENOENT)
101
+
102
+ message = error.message.to_s
103
+ message.include?("conflicting chdir during another chdir block") ||
104
+ message.include?("cannot load such file -- ./") ||
105
+ message.include?("cannot load such file -- ../")
106
+ end
107
+ private_class_method :gemspec_path_context_error?
108
+
109
+ def load_gemspec_direct(absolute_path)
110
+ GEMSPEC_LOAD_MUTEX.synchronize do
111
+ ::Gem::Specification.load(absolute_path)
112
+ end
113
+ end
114
+ private_class_method :load_gemspec_direct
115
+
116
+ def load_gemspec_in_subprocess(absolute_path)
117
+ out, _err, status = Open3.capture3(
118
+ RbConfig.ruby,
119
+ "-rrubygems",
120
+ "-e",
121
+ GEMSPEC_SUBPROCESS_LOADER,
122
+ absolute_path,
123
+ )
124
+ return nil unless status.success?
125
+ return nil if out.nil? || out.empty?
126
+
127
+ Marshal.load(out)
128
+ rescue StandardError
129
+ nil
130
+ end
131
+ private_class_method :load_gemspec_in_subprocess
57
132
  end
58
133
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobi Lutke
@@ -27,8 +27,10 @@ files:
27
27
  - lib/bundler/setup.rb
28
28
  - lib/scint.rb
29
29
  - lib/scint/cache/layout.rb
30
+ - lib/scint/cache/manifest.rb
30
31
  - lib/scint/cache/metadata_store.rb
31
32
  - lib/scint/cache/prewarm.rb
33
+ - lib/scint/cache/validity.rb
32
34
  - lib/scint/cli.rb
33
35
  - lib/scint/cli/add.rb
34
36
  - lib/scint/cli/cache.rb
@@ -56,6 +58,8 @@ files:
56
58
  - lib/scint/installer/linker.rb
57
59
  - lib/scint/installer/planner.rb
58
60
  - lib/scint/installer/preparer.rb
61
+ - lib/scint/installer/promoter.rb
62
+ - lib/scint/linker.sh
59
63
  - lib/scint/lockfile/parser.rb
60
64
  - lib/scint/lockfile/writer.rb
61
65
  - lib/scint/platform.rb