herve 0.1.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.
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herve
4
+ class Ruby
5
+ # Error on Request
6
+ class RequestError < Error; end
7
+
8
+ Request = Data.define(:engine, :major, :minor, :patch, :tiny, :prerelease)
9
+
10
+ class Request
11
+ # @private Version fields
12
+ VERSION_FIELDS = %i[major minor patch tiny prerelease].freeze
13
+ # @private Regex to check a character is alphabetic
14
+ ALPHA_RE = /^[[:alpha:]]/
15
+
16
+ class << self
17
+ def parse(str)
18
+ raise RequestError, "invalid string" if str.empty?
19
+
20
+ str = str.strip
21
+ engine, version = if str[0] =~ ALPHA_RE
22
+ str.split("-", 2)
23
+ else
24
+ ["ruby", str]
25
+ end
26
+
27
+ major, minor, patch, tiny, pre = dissect_version(version)
28
+ new(Engine.new(engine), major, minor, patch, tiny, pre)
29
+ end
30
+
31
+ def dissect_version(version)
32
+ return [nil] * 4 if version.nil?
33
+
34
+ number = nil
35
+ pre = nil
36
+ if version[0] =~ ALPHA_RE
37
+ raise RequestError, "invalid version" unless version == "dev"
38
+
39
+ pre = version
40
+ else
41
+ number, pre = version.split("-", 2)
42
+ end
43
+
44
+ numbers = if number
45
+ number.split(".").map(&:to_i)
46
+ else
47
+ []
48
+ end
49
+ raise RequestError, "too many numbers in version #{version}" if numbers.size > 4
50
+
51
+ numbers.concat([nil] * (4 - numbers.size))
52
+ numbers << pre
53
+ end
54
+ end
55
+
56
+ def to_s # rubocop:disable Metrics/AbcSize
57
+ str = engine.to_s
58
+ unless major.nil?
59
+ str << "-#{major}"
60
+ unless minor.nil?
61
+ str << ".#{minor}"
62
+ unless patch.nil?
63
+ str << ".#{patch}"
64
+ str << ".#{tiny}" unless tiny.nil?
65
+ end
66
+ end
67
+ end
68
+
69
+ str << "-#{prerelease}" if prerelease
70
+ str
71
+ end
72
+
73
+ def find_match_in(rubies)
74
+ rubies.find { |ruby| satisfied_by(ruby) }
75
+ end
76
+
77
+ def satisfied_by(ruby)
78
+ other = ruby.version
79
+
80
+ (engine == other.engine) && check_fields(other, VERSION_FIELDS)
81
+ end
82
+
83
+ def version_number
84
+ VERSION_FIELDS.map { |m| send(m) }.compact.join(".")
85
+ end
86
+
87
+ def <=>(other)
88
+ res = engine <=> other.engine
89
+ return res if res != 0
90
+
91
+ return 1 if !major.nil? && other.major.nil?
92
+ return 1 if !major.nil? && (major > other.major)
93
+ return -1 if !major.nil? && (major < other.major)
94
+ return 1 if !minor.nil? && other.minor.nil?
95
+ return 1 if !minor.nil? && (minor > other.minor)
96
+ return -1 if !minor.nil? && (minor < other.minor)
97
+ return 1 if !patch.nil? && other.patch.nil?
98
+ return 1 if !patch.nil? && (patch > other.patch)
99
+ return -1 if !patch.nil? && (patch < other.patch)
100
+ return 1 if !tiny.nil? && other.tiny.nil?
101
+ return 1 if !tiny.nil? && (tiny > other.tiny)
102
+ return -1 if !tiny.nil? && (tiny < other.tiny)
103
+ return 1 if prerelease.nil? && !other.prerelease.nil?
104
+ return -1 if !prerelease.nil? && other.prerelease.nil?
105
+ return 1 if !prerelease.nil? && (prerelease > other.prerelease)
106
+ return -1 if !prerelease.nil? && (prerelease < other.prerelease)
107
+
108
+ 0
109
+ end
110
+
111
+ private
112
+
113
+ def check_fields(other, fields)
114
+ fields.all? do |field|
115
+ value = send(field)
116
+ value.nil? || (value == other.send(field))
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
data/lib/herve/ruby.rb ADDED
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herve
4
+ # Error on Ruby
5
+ class RubyError < Error; end
6
+
7
+ class Ruby
8
+ attr_reader :key, :version, :path, :symlink, :arch, :os, :gem_root
9
+
10
+ RUBY_EXTRACT_SCRIPT = <<~ENDOFSCRIPT
11
+ puts(Object.const_defined?(:RUBY_ENGINE) ? RUBY_ENGINE : 'ruby')
12
+ puts(RUBY_VERSION)
13
+ puts(Object.const_defined?(:RUBY_PLATFORM) ? RUBY_PLATFORM : 'unknown')
14
+ puts(Object.const_defined?(:RbConfig) && RbConfig::CONFIG['host_cpu'] ? RbConfig::CONFIG['host_cpu'] : 'unknown')
15
+ puts(Object.const_defined?(:RbConfig) && RbConfig::CONFIG['host_os'] ? RbConfig::CONFIG['host_os'] : 'unknown')
16
+ puts(begin; require 'rubygems'; Gem.default_dir; rescue ScriptError, NoMethodError; end)
17
+ ENDOFSCRIPT
18
+
19
+ class << self
20
+ def from_directory(directory)
21
+ dir = Pathname.new(directory)
22
+ raise RubyError, "invalid directory #{directory}" if !dir.exist? || dir.empty?
23
+
24
+ ruby_bin = dir.join("bin", "ruby")
25
+ raise RubyError, "no ruby executable found" unless ruby_bin.executable?
26
+
27
+ extract_info(ruby_bin, dir)
28
+ end
29
+
30
+ private
31
+
32
+ def extract_info(ruby_bin, path)
33
+ symlink = find_symlink_target(ruby_bin)
34
+
35
+ data = run_ruby_with_script(ruby_bin, RUBY_EXTRACT_SCRIPT)
36
+ ruby_engine = data[0]
37
+ ruby_version = data[1]
38
+ ruby_platform = data[2]
39
+ host_cpu = data[3]
40
+ host_os = data[4]
41
+ gem_root = data[5].empty? ? nil : Pathname.new(data[5])
42
+
43
+ version = Request.parse("#{ruby_engine}-#{ruby_version}")
44
+ host_cpu = extract_arch_from_platform(ruby_platform) if host_cpu == "unknown"
45
+ host_os = extract_os_from_platform(ruby_platform) if host_os == "unknown"
46
+
47
+ new("#{version}-#{host_os}-#{host_cpu}",
48
+ version,
49
+ path,
50
+ symlink,
51
+ normalize_arch(host_cpu),
52
+ normalize_os(host_os),
53
+ gem_root)
54
+ end
55
+
56
+ def find_symlink_target(ruby_bin)
57
+ ruby_bin.realpath if ruby_bin.symlink?
58
+ end
59
+
60
+ def run_ruby_with_script(ruby_bin, script)
61
+ output = nil
62
+ # clean up env to run ruby
63
+ env = Config.env_for(nil)
64
+ IO.popen(env, [ruby_bin.to_s, "-e", script]) do |io|
65
+ output = io.read
66
+ end
67
+
68
+ raise RubyError, "running ruby failed with status #{$CHILD_STATUS}" if output.nil? || output.empty?
69
+
70
+ output.lines.map(&:strip)
71
+ end
72
+
73
+ def extract_arch_from_platform(platform)
74
+ if platform.include?("aarch64") || platform.include?("arm64")
75
+ "aarch64"
76
+ elsif platform.include?("x86_64") || platform.include?("amd64")
77
+ "x86_64"
78
+ elsif platform.include?("i386") || platform.include?("i686")
79
+ "x86"
80
+ else
81
+ "unknown"
82
+ end
83
+ end
84
+
85
+ def extract_os_from_platform(platform)
86
+ if platform.include?("darwin")
87
+ "darwin"
88
+ elsif platform.include?("linux")
89
+ "linux"
90
+ elsif platform.include?("mingw") || platform.include?("mswin")
91
+ "windows"
92
+ else
93
+ "unknown"
94
+ end
95
+ end
96
+
97
+ def normalize_arch(arch)
98
+ case arch
99
+ when "aarch64", "arm64"
100
+ "aarch64"
101
+ when "x86_64", "amd64"
102
+ "x86_64"
103
+ when "i386", "i686"
104
+ "x86"
105
+ else
106
+ arch
107
+ end
108
+ end
109
+
110
+ def normalize_os(os)
111
+ if os.include?("darwin")
112
+ "macos"
113
+ elsif os.include?("linux")
114
+ "linux"
115
+ elsif os.include?("mingw") || os.include?("mswin") || os.include?("window")
116
+ "windows"
117
+ elsif os.include?("freebsd")
118
+ "freebsd"
119
+ elsif os.include?("netbsd")
120
+ "netbsd"
121
+ elsif os.include?("openbsd")
122
+ "openbsd"
123
+ else
124
+ os
125
+ end
126
+ end
127
+ end
128
+
129
+ def initialize(key, version, path, symlink, arch, os, gem_root)
130
+ @key = key
131
+ @version = version
132
+ @path = path
133
+ @symlink = symlink
134
+ @arch = arch
135
+ @os = os
136
+ @gem_root = gem_root
137
+ end
138
+
139
+ def display_name
140
+ version.to_s
141
+ end
142
+
143
+ def valid?
144
+ executable_path.exist?
145
+ end
146
+
147
+ def executable_path
148
+ bin_path.join("ruby")
149
+ end
150
+
151
+ def bin_path
152
+ path.join("bin")
153
+ end
154
+
155
+ def gem_home
156
+ Pathname.new(Dir.home).join(".gem", version.engine.name, version.version_number)
157
+ end
158
+
159
+ def to_json(state = nil, *)
160
+ hsh = {
161
+ key: key,
162
+ version: display_name,
163
+ path: path.to_s,
164
+ arch: arch,
165
+ os: os,
166
+ gem_root: gem_root.to_s
167
+ }
168
+ hsh[:symlink] = symlink.to_s if symlink
169
+ JSON::State.from_state(state).generate(hsh)
170
+ end
171
+
172
+ # Sort by version
173
+ # @return [-1, 0, 1, nil]
174
+ def <=>(other)
175
+ version <=> other.version
176
+ end
177
+
178
+ # Check equality. +other+ must be a {Ruby} with same {#version}, {#os}, {#arch} and {#path}.
179
+ # @param [Object] other
180
+ # @return [Boolean]
181
+ def ==(other)
182
+ other.is_a?(Ruby) && (version == other.version) && (arch == other.arch) && (os == other.os) && (path == other.path)
183
+ end
184
+ end
185
+ end
186
+
187
+ require_relative "ruby/engine"
188
+ require_relative "ruby/request"
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herve
4
+ # Error while handling version
5
+ class VersionError < Error; end
6
+
7
+ # Version handling
8
+ class Version
9
+ include Comparable
10
+
11
+ # Full version, as String
12
+ attr_reader :version
13
+ # Version segments
14
+ attr_reader :segments
15
+
16
+ def initialize(version)
17
+ @version = normalized = normalize_version(version)
18
+ @segments = parse_segments(normalized)
19
+ end
20
+
21
+ def initialize_copy(_other)
22
+ @version = @version.dup
23
+ @segments = @segments.dup
24
+ end
25
+
26
+ # Say if self is a prerelase
27
+ def prerelease?
28
+ @segments.any? { |s| s.is_a?(String) }
29
+ end
30
+
31
+ # Get segments with trailing zeros in release and prerelease parts removed
32
+ def canonical_segments
33
+ cseg = @segments.dup
34
+ pre_index = pre_index_in_segments || cseg.size
35
+ (pre_index - 1).downto(1) do |i|
36
+ break unless cseg[i].zero?
37
+
38
+ cseg.delete_at(i)
39
+ end
40
+
41
+ if pre_index < cseg.size
42
+ (cseg.size - 1).downto(pre_index + 1) do |i|
43
+ break unless cseg[i].zero?
44
+
45
+ cseg.delete_at(i)
46
+ end
47
+ end
48
+
49
+ cseg
50
+ end
51
+
52
+ # Give a new version from self without prerelease part
53
+ def release
54
+ pre_index = pre_index_in_segments
55
+ return clone if pre_index.nil?
56
+
57
+ new_segments = @segments[0...pre_index]
58
+ Version.new(new_segments.join("."))
59
+ end
60
+
61
+ # eql? is strict equality. Versions are eql? if, and only if, hey have same version, with same precision
62
+ def eql?(other)
63
+ (self.class == other.class) && (segments == other.segments)
64
+ end
65
+
66
+ # Compares this version with +other+ returning -1, 0, or 1 if the
67
+ # other version is larger, the same, or smaller than this
68
+ # one. Attempts to compare to something that's not a
69
+ # Herve::Version or a valid version String return +nil+.
70
+ def <=>(other)
71
+ begin
72
+ return self <=> self.class.new(other) if other.is_a?(String)
73
+ rescue VersionError # rubocop:disable Lint/SuppressedException
74
+ end
75
+ return nil unless other.is_a?(Version)
76
+
77
+ compare_segments(canonical_segments, other.canonical_segments)
78
+ end
79
+
80
+ def inspect
81
+ "#<#{self.class} #{version.inspect}>"
82
+ end
83
+
84
+ private
85
+
86
+ def normalize_version(version)
87
+ nver = version.strip
88
+ return "0" if nver.empty?
89
+
90
+ if nver.include?("\n") && (nver.split("\n").size > 1)
91
+ raise VersionError, "version cannot contain new lines (#{version})"
92
+ end
93
+
94
+ raise VersionError, "version cannot contain consecutive dots (#{version})" if nver.include?("..")
95
+ raise VersionError, "version cannot end with a dot (#{version})" if nver.end_with?(".")
96
+ raise VersionError, "version cannot contain spaces (#{version})" if nver.include?(" ")
97
+
98
+ nver.to_s.freeze
99
+ end
100
+
101
+ def parse_segments(version)
102
+ version = version.sub("-", ".pre.")
103
+ segments = version.scan(/[0-9]+|[a-z]+/i).map do |s|
104
+ s =~ /^\d+$/ ? s.to_i : s
105
+ end
106
+ segments = [0] if segments.empty?
107
+ segments.freeze
108
+ end
109
+
110
+ def pre_index_in_segments
111
+ @segments.each_with_index do |segment, i|
112
+ return i if segment.is_a?(String)
113
+ end
114
+ nil
115
+ end
116
+
117
+ def compare_segments(lcs, rcs)
118
+ return 0 if lcs == rcs
119
+
120
+ limit = [lcs.size, rcs.size].max
121
+
122
+ limit.times do |i|
123
+ left = lcs[i] || 0
124
+ right = rcs[i] || 0
125
+ next if left == right
126
+ return -1 if left.is_a?(String) && right.is_a?(Numeric)
127
+ return 1 if left.is_a?(Numeric) && right.is_a?(String)
128
+
129
+ return left <=> right
130
+ end
131
+
132
+ 0
133
+ end
134
+ end
135
+ end
data/lib/herve.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Herve
4
+ # Base Herve error class
5
+ class Error < StandardError; end
6
+
7
+ DEFAULT_RUBIES = "$HOME/.rubies"
8
+
9
+ # Expand a path containing environment variables
10
+ # @param [String] path
11
+ # @return [Pathname] expanded path, or +path+ if nothing to expand
12
+ def self.expand_path(path)
13
+ return Pathname.new(path) unless path.include?("$")
14
+
15
+ new_path = path.gsub(/\$\w+/) do |envvar|
16
+ ENV[envvar[1..]] || ""
17
+ end
18
+ Pathname.new(new_path)
19
+ end
20
+ end
21
+
22
+ require_relative "herve/version"
23
+ require_relative "herve/cache"
24
+ require_relative "herve/config"
25
+ require_relative "herve/ruby"
26
+ require_relative "herve/cli"
metadata ADDED
@@ -0,0 +1,187 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: herve
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - sd77
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 2025-09-21 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: completely
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.7.2
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.7.2
26
+ - !ruby/object:Gem::Dependency
27
+ name: digest-xxhash
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: 0.2.9
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: 0.2.9
40
+ - !ruby/object:Gem::Dependency
41
+ name: dry-cli
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 1.3.0
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 1.3.0
54
+ - !ruby/object:Gem::Dependency
55
+ name: faraday
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.13'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.13'
68
+ - !ruby/object:Gem::Dependency
69
+ name: faraday-follow_redirects
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: 0.3.0
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.3.0
82
+ - !ruby/object:Gem::Dependency
83
+ name: json
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '2.13'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2.13'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rainbow
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.1'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.1'
110
+ - !ruby/object:Gem::Dependency
111
+ name: xdg
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '7.1'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '7.1'
124
+ email:
125
+ - sd77@ld77.eu
126
+ executables:
127
+ - herve
128
+ extensions: []
129
+ extra_rdoc_files: []
130
+ files:
131
+ - ".rspec"
132
+ - ".rubocop.yml"
133
+ - CHANGELOG.md
134
+ - LICENSE.txt
135
+ - README.md
136
+ - Rakefile
137
+ - exe/herve
138
+ - lib/herve.rb
139
+ - lib/herve/cache.rb
140
+ - lib/herve/cache/entry.rb
141
+ - lib/herve/cache/shard.rb
142
+ - lib/herve/cache/timestamp.rb
143
+ - lib/herve/cli.rb
144
+ - lib/herve/cli/gem_command.rb
145
+ - lib/herve/cli/ruby_commands.rb
146
+ - lib/herve/cli/ruby_commands/find.rb
147
+ - lib/herve/cli/ruby_commands/install.rb
148
+ - lib/herve/cli/ruby_commands/list.rb
149
+ - lib/herve/cli/ruby_commands/pin.rb
150
+ - lib/herve/cli/ruby_commands/run.rb
151
+ - lib/herve/cli/ruby_commands/uninstall.rb
152
+ - lib/herve/cli/shell_commands.rb
153
+ - lib/herve/cli/shell_commands/completion.rb
154
+ - lib/herve/cli/shell_commands/env.rb
155
+ - lib/herve/cli/shell_commands/init.rb
156
+ - lib/herve/config.rb
157
+ - lib/herve/release.rb
158
+ - lib/herve/ruby.rb
159
+ - lib/herve/ruby/engine.rb
160
+ - lib/herve/ruby/request.rb
161
+ - lib/herve/version.rb
162
+ homepage: https://codeberg.org/sd77/herve
163
+ licenses:
164
+ - MIT
165
+ metadata:
166
+ allowed_push_host: https://rubygems.org
167
+ homepage_uri: https://codeberg.org/sd77/herve
168
+ rubygems_mfa_required: 'true'
169
+ source_code_uri: https://codeberg.org/sd77/herve
170
+ rdoc_options: []
171
+ require_paths:
172
+ - lib
173
+ required_ruby_version: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - ">="
176
+ - !ruby/object:Gem::Version
177
+ version: 3.2.0
178
+ required_rubygems_version: !ruby/object:Gem::Requirement
179
+ requirements:
180
+ - - ">="
181
+ - !ruby/object:Gem::Version
182
+ version: '0'
183
+ requirements: []
184
+ rubygems_version: 3.6.2
185
+ specification_version: 4
186
+ summary: Herve is like rv, but pure ruby
187
+ test_files: []