scint 0.1.0 → 0.6.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 +4 -4
- data/VERSION +1 -0
- data/lib/scint/cli/install.rb +206 -36
- data/lib/scint/fs.rb +46 -1
- data/lib/scint/index/client.rb +1 -1
- data/lib/scint/installer/extension_builder.rb +37 -2
- data/lib/scint/installer/linker.rb +1 -1
- data/lib/scint/installer/planner.rb +7 -6
- data/lib/scint/lockfile/writer.rb +1 -7
- data/lib/scint/progress.rb +128 -73
- data/lib/scint/resolver/provider.rb +56 -5
- data/lib/scint/vendor/pub_grub/version.rb +5 -1
- data/lib/scint/version.rb +5 -0
- data/lib/scint.rb +2 -2
- metadata +4 -7
- data/bin/bundler-vs-scint +0 -233
- data/bin/scint-io-summary +0 -46
- data/bin/scint-syscall-trace +0 -41
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1c0c2f09127da28473aad491848c5b3e7b976fa41405a94ce542859fa1aabf8
|
|
4
|
+
data.tar.gz: 2e7b1801b9a3004bc0d9709de9502bfff5a8b475178aa102ebb730a6cb1b6344
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3d95e5006966b2025c5e21951bc918849081c330a2872e7e665cafa6d73ab522871f7e41aa2ed4c61bbed37e73363f19650dce16bbc1e472b3e4fe54c0177d97
|
|
7
|
+
data.tar.gz: 40fba5802be2359b85d201c2c732f8edac15f1e1be7ee17b2d59a39de9ac2f1ca6befd14d527fcacb4a5e763408c1296abe35301f8224a4b1138a5a4a1706096
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.6.0
|
data/lib/scint/cli/install.rb
CHANGED
|
@@ -52,10 +52,13 @@ module Scint
|
|
|
52
52
|
|
|
53
53
|
cache = Scint::Cache::Layout.new
|
|
54
54
|
bundle_path = @path || ENV["BUNDLER_PATH"] || ".bundle"
|
|
55
|
+
bundle_display = display_bundle_path(bundle_path)
|
|
55
56
|
bundle_path = File.expand_path(bundle_path)
|
|
56
57
|
worker_count = @jobs || [Platform.cpu_count * 2, 50].min
|
|
57
58
|
compile_slots = compile_slots_for(worker_count)
|
|
58
59
|
per_type_limits = install_task_limits(worker_count, compile_slots)
|
|
60
|
+
$stdout.puts "#{GREEN}💎#{RESET} Scintellating Gemfile into #{BOLD}#{bundle_display}#{RESET} #{DIM}(scint #{VERSION}, ruby #{RUBY_VERSION})#{RESET}"
|
|
61
|
+
$stdout.puts
|
|
59
62
|
|
|
60
63
|
# 0. Build credential store from config files (~/.bundle/config, XDG scint/credentials)
|
|
61
64
|
@credentials = Credentials.new
|
|
@@ -116,8 +119,9 @@ module Scint
|
|
|
116
119
|
|
|
117
120
|
if to_install.empty?
|
|
118
121
|
elapsed_ms = elapsed_ms_since(start_time)
|
|
122
|
+
worker_count = scheduler.stats[:workers]
|
|
119
123
|
warn_missing_bundle_gitignore_entry
|
|
120
|
-
$stdout.puts "\n#{GREEN}#{total_gems}#{RESET} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems)}. #{DIM}(#{
|
|
124
|
+
$stdout.puts "\n#{GREEN}#{total_gems}#{RESET} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems)}. #{DIM}(#{format_run_footer(elapsed_ms, worker_count)})#{RESET}"
|
|
121
125
|
return 0
|
|
122
126
|
end
|
|
123
127
|
|
|
@@ -151,6 +155,7 @@ module Scint
|
|
|
151
155
|
end
|
|
152
156
|
|
|
153
157
|
elapsed_ms = elapsed_ms_since(start_time)
|
|
158
|
+
worker_count = stats[:workers]
|
|
154
159
|
failed = errors.filter_map { |e| e[:name] }.uniq
|
|
155
160
|
failed_count = failed.size
|
|
156
161
|
failed_count = 1 if failed_count.zero? && stats[:failed] > 0
|
|
@@ -159,14 +164,14 @@ module Scint
|
|
|
159
164
|
|
|
160
165
|
if has_failures
|
|
161
166
|
warn_missing_bundle_gitignore_entry
|
|
162
|
-
$stdout.puts "\n#{RED}Bundle failed!#{RESET} #{installed_total}/#{total_gems} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems, compiled: compiled_gems, failed: failed_count)}. #{DIM}(#{
|
|
167
|
+
$stdout.puts "\n#{RED}Bundle failed!#{RESET} #{installed_total}/#{total_gems} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems, compiled: compiled_gems, failed: failed_count)}. #{DIM}(#{format_run_footer(elapsed_ms, worker_count)})#{RESET}"
|
|
163
168
|
1
|
|
164
169
|
else
|
|
165
170
|
# 10. Write lockfile + runtime config only for successful installs
|
|
166
171
|
write_lockfile(resolved, gemfile)
|
|
167
172
|
write_runtime_config(resolved, bundle_path)
|
|
168
173
|
warn_missing_bundle_gitignore_entry
|
|
169
|
-
$stdout.puts "\n#{GREEN}#{total_gems}#{RESET} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems, compiled: compiled_gems)}. #{DIM}(#{
|
|
174
|
+
$stdout.puts "\n#{GREEN}#{total_gems}#{RESET} gems installed total#{install_breakdown(cached: cached_gems, updated: updated_gems, compiled: compiled_gems)}. #{DIM}(#{format_run_footer(elapsed_ms, worker_count)})#{RESET}"
|
|
170
175
|
0
|
|
171
176
|
end
|
|
172
177
|
ensure
|
|
@@ -194,7 +199,7 @@ module Scint
|
|
|
194
199
|
remote_uri: nil,
|
|
195
200
|
checksum: nil,
|
|
196
201
|
)
|
|
197
|
-
resolved
|
|
202
|
+
resolved.unshift(scint_spec)
|
|
198
203
|
|
|
199
204
|
resolved
|
|
200
205
|
end
|
|
@@ -221,7 +226,7 @@ module Scint
|
|
|
221
226
|
lib_dest = File.join(gem_dest, "lib")
|
|
222
227
|
unless Dir.exist?(lib_dest)
|
|
223
228
|
FS.mkdir_p(lib_dest)
|
|
224
|
-
FS.
|
|
229
|
+
FS.clone_tree(scint_root, lib_dest)
|
|
225
230
|
end
|
|
226
231
|
|
|
227
232
|
# Write gemspec
|
|
@@ -255,11 +260,12 @@ module Scint
|
|
|
255
260
|
def clone_git_source(source, cache)
|
|
256
261
|
return unless source.respond_to?(:uri)
|
|
257
262
|
git_dir = cache.git_path(source.uri)
|
|
258
|
-
|
|
263
|
+
if Dir.exist?(git_dir)
|
|
264
|
+
fetch_git_repo(git_dir)
|
|
265
|
+
return
|
|
266
|
+
end
|
|
259
267
|
|
|
260
|
-
|
|
261
|
-
system("git", "clone", "--bare", source.uri.to_s, git_dir,
|
|
262
|
-
[:out, :err] => File::NULL)
|
|
268
|
+
clone_git_repo(source.uri, git_dir)
|
|
263
269
|
end
|
|
264
270
|
|
|
265
271
|
def resolve(gemfile, lockfile, cache)
|
|
@@ -376,8 +382,16 @@ module Scint
|
|
|
376
382
|
end
|
|
377
383
|
|
|
378
384
|
def lockfile_to_resolved(lockfile)
|
|
379
|
-
|
|
380
|
-
|
|
385
|
+
local_plat = Platform.local_platform
|
|
386
|
+
|
|
387
|
+
# Pick one best platform variant per gem+version from lockfile specs.
|
|
388
|
+
by_gem = Hash.new { |h, k| h[k] = [] }
|
|
389
|
+
lockfile.specs.each { |ls| by_gem[[ls[:name], ls[:version]]] << ls }
|
|
390
|
+
|
|
391
|
+
resolved = by_gem.map do |(_name, _version), specs|
|
|
392
|
+
best = pick_best_platform_spec(specs, local_plat)
|
|
393
|
+
|
|
394
|
+
source = best[:source]
|
|
381
395
|
source_value =
|
|
382
396
|
if source.is_a?(Source::Rubygems)
|
|
383
397
|
source.uri.to_s
|
|
@@ -386,16 +400,96 @@ module Scint
|
|
|
386
400
|
end
|
|
387
401
|
|
|
388
402
|
ResolvedSpec.new(
|
|
389
|
-
name:
|
|
390
|
-
version:
|
|
391
|
-
platform:
|
|
392
|
-
dependencies:
|
|
403
|
+
name: best[:name],
|
|
404
|
+
version: best[:version],
|
|
405
|
+
platform: best[:platform],
|
|
406
|
+
dependencies: best[:dependencies],
|
|
393
407
|
source: source_value,
|
|
394
408
|
has_extensions: false,
|
|
395
409
|
remote_uri: nil,
|
|
396
|
-
checksum:
|
|
410
|
+
checksum: best[:checksum],
|
|
397
411
|
)
|
|
398
412
|
end
|
|
413
|
+
|
|
414
|
+
apply_locked_platform_preferences(resolved)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Preference: exact platform match > compatible match > ruby > first.
|
|
418
|
+
def pick_best_platform_spec(specs, local_plat)
|
|
419
|
+
return specs.first if specs.size == 1
|
|
420
|
+
|
|
421
|
+
best = nil
|
|
422
|
+
best_score = -2
|
|
423
|
+
|
|
424
|
+
specs.each do |ls|
|
|
425
|
+
platform = ls[:platform] || "ruby"
|
|
426
|
+
if platform == "ruby"
|
|
427
|
+
score = 0
|
|
428
|
+
else
|
|
429
|
+
spec_plat = Gem::Platform.new(platform)
|
|
430
|
+
if spec_plat === local_plat
|
|
431
|
+
score = spec_plat.to_s == local_plat.to_s ? 2 : 1
|
|
432
|
+
else
|
|
433
|
+
score = -1
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
if score > best_score
|
|
438
|
+
best = ls
|
|
439
|
+
best_score = score
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
best
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Lockfiles can carry only the ruby variant for a gem version.
|
|
447
|
+
# Re-check compact index for the same locked version and upgrade to the
|
|
448
|
+
# best local platform variant when available.
|
|
449
|
+
def apply_locked_platform_preferences(resolved_specs)
|
|
450
|
+
preferred = preferred_platforms_for_locked_specs(resolved_specs)
|
|
451
|
+
return resolved_specs if preferred.empty?
|
|
452
|
+
|
|
453
|
+
resolved_specs.each do |spec|
|
|
454
|
+
key = "#{spec.name}-#{spec.version}"
|
|
455
|
+
platform = preferred[key]
|
|
456
|
+
next if platform.nil? || platform.empty?
|
|
457
|
+
|
|
458
|
+
spec.platform = platform
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
resolved_specs
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def preferred_platforms_for_locked_specs(resolved_specs)
|
|
465
|
+
out = {}
|
|
466
|
+
by_source = resolved_specs
|
|
467
|
+
.select { |spec| rubygems_source_uri?(spec.source) }
|
|
468
|
+
.group_by { |spec| spec.source.to_s.chomp("/") }
|
|
469
|
+
|
|
470
|
+
by_source.each do |source_uri, specs|
|
|
471
|
+
begin
|
|
472
|
+
client = Index::Client.new(source_uri, credentials: @credentials)
|
|
473
|
+
provider = Resolver::Provider.new(client)
|
|
474
|
+
provider.prefetch(specs.map(&:name).uniq)
|
|
475
|
+
|
|
476
|
+
specs.each do |spec|
|
|
477
|
+
preferred = provider.preferred_platform_for(spec.name, Gem::Version.new(spec.version.to_s))
|
|
478
|
+
preferred = preferred.to_s
|
|
479
|
+
next if preferred.empty? || preferred == spec.platform.to_s
|
|
480
|
+
|
|
481
|
+
out["#{spec.name}-#{spec.version}"] = preferred
|
|
482
|
+
end
|
|
483
|
+
rescue StandardError
|
|
484
|
+
next
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
out
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def rubygems_source_uri?(source)
|
|
492
|
+
source.is_a?(String) && source.match?(%r{\Ahttps?://})
|
|
399
493
|
end
|
|
400
494
|
|
|
401
495
|
def download_gem(entry, cache)
|
|
@@ -463,22 +557,37 @@ module Scint
|
|
|
463
557
|
# and can't handle concurrent checkouts from the same repo.
|
|
464
558
|
git_mutex_for(bare_repo).synchronize do
|
|
465
559
|
clone_git_repo(uri, bare_repo) unless Dir.exist?(bare_repo)
|
|
560
|
+
fetch_git_repo(bare_repo)
|
|
561
|
+
|
|
562
|
+
resolved_revision = resolve_git_revision(bare_repo, revision)
|
|
466
563
|
|
|
467
564
|
extracted = cache.extracted_path(spec)
|
|
468
|
-
|
|
565
|
+
marker = git_checkout_marker_path(extracted)
|
|
566
|
+
if Dir.exist?(extracted) && File.exist?(marker)
|
|
567
|
+
return if File.read(marker).strip == resolved_revision
|
|
568
|
+
end
|
|
469
569
|
|
|
470
570
|
tmp = "#{extracted}.#{Process.pid}.#{Thread.current.object_id}.tmp"
|
|
471
571
|
begin
|
|
472
572
|
FileUtils.rm_rf(tmp)
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
573
|
+
FileUtils.mkdir_p(tmp)
|
|
574
|
+
|
|
575
|
+
_out, err, status = git_capture3(
|
|
576
|
+
"--git-dir", bare_repo,
|
|
577
|
+
"--work-tree", tmp,
|
|
578
|
+
"checkout",
|
|
579
|
+
"-f",
|
|
580
|
+
resolved_revision,
|
|
581
|
+
"--",
|
|
582
|
+
".",
|
|
583
|
+
)
|
|
477
584
|
unless status.success?
|
|
478
|
-
raise InstallError, "Git checkout failed for #{spec.name} (#{uri}@#{
|
|
585
|
+
raise InstallError, "Git checkout failed for #{spec.name} (#{uri}@#{resolved_revision}): #{err.to_s.strip}"
|
|
479
586
|
end
|
|
480
587
|
|
|
588
|
+
FileUtils.rm_rf(extracted)
|
|
481
589
|
FS.atomic_move(tmp, extracted)
|
|
590
|
+
FS.atomic_write(marker, "#{resolved_revision}\n")
|
|
482
591
|
ensure
|
|
483
592
|
FileUtils.rm_rf(tmp) if tmp && File.exist?(tmp)
|
|
484
593
|
end
|
|
@@ -504,18 +613,47 @@ module Scint
|
|
|
504
613
|
|
|
505
614
|
def clone_git_repo(uri, bare_repo)
|
|
506
615
|
FS.mkdir_p(File.dirname(bare_repo))
|
|
507
|
-
_out, err, status =
|
|
616
|
+
_out, err, status = git_capture3("clone", "--bare", uri.to_s, bare_repo)
|
|
508
617
|
unless status.success?
|
|
509
618
|
raise InstallError, "Git clone failed for #{uri}: #{err.to_s.strip}"
|
|
510
619
|
end
|
|
511
620
|
end
|
|
512
621
|
|
|
622
|
+
def fetch_git_repo(bare_repo)
|
|
623
|
+
_out, err, status = git_capture3(
|
|
624
|
+
"--git-dir", bare_repo,
|
|
625
|
+
"fetch",
|
|
626
|
+
"--prune",
|
|
627
|
+
"origin",
|
|
628
|
+
"+refs/heads/*:refs/heads/*",
|
|
629
|
+
"+refs/tags/*:refs/tags/*",
|
|
630
|
+
)
|
|
631
|
+
unless status.success?
|
|
632
|
+
raise InstallError, "Git fetch failed for #{bare_repo}: #{err.to_s.strip}"
|
|
633
|
+
end
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def resolve_git_revision(bare_repo, revision)
|
|
637
|
+
out, err, status = git_capture3("--git-dir", bare_repo, "rev-parse", "#{revision}^{commit}")
|
|
638
|
+
unless status.success?
|
|
639
|
+
raise InstallError, "Unable to resolve git revision #{revision.inspect} in #{bare_repo}: #{err.to_s.strip}"
|
|
640
|
+
end
|
|
641
|
+
out.strip
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
def git_capture3(*args)
|
|
645
|
+
Open3.capture3("git", "-c", "core.fsmonitor=false", *args)
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
def git_checkout_marker_path(dir)
|
|
649
|
+
"#{dir}.scint_git_revision"
|
|
650
|
+
end
|
|
651
|
+
|
|
513
652
|
def compile_slots_for(worker_count)
|
|
514
|
-
# Keep
|
|
515
|
-
#
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
[max_compile, available].min
|
|
653
|
+
# Keep native builds serialized to avoid build-env races and reduce
|
|
654
|
+
# memory/CPU spikes from concurrent extension compiles.
|
|
655
|
+
_ = worker_count
|
|
656
|
+
1
|
|
519
657
|
end
|
|
520
658
|
|
|
521
659
|
def install_task_limits(worker_count, compile_slots)
|
|
@@ -531,6 +669,12 @@ module Scint
|
|
|
531
669
|
}
|
|
532
670
|
end
|
|
533
671
|
|
|
672
|
+
def display_bundle_path(path)
|
|
673
|
+
return path if path.start_with?("/", "./", "../")
|
|
674
|
+
|
|
675
|
+
"./#{path}"
|
|
676
|
+
end
|
|
677
|
+
|
|
534
678
|
# Enqueue dependency-aware install tasks so compile/binstub can run
|
|
535
679
|
# concurrently with link/download once prerequisites are satisfied.
|
|
536
680
|
def enqueue_install_dag(scheduler, plan, cache, bundle_path, progress = nil, compile_slots: 1)
|
|
@@ -545,8 +689,8 @@ module Scint
|
|
|
545
689
|
when :skip
|
|
546
690
|
next
|
|
547
691
|
when :builtin
|
|
548
|
-
|
|
549
|
-
|
|
692
|
+
link_id = scheduler.enqueue(:link, entry.spec.name,
|
|
693
|
+
-> { install_builtin_gem(entry, bundle_path) })
|
|
550
694
|
when :download
|
|
551
695
|
key = spec_key(entry.spec)
|
|
552
696
|
download_id = scheduler.enqueue(:download, entry.spec.name,
|
|
@@ -563,7 +707,7 @@ module Scint
|
|
|
563
707
|
build_depends = (depends_on + dep_links).uniq
|
|
564
708
|
|
|
565
709
|
extracted = extracted_path_for_entry(entry, cache)
|
|
566
|
-
if Installer::ExtensionBuilder.
|
|
710
|
+
if Installer::ExtensionBuilder.needs_build?(entry.spec, extracted)
|
|
567
711
|
build_id = scheduler.enqueue(:build_ext, entry.spec.name,
|
|
568
712
|
-> { build_extensions(entry, cache, bundle_path, progress, compile_slots: compile_slots) },
|
|
569
713
|
depends_on: build_depends)
|
|
@@ -649,7 +793,7 @@ module Scint
|
|
|
649
793
|
enqueued = 0
|
|
650
794
|
entries.each do |entry|
|
|
651
795
|
extracted = extracted_path_for_entry(entry, cache)
|
|
652
|
-
next unless Installer::ExtensionBuilder.
|
|
796
|
+
next unless Installer::ExtensionBuilder.needs_build?(entry.spec, extracted)
|
|
653
797
|
|
|
654
798
|
scheduler.enqueue(:build_ext, entry.spec.name,
|
|
655
799
|
-> { build_extensions(entry, cache, bundle_path, nil, compile_slots: compile_slots) })
|
|
@@ -663,10 +807,29 @@ module Scint
|
|
|
663
807
|
if source_str.start_with?("/") && Dir.exist?(source_str)
|
|
664
808
|
source_str
|
|
665
809
|
else
|
|
666
|
-
entry.cached_path || cache.extracted_path(entry.spec)
|
|
810
|
+
base = entry.cached_path || cache.extracted_path(entry.spec)
|
|
811
|
+
if git_source?(entry.spec.source) && Dir.exist?(base)
|
|
812
|
+
resolve_git_gem_subdir(base, entry.spec)
|
|
813
|
+
else
|
|
814
|
+
base
|
|
815
|
+
end
|
|
667
816
|
end
|
|
668
817
|
end
|
|
669
818
|
|
|
819
|
+
# For git monorepo sources, map gem name to its gemspec subdirectory.
|
|
820
|
+
def resolve_git_gem_subdir(repo_root, spec)
|
|
821
|
+
name = spec.name
|
|
822
|
+
return repo_root if File.exist?(File.join(repo_root, "#{name}.gemspec"))
|
|
823
|
+
|
|
824
|
+
source = spec.source
|
|
825
|
+
glob = source.respond_to?(:glob) ? source.glob : Source::Git::DEFAULT_GLOB
|
|
826
|
+
Dir.glob(File.join(repo_root, glob)).each do |path|
|
|
827
|
+
return File.dirname(path) if File.basename(path, ".gemspec") == name
|
|
828
|
+
end
|
|
829
|
+
|
|
830
|
+
repo_root
|
|
831
|
+
end
|
|
832
|
+
|
|
670
833
|
def link_gem_files(entry, cache, bundle_path)
|
|
671
834
|
spec = entry.spec
|
|
672
835
|
extracted = extracted_path_for_entry(entry, cache)
|
|
@@ -680,14 +843,13 @@ module Scint
|
|
|
680
843
|
from_cache: true,
|
|
681
844
|
)
|
|
682
845
|
Installer::Linker.link_files(prepared, bundle_path)
|
|
683
|
-
Installer::Linker.link_files_to_ruby_dir(prepared, cache.install_ruby_dir)
|
|
684
846
|
# If this gem has a cached native build, materialize it during link.
|
|
685
847
|
# This lets reinstalling into a fresh .bundle skip build_ext entirely.
|
|
686
848
|
Installer::ExtensionBuilder.link_cached_build(prepared, bundle_path, cache)
|
|
687
849
|
end
|
|
688
850
|
|
|
689
851
|
def build_extensions(entry, cache, bundle_path, progress = nil, compile_slots: 1)
|
|
690
|
-
extracted = entry
|
|
852
|
+
extracted = extracted_path_for_entry(entry, cache)
|
|
691
853
|
gemspec = load_gemspec(extracted, entry.spec, cache)
|
|
692
854
|
|
|
693
855
|
sync_build_env_dependencies(entry.spec, bundle_path, cache)
|
|
@@ -716,6 +878,8 @@ module Scint
|
|
|
716
878
|
dep.name
|
|
717
879
|
end
|
|
718
880
|
end
|
|
881
|
+
dep_names << "rake"
|
|
882
|
+
dep_names.uniq!
|
|
719
883
|
return if dep_names.empty?
|
|
720
884
|
|
|
721
885
|
source_ruby_dir = File.join(bundle_path, "ruby", RUBY_VERSION.split(".")[0, 2].join(".") + ".0")
|
|
@@ -734,7 +898,7 @@ module Scint
|
|
|
734
898
|
next unless Dir.exist?(source_gem_dir)
|
|
735
899
|
|
|
736
900
|
target_gem_dir = File.join(target_ruby_dir, "gems", full_name)
|
|
737
|
-
FS.
|
|
901
|
+
FS.clone_tree(source_gem_dir, target_gem_dir) unless Dir.exist?(target_gem_dir)
|
|
738
902
|
|
|
739
903
|
target_spec_dir = File.join(target_ruby_dir, "specifications")
|
|
740
904
|
target_spec_path = File.join(target_spec_dir, "#{full_name}.gemspec")
|
|
@@ -990,6 +1154,12 @@ module Scint
|
|
|
990
1154
|
"#{(elapsed_ms / 1000.0).round(2)}s"
|
|
991
1155
|
end
|
|
992
1156
|
|
|
1157
|
+
def format_run_footer(elapsed_ms, worker_count)
|
|
1158
|
+
workers = worker_count.to_i
|
|
1159
|
+
noun = workers == 1 ? "worker" : "workers"
|
|
1160
|
+
"#{format_elapsed(elapsed_ms)}, #{workers} #{noun} used"
|
|
1161
|
+
end
|
|
1162
|
+
|
|
993
1163
|
def warn_missing_bundle_gitignore_entry
|
|
994
1164
|
path = ".gitignore"
|
|
995
1165
|
return unless File.file?(path)
|
data/lib/scint/fs.rb
CHANGED
|
@@ -32,6 +32,11 @@ module Scint
|
|
|
32
32
|
return if system("cp", "-c", src, dst, [:out, :err] => File::NULL)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Try Linux reflink copy-on-write where supported (btrfs/xfs/etc).
|
|
36
|
+
if Platform.linux?
|
|
37
|
+
return if system("cp", "--reflink=always", src, dst, [:out, :err] => File::NULL)
|
|
38
|
+
end
|
|
39
|
+
|
|
35
40
|
# Fallback: hardlink
|
|
36
41
|
begin
|
|
37
42
|
File.link(src, dst)
|
|
@@ -44,6 +49,30 @@ module Scint
|
|
|
44
49
|
FileUtils.cp(src, dst)
|
|
45
50
|
end
|
|
46
51
|
|
|
52
|
+
# Recursively clone directory tree from src_dir into dst_dir.
|
|
53
|
+
# On macOS/APFS, prefers CoW clones via `cp -cR`.
|
|
54
|
+
# Falls back to hardlink_tree, then regular copy per-file if needed.
|
|
55
|
+
def clone_tree(src_dir, dst_dir)
|
|
56
|
+
src_dir = src_dir.to_s
|
|
57
|
+
dst_dir = dst_dir.to_s
|
|
58
|
+
raise Errno::ENOENT, src_dir unless Dir.exist?(src_dir)
|
|
59
|
+
mkdir_p(dst_dir)
|
|
60
|
+
|
|
61
|
+
# Fast path on macOS/APFS: copy-on-write clone of full tree.
|
|
62
|
+
if Platform.macos?
|
|
63
|
+
src_contents = File.join(src_dir, ".")
|
|
64
|
+
return if system("cp", "-cR", src_contents, dst_dir, [:out, :err] => File::NULL)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Fast path on Linux filesystems with reflink support.
|
|
68
|
+
if Platform.linux?
|
|
69
|
+
src_contents = File.join(src_dir, ".")
|
|
70
|
+
return if system("cp", "--reflink=always", "-R", src_contents, dst_dir, [:out, :err] => File::NULL)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
hardlink_tree(src_dir, dst_dir)
|
|
74
|
+
end
|
|
75
|
+
|
|
47
76
|
# Recursively hardlink all files from src_dir into dst_dir.
|
|
48
77
|
# Directory structure is recreated; files are hardlinked.
|
|
49
78
|
def hardlink_tree(src_dir, dst_dir)
|
|
@@ -68,10 +97,26 @@ module Scint
|
|
|
68
97
|
end
|
|
69
98
|
|
|
70
99
|
mkdir_p(File.dirname(dst_path))
|
|
100
|
+
# Another worker may have already materialized this file.
|
|
101
|
+
next if File.exist?(dst_path)
|
|
102
|
+
|
|
71
103
|
begin
|
|
72
104
|
File.link(src_path, dst_path)
|
|
105
|
+
rescue Errno::EEXIST
|
|
106
|
+
# Lost a race to another concurrent linker; destination is valid.
|
|
107
|
+
next
|
|
73
108
|
rescue SystemCallError
|
|
74
|
-
|
|
109
|
+
# TOCTOU guard: destination may have appeared after File.link failed.
|
|
110
|
+
next if File.exist?(dst_path)
|
|
111
|
+
|
|
112
|
+
begin
|
|
113
|
+
clonefile(src_path, dst_path)
|
|
114
|
+
rescue StandardError
|
|
115
|
+
# If a concurrent worker created destination in the meantime,
|
|
116
|
+
# treat this as success; otherwise bubble up.
|
|
117
|
+
next if File.exist?(dst_path)
|
|
118
|
+
raise
|
|
119
|
+
end
|
|
75
120
|
end
|
|
76
121
|
end
|
|
77
122
|
end
|
data/lib/scint/index/client.rb
CHANGED
|
@@ -71,6 +71,41 @@ module Scint
|
|
|
71
71
|
true
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
# True when a gem has native extension sources that need compiling.
|
|
75
|
+
# Platform-specific gems usually ship precompiled binaries and should
|
|
76
|
+
# not be compiled from ext/ unless they lack support for this Ruby.
|
|
77
|
+
def needs_build?(spec, gem_dir)
|
|
78
|
+
platform = spec.respond_to?(:platform) ? spec.platform : nil
|
|
79
|
+
if platform && !platform.to_s.empty? && platform.to_s != "ruby"
|
|
80
|
+
return prebuilt_missing_for_ruby?(gem_dir) && buildable_source_dir?(gem_dir)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
buildable_source_dir?(gem_dir)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Detect versioned prebuilt extension folders like:
|
|
87
|
+
# lib/sqlite3/3.1, lib/sqlite3/3.2 ...
|
|
88
|
+
# If present, the current Ruby minor must exist or we need a build.
|
|
89
|
+
def prebuilt_missing_for_ruby?(gem_dir)
|
|
90
|
+
ruby_minor = RUBY_VERSION[/\d+\.\d+/]
|
|
91
|
+
lib_dir = File.join(gem_dir, "lib")
|
|
92
|
+
return false unless Dir.exist?(lib_dir)
|
|
93
|
+
|
|
94
|
+
Dir.children(lib_dir).each do |child|
|
|
95
|
+
child_path = File.join(lib_dir, child)
|
|
96
|
+
next unless File.directory?(child_path)
|
|
97
|
+
|
|
98
|
+
version_dirs = Dir.children(child_path).select do |entry|
|
|
99
|
+
File.directory?(File.join(child_path, entry)) && entry.match?(/\A\d+\.\d+\z/)
|
|
100
|
+
end
|
|
101
|
+
next if version_dirs.empty?
|
|
102
|
+
|
|
103
|
+
return true unless version_dirs.include?(ruby_minor)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
false
|
|
107
|
+
end
|
|
108
|
+
|
|
74
109
|
# --- private ---
|
|
75
110
|
|
|
76
111
|
def buildable_source_dir?(gem_dir)
|
|
@@ -175,7 +210,7 @@ module Scint
|
|
|
175
210
|
spec_full_name(spec))
|
|
176
211
|
return if Dir.exist?(ext_install_dir)
|
|
177
212
|
|
|
178
|
-
FS.
|
|
213
|
+
FS.clone_tree(cached_ext, ext_install_dir)
|
|
179
214
|
end
|
|
180
215
|
|
|
181
216
|
def build_env(gem_dir, build_ruby_dir, make_jobs)
|
|
@@ -258,7 +293,7 @@ module Scint
|
|
|
258
293
|
private_class_method :find_extension_dirs, :compile_extension,
|
|
259
294
|
:compile_extconf, :compile_cmake, :compile_rake,
|
|
260
295
|
:find_rake_executable, :link_extensions, :build_env, :run_cmd,
|
|
261
|
-
:spec_full_name, :ruby_install_dir
|
|
296
|
+
:spec_full_name, :ruby_install_dir, :prebuilt_missing_for_ruby?
|
|
262
297
|
end
|
|
263
298
|
end
|
|
264
299
|
end
|
|
@@ -33,7 +33,7 @@ module Scint
|
|
|
33
33
|
# 1. Link gem files into gems/{full_name}/
|
|
34
34
|
gem_dest = File.join(ruby_dir, "gems", full_name)
|
|
35
35
|
unless Dir.exist?(gem_dest)
|
|
36
|
-
FS.
|
|
36
|
+
FS.clone_tree(prepared_gem.extracted_path, gem_dest)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
39
|
# 2. Write gemspec into specifications/
|
|
@@ -23,11 +23,12 @@ module Scint
|
|
|
23
23
|
plan_one(spec, ruby_dir, cache_layout)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
#
|
|
27
|
-
|
|
26
|
+
# Keep built-ins first, then downloads (big->small), then the rest.
|
|
27
|
+
builtins, non_builtins = entries.partition { |e| e.action == :builtin }
|
|
28
|
+
downloads, rest = non_builtins.partition { |e| e.action == :download }
|
|
28
29
|
downloads.sort_by! { |e| -(estimated_size(e.spec)) }
|
|
29
30
|
|
|
30
|
-
downloads + rest
|
|
31
|
+
builtins + downloads + rest
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def plan_one(spec, ruby_dir, cache_layout)
|
|
@@ -58,7 +59,7 @@ module Scint
|
|
|
58
59
|
# Local path sources are linked directly from their source tree.
|
|
59
60
|
local_source = local_source_path(spec)
|
|
60
61
|
if local_source
|
|
61
|
-
action = ExtensionBuilder.
|
|
62
|
+
action = ExtensionBuilder.needs_build?(spec, local_source) ? :build_ext : :link
|
|
62
63
|
return PlanEntry.new(spec: spec, action: action, cached_path: local_source, gem_path: gem_path)
|
|
63
64
|
end
|
|
64
65
|
|
|
@@ -75,7 +76,7 @@ module Scint
|
|
|
75
76
|
|
|
76
77
|
def needs_ext_build?(spec, cache_layout)
|
|
77
78
|
extracted = cache_layout.extracted_path(spec)
|
|
78
|
-
return false unless ExtensionBuilder.
|
|
79
|
+
return false unless ExtensionBuilder.needs_build?(spec, extracted)
|
|
79
80
|
|
|
80
81
|
!ExtensionBuilder.cached_build_available?(spec, cache_layout)
|
|
81
82
|
end
|
|
@@ -83,7 +84,7 @@ module Scint
|
|
|
83
84
|
def extension_link_missing?(spec, ruby_dir, cache_layout)
|
|
84
85
|
extracted = cache_layout.extracted_path(spec)
|
|
85
86
|
return false unless Dir.exist?(extracted)
|
|
86
|
-
return false unless ExtensionBuilder.
|
|
87
|
+
return false unless ExtensionBuilder.needs_build?(spec, extracted)
|
|
87
88
|
|
|
88
89
|
full = cache_layout.full_name(spec)
|
|
89
90
|
ext_install_dir = File.join(
|
|
@@ -6,7 +6,7 @@ module Scint
|
|
|
6
6
|
# Produces output compatible with stock bundler.
|
|
7
7
|
#
|
|
8
8
|
# Sections in order: source blocks (GEM/GIT/PATH), PLATFORMS,
|
|
9
|
-
# DEPENDENCIES, CHECKSUMS (if present), RUBY VERSION
|
|
9
|
+
# DEPENDENCIES, CHECKSUMS (if present), RUBY VERSION.
|
|
10
10
|
class Writer
|
|
11
11
|
def self.write(lockfile_data)
|
|
12
12
|
new(lockfile_data).generate
|
|
@@ -24,7 +24,6 @@ module Scint
|
|
|
24
24
|
add_dependencies(out)
|
|
25
25
|
add_checksums(out)
|
|
26
26
|
add_ruby_version(out)
|
|
27
|
-
add_bundled_with(out)
|
|
28
27
|
|
|
29
28
|
out
|
|
30
29
|
end
|
|
@@ -168,11 +167,6 @@ module Scint
|
|
|
168
167
|
out << " #{@data.ruby_version}\n"
|
|
169
168
|
end
|
|
170
169
|
|
|
171
|
-
def add_bundled_with(out)
|
|
172
|
-
return unless @data.bundler_version
|
|
173
|
-
out << "\nBUNDLED WITH\n"
|
|
174
|
-
out << " #{@data.bundler_version}\n"
|
|
175
|
-
end
|
|
176
170
|
end
|
|
177
171
|
end
|
|
178
172
|
end
|