ruby_wasm_ic_installer 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.
Potentially problematic release.
This version of ruby_wasm_ic_installer might be problematic. Click here for more details.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +189 -0
- data/exe/ruby_wasm_ic_installer +6 -0
- data/lib/ruby_wasm_ic_installer/app_setup.rb +176 -0
- data/lib/ruby_wasm_ic_installer/candid.rb +262 -0
- data/lib/ruby_wasm_ic_installer/canister.rb +208 -0
- data/lib/ruby_wasm_ic_installer/cli.rb +108 -0
- data/lib/ruby_wasm_ic_installer/diff.rb +73 -0
- data/lib/ruby_wasm_ic_installer/gem_files.rb +125 -0
- data/lib/ruby_wasm_ic_installer/installer.rb +94 -0
- data/lib/ruby_wasm_ic_installer/lockfile.rb +63 -0
- data/lib/ruby_wasm_ic_installer/magic_comment.rb +60 -0
- data/lib/ruby_wasm_ic_installer/plan.rb +84 -0
- data/lib/ruby_wasm_ic_installer/runner.rb +59 -0
- data/lib/ruby_wasm_ic_installer/version.rb +5 -0
- data/lib/ruby_wasm_ic_installer.rb +21 -0
- metadata +65 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyWasmIcInstaller
|
|
4
|
+
# A file's magic comment, against the twelve encodings a `minimal` canister
|
|
5
|
+
# knows, each by one name and no alias (W26; ADR-0053's amendment of
|
|
6
|
+
# 28 October 2026). The comment is read in C before any Ruby runs, so no
|
|
7
|
+
# stand-in can help: an alias of one of the twelve is rewritten to its
|
|
8
|
+
# canonical name, found with MRI's own Encoding.find; any other encoding
|
|
9
|
+
# refuses the gem (W29, Q9). A change to the table is a release of this gem.
|
|
10
|
+
module MagicComment
|
|
11
|
+
CANISTER_ENCODINGS = %w[
|
|
12
|
+
ASCII-8BIT UTF-8 US-ASCII UTF-16BE UTF-16LE UTF-32BE UTF-32LE UTF-16 UTF-32 UTF8-MAC EUC-JP Windows-31J
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
# Ruby reads a magic comment on the first line, or the second after a
|
|
16
|
+
# shebang. `coding: x`, `coding=x`, `-*- coding: x -*-` and
|
|
17
|
+
# `encoding: x` all match.
|
|
18
|
+
PATTERN = /\A(#.*?coding[:=][ \t]*)([\w.\-]+)/
|
|
19
|
+
|
|
20
|
+
Rewrite = Struct.new(:file, :line, :from, :to, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
class Refused < Error; end
|
|
23
|
+
|
|
24
|
+
module_function
|
|
25
|
+
|
|
26
|
+
# [new_body, rewrite or nil]; raises Refused for an encoding a canister
|
|
27
|
+
# lacks or one MRI does not know.
|
|
28
|
+
def fix(file, body)
|
|
29
|
+
lines = body.b.lines
|
|
30
|
+
idx = comment_line(lines) or return [body, nil]
|
|
31
|
+
m = lines[idx].match(PATTERN)
|
|
32
|
+
name = m[2]
|
|
33
|
+
canonical = canonical!(file, name)
|
|
34
|
+
return [body, nil] if canonical.casecmp?(name)
|
|
35
|
+
|
|
36
|
+
lines[idx] = lines[idx].sub(PATTERN) { "#{m[1]}#{canonical.downcase}" }
|
|
37
|
+
[lines.join, Rewrite.new(file: file, line: idx + 1, from: name, to: canonical.downcase)]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def comment_line(lines)
|
|
41
|
+
return 0 if lines[0]&.match?(PATTERN)
|
|
42
|
+
return 1 if lines[0]&.start_with?("#!") && lines[1]&.match?(PATTERN)
|
|
43
|
+
|
|
44
|
+
nil
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def canonical!(file, name)
|
|
48
|
+
enc = begin
|
|
49
|
+
Encoding.find(name)
|
|
50
|
+
rescue ArgumentError
|
|
51
|
+
raise Refused, "#{file} names the encoding #{name.inspect}, which Ruby does not know"
|
|
52
|
+
end
|
|
53
|
+
known = CANISTER_ENCODINGS.find { _1.casecmp?(enc.name) }
|
|
54
|
+
return known if known
|
|
55
|
+
|
|
56
|
+
raise Refused, "#{file} is written in #{enc.name} (its magic comment says #{name.inspect}), which a " \
|
|
57
|
+
"minimal canister does not have: it knows #{CANISTER_ENCODINGS.join(', ')}, and transcodes nothing"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyWasmIcInstaller
|
|
4
|
+
# What one install run sends (W29, Q10): every prepared gem the canister
|
|
5
|
+
# lacks, whole, packed into install_put batches by bytes, each followed by
|
|
6
|
+
# an install_commit for the gems it completed; then one install_commit that
|
|
7
|
+
# writes the setup file, unless the pending one already says the same.
|
|
8
|
+
#
|
|
9
|
+
# A batch is at most `budget` bytes of Candid (2 MiB is the ingress limit;
|
|
10
|
+
# W28 saw 2,093,069 bytes pass and 2,101,455 refused at submission, so the
|
|
11
|
+
# default leaves room for the envelope icp-cli adds). A gem bigger than a
|
|
12
|
+
# batch spans several; .partial holds it until its commit.
|
|
13
|
+
class Plan
|
|
14
|
+
DEFAULT_BUDGET = 2_000_000
|
|
15
|
+
|
|
16
|
+
Step = Struct.new(:method, :args, :note, keyword_init: true)
|
|
17
|
+
|
|
18
|
+
attr_reader :steps, :sent, :generation
|
|
19
|
+
|
|
20
|
+
# `prepared`: GemFiles::Prepared in the lock's order (stand-ins already
|
|
21
|
+
# left out). `installed`: the canister's installed() record.
|
|
22
|
+
def initialize(prepared, installed, budget: DEFAULT_BUDGET)
|
|
23
|
+
@budget = budget
|
|
24
|
+
have = installed[:gems]
|
|
25
|
+
@sent = prepared.reject { have.include?(_1.name_version) }
|
|
26
|
+
@steps = []
|
|
27
|
+
# A version half-sent by an earlier run is cleared first: its files are
|
|
28
|
+
# resent whole, so nothing stale is committed with them.
|
|
29
|
+
(installed[:partial] & @sent.map(&:name_version)).each do |nv|
|
|
30
|
+
@steps << Step.new(method: "install_remove", args: Candid.text_arg(nv), note: "clear #{nv} from .partial")
|
|
31
|
+
end
|
|
32
|
+
batch!
|
|
33
|
+
setup!(prepared, installed)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def empty? = @steps.empty?
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def batch!
|
|
41
|
+
files = []
|
|
42
|
+
size = 0
|
|
43
|
+
done = []
|
|
44
|
+
flush = lambda do
|
|
45
|
+
next if files.empty?
|
|
46
|
+
|
|
47
|
+
@steps << Step.new(method: "install_put", args: Candid.install_files(files),
|
|
48
|
+
note: "#{files.size} files, #{files.sum { _1[:body].bytesize }} bytes")
|
|
49
|
+
unless done.empty?
|
|
50
|
+
@steps << Step.new(method: "install_commit", args: Candid.install_commit(gems: done),
|
|
51
|
+
note: "commit #{done.join(', ')}")
|
|
52
|
+
end
|
|
53
|
+
files = []
|
|
54
|
+
size = 0
|
|
55
|
+
done = []
|
|
56
|
+
end
|
|
57
|
+
@sent.each do |gem|
|
|
58
|
+
gem.files.each do |rel, body|
|
|
59
|
+
path = "#{gem.name_version}/#{rel}"
|
|
60
|
+
one = Candid.install_file_size(path, body)
|
|
61
|
+
if one + Candid.install_files_overhead(1) > @budget
|
|
62
|
+
raise Error, "#{path} is #{body.bytesize} bytes, more than one call can carry (#{@budget})"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
flush.call if size + one + Candid.install_files_overhead(files.size + 1) > @budget
|
|
66
|
+
files << { path: path, body: body }
|
|
67
|
+
size += one
|
|
68
|
+
end
|
|
69
|
+
done << gem.name_version
|
|
70
|
+
end
|
|
71
|
+
flush.call
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def setup!(prepared, installed)
|
|
75
|
+
paths = prepared.flat_map { |g| g.require_paths.map { "#{g.name_version}/#{_1}" } }
|
|
76
|
+
return if installed[:pending] && installed[:load_paths] == paths
|
|
77
|
+
|
|
78
|
+
@generation = [installed[:pending], installed[:running]].compact.max.to_i + 1
|
|
79
|
+
@steps << Step.new(method: "install_commit",
|
|
80
|
+
args: Candid.install_commit(gems: [], setup: { generation: @generation, load_paths: paths }),
|
|
81
|
+
note: "write setup generation #{@generation} (#{paths.size} load paths)")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
module RubyWasmIcInstaller
|
|
7
|
+
# The one way the off-chain half reaches a canister (W29, Q13):
|
|
8
|
+
# `icp canister call <canister> <method> --args-file <file> --args-format
|
|
9
|
+
# bin -o hex`, with `--query` for a query. The author's icp.yaml,
|
|
10
|
+
# environment, network and identity pass through as `icp_args`; the
|
|
11
|
+
# Installer holds no keys and picks no network. A file carries the
|
|
12
|
+
# arguments because one command-line argument is capped at 128 KiB and a
|
|
13
|
+
# batch is up to 2 MiB.
|
|
14
|
+
#
|
|
15
|
+
# It is also the test seam: the specs give the Installer a runner whose
|
|
16
|
+
# #call answers from a canister held in memory.
|
|
17
|
+
class Runner
|
|
18
|
+
class CallError < Error
|
|
19
|
+
attr_reader :refusal
|
|
20
|
+
|
|
21
|
+
def initialize(message, refusal: nil)
|
|
22
|
+
super(message)
|
|
23
|
+
@refusal = refusal
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# `spawn` is called as spawn.(*argv) and returns [stdout, stderr, status];
|
|
28
|
+
# the default is Open3.capture3.
|
|
29
|
+
def initialize(canister:, icp: "icp", icp_args: [], spawn: nil)
|
|
30
|
+
@canister = canister
|
|
31
|
+
@icp = icp
|
|
32
|
+
@icp_args = icp_args
|
|
33
|
+
@spawn = spawn || Open3.method(:capture3)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# The reply's bytes.
|
|
37
|
+
def call(method, args, query: false)
|
|
38
|
+
Tempfile.create(["ruby_wasm_ic_installer", ".bin"], binmode: true) do |f|
|
|
39
|
+
f.write(args)
|
|
40
|
+
f.flush
|
|
41
|
+
argv = [@icp, "canister", "call", @canister, method, "--args-file", f.path, "--args-format", "bin",
|
|
42
|
+
"-o", "hex", *(query ? ["--query"] : []), *@icp_args]
|
|
43
|
+
out, err, status = @spawn.call(*argv)
|
|
44
|
+
raise CallError.new("#{method}: #{err.strip.empty? ? out.strip : err.strip}", refusal: refusal(err)) \
|
|
45
|
+
unless status.success?
|
|
46
|
+
|
|
47
|
+
hex = out.strip
|
|
48
|
+
raise CallError, "#{method}: icp replied #{out.inspect[0, 200]}, not hex" unless hex.match?(/\A(?:[0-9a-fA-F]{2})+\z/)
|
|
49
|
+
|
|
50
|
+
[hex].pack("H*")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# The text after `refused: ` in a trap message, if the canister refused.
|
|
57
|
+
def refusal(err) = err[/refused: (.*?)(?:'|"|$)/, 1]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# ruby_wasm_ic_installer: the Installer (W29). Its off-chain half, loaded
|
|
4
|
+
# here, sends an Application's pure-Ruby gems into a Ruby canister packed by
|
|
5
|
+
# ruby_wasm_builder; its canister half, ruby_wasm_ic_installer/canister.rb,
|
|
6
|
+
# is carried by the Application and is never loaded by this file.
|
|
7
|
+
module RubyWasmIcInstaller
|
|
8
|
+
class Error < StandardError; end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
require_relative "ruby_wasm_ic_installer/version"
|
|
12
|
+
require_relative "ruby_wasm_ic_installer/candid"
|
|
13
|
+
require_relative "ruby_wasm_ic_installer/lockfile"
|
|
14
|
+
require_relative "ruby_wasm_ic_installer/magic_comment"
|
|
15
|
+
require_relative "ruby_wasm_ic_installer/gem_files"
|
|
16
|
+
require_relative "ruby_wasm_ic_installer/plan"
|
|
17
|
+
require_relative "ruby_wasm_ic_installer/runner"
|
|
18
|
+
require_relative "ruby_wasm_ic_installer/installer"
|
|
19
|
+
require_relative "ruby_wasm_ic_installer/diff"
|
|
20
|
+
require_relative "ruby_wasm_ic_installer/app_setup"
|
|
21
|
+
require_relative "ruby_wasm_ic_installer/cli"
|
metadata
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ruby_wasm_ic_installer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- the ruby_wasm_builder authors
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: 'The Installer for canisters packed by ruby_wasm_builder: reads an Application''s
|
|
14
|
+
Gemfile.lock, fetches its pure-Ruby gems and sends the ones a canister lacks through
|
|
15
|
+
`icp canister call`, into /data, where a carried canister half loads them at the
|
|
16
|
+
next boot. `setup` wires the canister half into an Application.'
|
|
17
|
+
email:
|
|
18
|
+
executables:
|
|
19
|
+
- ruby_wasm_ic_installer
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.md
|
|
25
|
+
- exe/ruby_wasm_ic_installer
|
|
26
|
+
- lib/ruby_wasm_ic_installer.rb
|
|
27
|
+
- lib/ruby_wasm_ic_installer/app_setup.rb
|
|
28
|
+
- lib/ruby_wasm_ic_installer/candid.rb
|
|
29
|
+
- lib/ruby_wasm_ic_installer/canister.rb
|
|
30
|
+
- lib/ruby_wasm_ic_installer/cli.rb
|
|
31
|
+
- lib/ruby_wasm_ic_installer/diff.rb
|
|
32
|
+
- lib/ruby_wasm_ic_installer/gem_files.rb
|
|
33
|
+
- lib/ruby_wasm_ic_installer/installer.rb
|
|
34
|
+
- lib/ruby_wasm_ic_installer/lockfile.rb
|
|
35
|
+
- lib/ruby_wasm_ic_installer/magic_comment.rb
|
|
36
|
+
- lib/ruby_wasm_ic_installer/plan.rb
|
|
37
|
+
- lib/ruby_wasm_ic_installer/runner.rb
|
|
38
|
+
- lib/ruby_wasm_ic_installer/version.rb
|
|
39
|
+
homepage: https://github.com/graial/ruby_wasm_ic_installer
|
|
40
|
+
licenses:
|
|
41
|
+
- MIT
|
|
42
|
+
metadata:
|
|
43
|
+
source_code_uri: https://github.com/graial/ruby_wasm_ic_installer
|
|
44
|
+
rubygems_mfa_required: 'true'
|
|
45
|
+
post_install_message:
|
|
46
|
+
rdoc_options: []
|
|
47
|
+
require_paths:
|
|
48
|
+
- lib
|
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.2'
|
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '0'
|
|
59
|
+
requirements: []
|
|
60
|
+
rubygems_version: 3.5.11
|
|
61
|
+
signing_key:
|
|
62
|
+
specification_version: 4
|
|
63
|
+
summary: Install pure-Ruby gems into a Ruby canister on the Internet Computer, through
|
|
64
|
+
calls
|
|
65
|
+
test_files: []
|