traveling-ruby 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.
@@ -0,0 +1,136 @@
1
+ require "json"
2
+ require "set"
3
+
4
+ module TravelingRuby
5
+ # One Ruby version and revision of Traveling Ruby, published as a release per
6
+ # target tagged ruby-<ruby>-<revision>-<target>. Each release has the runtime,
7
+ # a tarball per native gem, SHA256SUMS and manifest.json.
8
+ #
9
+ # The source is a GitHub repository URL, or a local directory holding a
10
+ # directory per release tag (handy for mirrors and testing).
11
+ class Release
12
+ DEFAULT_SOURCE = "https://github.com/trubygems/traveling-ruby".freeze
13
+
14
+ attr_reader :ruby, :revision
15
+
16
+ def self.resolve(fetcher:, source: DEFAULT_SOURCE, ruby: nil, revision: nil)
17
+ source = source.chomp("/")
18
+ if ruby.nil?
19
+ raise Error, "set the Ruby version to use a local source" if fetcher.local?(source)
20
+ tag = fetcher.redirect_location("#{source}/releases/latest").to_s.split("/").last
21
+ match = tag.to_s.match(/\Aruby-([^-]+)-(\d+)-/)
22
+ raise Error, "could not find the latest release at #{source}; set the Ruby version" unless match
23
+ ruby = match[1]
24
+ revision ||= match[2]
25
+ end
26
+ revision ||= newest_revision(fetcher, source, ruby)
27
+ new(fetcher: fetcher, source: source, ruby: ruby, revision: revision.to_s)
28
+ end
29
+
30
+ # From versions/RUBY_REVISIONS.txt, one "<ruby> <revision>" per line
31
+ def self.newest_revision(fetcher, source, ruby)
32
+ url =
33
+ if fetcher.local?(source)
34
+ File.join(source, "RUBY_REVISIONS.txt")
35
+ elsif (repo = source[%r{\Ahttps://github\.com/([^/]+/[^/]+)\z}, 1])
36
+ "https://raw.githubusercontent.com/#{repo}/main/versions/RUBY_REVISIONS.txt"
37
+ end
38
+ revisions = url && fetcher.read(url)
39
+ line = revisions.to_s.lines.map(&:split).find { |version, _| version == ruby }
40
+ line ? line[1] : "0"
41
+ end
42
+
43
+ def initialize(fetcher:, source:, ruby:, revision:)
44
+ @fetcher = fetcher
45
+ @source = source
46
+ @ruby = ruby
47
+ @revision = revision
48
+ @checksums = {}
49
+ @manifests = {}
50
+ end
51
+
52
+ def to_s
53
+ "#{ruby}-#{revision}"
54
+ end
55
+
56
+ def key(target)
57
+ "#{ruby}-#{revision}-#{target}"
58
+ end
59
+
60
+ def tag(target)
61
+ "ruby-#{key(target)}"
62
+ end
63
+
64
+ # The runtime tarball, downloaded and verified. The full runtime keeps what
65
+ # the slim one leaves out, such as Ruby's headers and static libraries.
66
+ def runtime(target, full: false)
67
+ file =
68
+ if full
69
+ manifest(target).dig("runtime_full", "file") || "traveling-ruby-#{key(target)}-full.tar.gz"
70
+ else
71
+ manifest(target).dig("runtime", "file") || "traveling-ruby-#{key(target)}.tar.gz"
72
+ end
73
+ fetch(target, file)
74
+ end
75
+
76
+ # Full names (name-version) of the gems the runtime comes with: Ruby's
77
+ # default and bundled gems
78
+ def runtime_gems(target, full: false)
79
+ @runtime_gems ||= {}
80
+ @runtime_gems[[target, full]] ||= begin
81
+ listing = IO.popen(["tar", "-tzf", runtime(target, full: full)], &:read)
82
+ raise Error, "could not list #{File.basename(runtime(target, full: full))}" unless $?.success?
83
+ listing.scan(%r{/specifications/(?:default/)?([^/\n]+)\.gemspec$}).flatten.to_set
84
+ end
85
+ end
86
+
87
+ # Native gems built for target, as {name, version, file, sha256}
88
+ def gems(target)
89
+ manifest(target).fetch("gems", [])
90
+ end
91
+
92
+ # Versions of a native gem built for target
93
+ def built_versions(target, name)
94
+ gems(target).select { |gem| gem["name"] == name }.map { |gem| gem["version"] }
95
+ end
96
+
97
+ # The tarball of a native gem's extension, or nil when it was not built
98
+ def gem_extension(target, name, version)
99
+ gem = gems(target).find { |g| g["name"] == name && g["version"] == version }
100
+ gem && fetch(target, gem["file"])
101
+ end
102
+
103
+ def manifest(target)
104
+ @manifests[target] ||= begin
105
+ body = @fetcher.read(url(target, "manifest.json"))
106
+ raise Error, "no Traveling Ruby #{self} release for #{target} (#{url(target, "manifest.json")} not found)" unless body
107
+ JSON.parse(body)
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def fetch(target, file)
114
+ sha256 = checksums(target)[file] or raise Error, "#{file} is not listed in #{tag(target)}'s SHA256SUMS"
115
+ @fetcher.fetch(url(target, file), sha256: sha256, cache_key: File.join(tag(target), file))
116
+ end
117
+
118
+ def checksums(target)
119
+ @checksums[target] ||= begin
120
+ body = @fetcher.read(url(target, "SHA256SUMS")) or raise Error, "#{tag(target)} has no SHA256SUMS"
121
+ body.lines.to_h do |line|
122
+ sha256, file = line.strip.split(/ [ *]/, 2)
123
+ [file, sha256]
124
+ end
125
+ end
126
+ end
127
+
128
+ def url(target, file)
129
+ if @fetcher.local?(@source)
130
+ File.join(@source, tag(target), file)
131
+ else
132
+ "#{@source}/releases/download/#{tag(target)}/#{file}"
133
+ end
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,78 @@
1
+ module TravelingRuby
2
+ # Terminal output: colored when the stream is a terminal and NO_COLOR is not
3
+ # set, plain otherwise (CI logs, pipes, tests)
4
+ class UI
5
+ COLORS = {red: 31, green: 32, yellow: 33, cyan: 36, bold: 1, dim: 2}.freeze
6
+
7
+ attr_reader :io
8
+
9
+ def initialize(io = $stderr, verbose: false, color: nil)
10
+ @io = io
11
+ @verbose = verbose
12
+ @color = color.nil? ? io.respond_to?(:tty?) && io.tty? && ENV["NO_COLOR"].to_s.empty? : color
13
+ end
14
+
15
+ def verbose?
16
+ @verbose
17
+ end
18
+
19
+ def say(message = "")
20
+ @io.puts message
21
+ end
22
+
23
+ def title(message)
24
+ say paint(message, :bold)
25
+ end
26
+
27
+ # A finished step: " ✓ label detail"
28
+ def done(label, detail = nil)
29
+ say " #{paint("✓", :green)} #{label}#{" #{paint(detail, :dim)}" if detail}"
30
+ end
31
+
32
+ def failed(label, detail = nil)
33
+ say " #{paint("✗", :red)} #{label}#{" #{detail}" if detail}"
34
+ end
35
+
36
+ def hint(message)
37
+ say " #{paint("→ #{message}", :cyan)}"
38
+ end
39
+
40
+ def warn(message)
41
+ say "#{paint("warning:", :yellow, :bold)} #{message}"
42
+ end
43
+
44
+ def error(message)
45
+ say "#{paint("error:", :red, :bold)} #{message}"
46
+ end
47
+
48
+ # Shown with --verbose only
49
+ def detail(message)
50
+ say paint(" #{message}", :dim) if @verbose
51
+ end
52
+
53
+ # Downloads are worth seeing: they explain a slow first run
54
+ def fetching(what)
55
+ say paint(" ↓ #{what}", :dim)
56
+ end
57
+
58
+ def paint(text, *styles)
59
+ return text.to_s unless @color
60
+ "\e[#{styles.map { |style| COLORS.fetch(style) }.join(";")}m#{text}\e[0m"
61
+ end
62
+
63
+ def self.size(bytes)
64
+ units = %w[B KB MB GB]
65
+ value = bytes.to_f
66
+ unit = units.shift
67
+ while value >= 1024 && units.any?
68
+ value /= 1024
69
+ unit = units.shift
70
+ end
71
+ unit == "B" ? "#{bytes} B" : format("%.1f %s", value, unit)
72
+ end
73
+
74
+ def self.duration(seconds)
75
+ seconds < 60 ? format("%.1fs", seconds) : format("%dm %ds", seconds / 60, seconds % 60)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,3 @@
1
+ module TravelingRuby
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,31 @@
1
+ $LOAD_PATH.unshift(__dir__) unless $LOAD_PATH.include?(__dir__)
2
+
3
+ require "traveling_ruby/version"
4
+
5
+ module TravelingRuby
6
+ class Error < StandardError; end
7
+
8
+ # Targets that have a release, as named in release tags
9
+ TARGETS = %w[
10
+ linux-x86_64 linux-arm64
11
+ linux-musl-x86_64 linux-musl-arm64
12
+ macos-x86_64 macos-arm64
13
+ windows-x86_64 windows-arm64
14
+ ].freeze
15
+
16
+ # Packaged when no targets are given
17
+ DEFAULT_TARGETS = %w[linux-x86_64 linux-arm64 macos-x86_64 macos-arm64 windows-x86_64].freeze
18
+
19
+ def self.windows?(target)
20
+ target.start_with?("windows-")
21
+ end
22
+
23
+ autoload :UI, "traveling_ruby/ui"
24
+ autoload :Config, "traveling_ruby/config"
25
+ autoload :Fetcher, "traveling_ruby/fetcher"
26
+ autoload :Release, "traveling_ruby/release"
27
+ autoload :Bundle, "traveling_ruby/bundle"
28
+ autoload :Launcher, "traveling_ruby/launcher"
29
+ autoload :Packager, "traveling_ruby/packager"
30
+ autoload :CLI, "traveling_ruby/cli"
31
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: traveling-ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yousaf Nabi
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ Builds self-contained packages of a Ruby app for each platform Traveling Ruby
14
+ supports: the app, its gems, the prebuilt native extensions they need and a
15
+ portable Ruby, behind a launcher script. No Ruby is needed to run them.
16
+ executables:
17
+ - traveling-ruby
18
+ - truby
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - LICENSE.md
23
+ - README.md
24
+ - exe/traveling-ruby
25
+ - exe/truby
26
+ - lib/traveling_ruby.rb
27
+ - lib/traveling_ruby/bundle.rb
28
+ - lib/traveling_ruby/cli.rb
29
+ - lib/traveling_ruby/config.rb
30
+ - lib/traveling_ruby/fetcher.rb
31
+ - lib/traveling_ruby/launcher.rb
32
+ - lib/traveling_ruby/packager.rb
33
+ - lib/traveling_ruby/rake_task.rb
34
+ - lib/traveling_ruby/release.rb
35
+ - lib/traveling_ruby/ui.rb
36
+ - lib/traveling_ruby/version.rb
37
+ homepage: https://github.com/trubygems/traveling-ruby
38
+ licenses:
39
+ - MIT
40
+ metadata:
41
+ source_code_uri: https://github.com/trubygems/traveling-ruby/tree/main/packager
42
+ bug_tracker_uri: https://github.com/trubygems/traveling-ruby/issues
43
+ rubygems_mfa_required: 'true'
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: '3.1'
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.6.9
59
+ specification_version: 4
60
+ summary: 'truby: package Ruby apps to run on Linux, macOS and Windows with no Ruby
61
+ installed'
62
+ test_files: []