cinnabar 0.0.0 → 0.0.2
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/.rubocop.yml +1 -1
- data/.yardopts +4 -0
- data/docs/ChangeLog.md +5 -0
- data/docs/Readme-zh.md +3 -35
- data/docs/Readme.md +96 -2
- data/lib/cinnabar/00_pre.rb +3 -4
- data/lib/cinnabar/cmd_runner.rb +437 -88
- data/lib/cinnabar/net.rb +49 -19
- data/lib/cinnabar/path.rb +253 -0
- data/lib/cinnabar/pipe.rb +5 -5
- data/lib/cinnabar/version.rb +1 -1
- data/lib/cinnabar.rb +1 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0d07b1f0ca422a414fa67b7017920940bd12eadfe4f2d104de088e6ce67c9def
|
|
4
|
+
data.tar.gz: a2065bf84722d56727ce1d12602ef3d498d0026ba420f3e213ce5b9d24fdd558
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ca95b7d444e89dd2c169d16b1255779c482dd4ed36bda09f7050a2d6c99f1049e27e52c660b4a2c35f0045b0c668a2954f7b982006257f3f74dcdb0a71a9d03c
|
|
7
|
+
data.tar.gz: 7da5135f3602cf662ea4bbc53e86cada66dc5d5601223600beaf53ac56d05262d7bb518a18cbfa2c22fc9128184032882c885e2906c438c3a7c7a00de8c3a69f
|
data/.rubocop.yml
CHANGED
data/.yardopts
ADDED
data/docs/ChangeLog.md
ADDED
data/docs/Readme-zh.md
CHANGED
|
@@ -24,41 +24,9 @@
|
|
|
24
24
|
2. 朱砂有毒。这个项目是为了 **猛、糙、快** (a.k.a. *Dirty and Quick*) 的目的而开发的,可能会产生意料之外的副作用(它并非完全无害)。
|
|
25
25
|
3. 朱砂是一种硫化汞 (HgS) 的矿物,呈深红色,而 Ruby 也是一种深红色的宝石。给一个 Ruby 项目取名为 “Cinnabar(朱砂)” 非常贴切。
|
|
26
26
|
|
|
27
|
-
##
|
|
27
|
+
## API DOC
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+

|
|
30
30
|
|
|
31
|
-
|
|
32
|
-
env:
|
|
33
|
-
# Speeds up script startup by disabling RubyGems
|
|
34
|
-
RUBYOPT: "--disable=gems"
|
|
35
|
-
default_ci_shell: ruby cinnabar/ci.rb {0}
|
|
31
|
+
- Github Pages: <https://2moe.github.io/cinnabar>
|
|
36
32
|
|
|
37
|
-
jobs:
|
|
38
|
-
build:
|
|
39
|
-
runs-on: ubuntu-latest
|
|
40
|
-
defaults:
|
|
41
|
-
run:
|
|
42
|
-
shell: ${{env.default_ci_shell}}
|
|
43
|
-
steps:
|
|
44
|
-
- uses: actions/checkout@v6
|
|
45
|
-
|
|
46
|
-
- name: clone cinnabar
|
|
47
|
-
uses: actions/checkout@v6
|
|
48
|
-
with:
|
|
49
|
-
repository: 2moe/cinnabar
|
|
50
|
-
path: cinnabar
|
|
51
|
-
ref: v0.0.0
|
|
52
|
-
|
|
53
|
-
- name: (example) run cargo command
|
|
54
|
-
run: |
|
|
55
|
-
{
|
|
56
|
-
cargo: (),
|
|
57
|
-
build: (),
|
|
58
|
-
profile: 'release',
|
|
59
|
-
verbose: true,
|
|
60
|
-
target: 'x86_64-unknown-linux-musl'
|
|
61
|
-
}
|
|
62
|
-
.to_argv
|
|
63
|
-
.run
|
|
64
|
-
```
|
data/docs/Readme.md
CHANGED
|
@@ -24,6 +24,12 @@ A:
|
|
|
24
24
|
2. Cinnabar is toxic. This project was developed for *Dirty and Quick* purposes and may produce unexpected side effects—in a sense, it is not entirely harmless.
|
|
25
25
|
3. Cinnabar, a mineral form of mercury sulfide (HgS), is a deep red-colored stone. And ruby is also a deep red stone. Naming a Ruby project "Cinnabar" is particularly fitting.
|
|
26
26
|
|
|
27
|
+
## API DOC
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
- Github Pages: <https://2moe.github.io/cinnabar>
|
|
32
|
+
|
|
27
33
|
## Quick Start
|
|
28
34
|
|
|
29
35
|
Github Actions for cinnabar
|
|
@@ -33,6 +39,8 @@ env:
|
|
|
33
39
|
# Speeds up script startup by disabling RubyGems
|
|
34
40
|
RUBYOPT: "--disable=gems"
|
|
35
41
|
default_ci_shell: ruby cinnabar/ci.rb {0}
|
|
42
|
+
# optional values: debug, info, warn, error, fatal, unknown
|
|
43
|
+
RUBY_LOG: debug
|
|
36
44
|
|
|
37
45
|
jobs:
|
|
38
46
|
build:
|
|
@@ -48,7 +56,7 @@ jobs:
|
|
|
48
56
|
with:
|
|
49
57
|
repository: 2moe/cinnabar
|
|
50
58
|
path: cinnabar
|
|
51
|
-
ref: v0.0.
|
|
59
|
+
ref: v0.0.2
|
|
52
60
|
|
|
53
61
|
- name: (example) run cargo command
|
|
54
62
|
run: |
|
|
@@ -60,5 +68,91 @@ jobs:
|
|
|
60
68
|
target: 'x86_64-unknown-linux-musl'
|
|
61
69
|
}
|
|
62
70
|
.to_argv
|
|
63
|
-
.
|
|
71
|
+
.run_cmd
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Examples
|
|
75
|
+
|
|
76
|
+
### Command Runner
|
|
77
|
+
|
|
78
|
+
#### `.run` + pass stdin data
|
|
79
|
+
|
|
80
|
+
```ruby,yaml
|
|
81
|
+
- run: |
|
|
82
|
+
opts = { stdin_data: "Hello", allow_failure: true }
|
|
83
|
+
stdout = %w[wc -m].run(opts:)
|
|
84
|
+
|
|
85
|
+
stdout.to_i == 5 #=> true
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### `.async_run` + log
|
|
89
|
+
|
|
90
|
+
```ruby,yaml
|
|
91
|
+
- run: |
|
|
92
|
+
'building wasi file...'.log_dbg
|
|
93
|
+
task = {
|
|
94
|
+
cargo: (),
|
|
95
|
+
b: (),
|
|
96
|
+
r: true,
|
|
97
|
+
target: 'wasm32-wasip2'
|
|
98
|
+
} .to_argv
|
|
99
|
+
.async_run
|
|
100
|
+
|
|
101
|
+
# log_dbg, log_info, log_warn, log_err, log_fatal, log_unk
|
|
102
|
+
"You can now do other things without waiting for
|
|
103
|
+
the process to complete.".log_dbg
|
|
104
|
+
|
|
105
|
+
stdout, status = task.wait_with_output
|
|
106
|
+
stdout.log_info
|
|
107
|
+
raise "wasi" unless status.success?
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### `.async_run` + pass stdin data
|
|
111
|
+
|
|
112
|
+
```ruby,yaml
|
|
113
|
+
- run: |
|
|
114
|
+
stdin_data = <<~'QMP_JSON'
|
|
115
|
+
{ "execute":"qmp_capabilities" }
|
|
116
|
+
{ "execute":"query-cpu-model-expansion",
|
|
117
|
+
"arguments":{"type":"full","model":{"name":"host"}} }
|
|
118
|
+
{ "execute":"quit" }
|
|
119
|
+
QMP_JSON
|
|
120
|
+
|
|
121
|
+
# opts = { stdin_data:, stdin_binmode: false }
|
|
122
|
+
opts = { stdin_data: }
|
|
123
|
+
|
|
124
|
+
accel = %w[kvm hvf whpx].join ':'
|
|
125
|
+
task = {
|
|
126
|
+
'qemu-system-x86_64': (),
|
|
127
|
+
machine: "accel=#{accel}",
|
|
128
|
+
cpu: 'host',
|
|
129
|
+
display: 'none',
|
|
130
|
+
nodefaults: true,
|
|
131
|
+
no_user_config: true,
|
|
132
|
+
qmp: 'stdio',
|
|
133
|
+
} .to_argv_bsd
|
|
134
|
+
.async_run(opts:)
|
|
135
|
+
|
|
136
|
+
stdout, status = task.wait_with_output
|
|
137
|
+
stdout.log_info if status.success?
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Downloader
|
|
141
|
+
|
|
142
|
+
```ruby,yaml
|
|
143
|
+
- run: |
|
|
144
|
+
url = 'https://docs.ruby-lang.org/en/master'
|
|
145
|
+
url.download
|
|
146
|
+
# OR: url.download({out_dir: "/tmp", file_name: "index.html"})
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Function Pipe
|
|
150
|
+
|
|
151
|
+
```ruby,yaml
|
|
152
|
+
- run: |
|
|
153
|
+
upper = ->s { s.upcase }
|
|
154
|
+
|
|
155
|
+
'Foo'
|
|
156
|
+
.▷(upper)
|
|
157
|
+
.▷ :puts #=> "FOO"
|
|
64
158
|
```
|
data/lib/cinnabar/00_pre.rb
CHANGED
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
# Define Cinnabar first to prevent errors when creating Cinnabar::SubMod
|
|
4
4
|
# (compact ClassAndModuleChildren)
|
|
5
|
-
#
|
|
6
5
|
# ------------------
|
|
7
6
|
|
|
7
|
+
# @see https://github.com/2moe/cinnabar
|
|
8
8
|
module Cinnabar; end
|
|
9
9
|
|
|
10
|
+
require 'pathname'
|
|
11
|
+
|
|
10
12
|
# To ensure compatibility with "--disable=gems" (allowing users to pre-require),
|
|
11
13
|
# add conditional checks before requiring these libraries.
|
|
12
14
|
require 'sinlog' unless defined? Sinlog::VERSION
|
|
13
|
-
# require 'argvise' unless defined? Argvise::VERSION
|
|
14
|
-
|
|
15
|
-
require 'pathname'
|
data/lib/cinnabar/cmd_runner.rb
CHANGED
|
@@ -1,162 +1,511 @@
|
|
|
1
|
+
# typed: false
|
|
1
2
|
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
module Cinnabar::Command
|
|
4
5
|
module_function
|
|
5
6
|
|
|
7
|
+
require 'open3'
|
|
6
8
|
using Sinlog::Refin
|
|
7
9
|
|
|
8
|
-
#
|
|
10
|
+
# Executes the command synchronously (blocking) and returns its standard output.
|
|
9
11
|
#
|
|
10
|
-
#
|
|
12
|
+
# @raise [RuntimeError] when `allow_failure: false` and the process exits with non-zero status
|
|
11
13
|
#
|
|
12
|
-
#
|
|
14
|
+
# @example pass env
|
|
13
15
|
#
|
|
14
16
|
# Cmd = Cinnabar::Command
|
|
15
|
-
# %w[
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
# cmd_arr = %w[sh -c] << 'printf $WW'
|
|
18
|
+
# env_hash = {WW: 2}
|
|
19
|
+
# opts = {allow_failure: true}
|
|
20
|
+
# output = Cmd.run(cmd_arr, env_hash, opts:)
|
|
21
|
+
# output.to_i == 2 #=> true
|
|
22
|
+
#
|
|
23
|
+
# @example pass stdin data
|
|
24
|
+
#
|
|
25
|
+
# opts = {stdin_data: "Hello\nWorld\n"}
|
|
26
|
+
# output = Cinnabar::Command.run(%w[wc -l], opts:)
|
|
27
|
+
# output.to_i == 2 #=> true
|
|
28
|
+
#
|
|
29
|
+
# @param cmd_arr [Array<String>] The command and its arguments (e.g., `%w[printf hello]`).
|
|
30
|
+
# @param env_hash [#to_h] Environment variables to pass to the command.
|
|
31
|
+
# @param opts [Hash]
|
|
32
|
+
#
|
|
33
|
+
# - Only the `:allow_failure` is extracted and handled explicitly;
|
|
34
|
+
# - all other keys are passed through to **Open3.capture2** unchanged.
|
|
35
|
+
#
|
|
36
|
+
# @option opts [Boolean] :allow_failure
|
|
37
|
+
# Indicates whether the command is allowed to fail.
|
|
38
|
+
#
|
|
39
|
+
# @return [String, nil] the standard output of the command.
|
|
40
|
+
#
|
|
41
|
+
# @see async_run
|
|
42
|
+
def run(cmd_arr, env_hash = nil, opts: {}) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
|
43
|
+
'Running and capturing the output of a system command.'.log_dbg
|
|
44
|
+
cmd_arr.log_info
|
|
45
|
+
"opts: #{opts}".log_dbg
|
|
46
|
+
|
|
47
|
+
allow_failure = opts.delete(:allow_failure) || false
|
|
48
|
+
|
|
49
|
+
final_env = normalize_env(env_hash)
|
|
50
|
+
|
|
51
|
+
begin
|
|
52
|
+
stdout, status =
|
|
53
|
+
if final_env.nil?
|
|
54
|
+
Open3.capture2(*cmd_arr, opts)
|
|
55
|
+
else
|
|
56
|
+
Open3.capture2(final_env, *cmd_arr, opts)
|
|
57
|
+
end
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
Kernel.raise e unless allow_failure
|
|
60
|
+
e.log_err
|
|
61
|
+
return stdout
|
|
22
62
|
end
|
|
63
|
+
|
|
64
|
+
return stdout if status.success?
|
|
65
|
+
|
|
66
|
+
err_msg = "Command failed: #{cmd_arr.join(' ')}"
|
|
67
|
+
Kernel.raise err_msg unless allow_failure
|
|
68
|
+
|
|
69
|
+
err_msg.log_err
|
|
70
|
+
stdout
|
|
23
71
|
end
|
|
24
72
|
|
|
25
|
-
#
|
|
73
|
+
# Executes a system command using Ruby's `Kernel.system`.
|
|
26
74
|
#
|
|
27
|
-
#
|
|
75
|
+
# It runs the command synchronously, blocks until completion, and does not capture stdout or stderr.
|
|
28
76
|
#
|
|
29
|
-
#
|
|
77
|
+
# @param cmd_arr [Array<String>] The command and its arguments as an array,
|
|
78
|
+
# e.g., `%w[ls -lh]`.
|
|
79
|
+
#
|
|
80
|
+
# @param env_hash [#to_h] Environment variables to pass to the command.
|
|
81
|
+
# @param opts [Hash]
|
|
82
|
+
#
|
|
83
|
+
# - Only the `:allow_failure` is extracted and handled explicitly;
|
|
84
|
+
# - all other keys are passed through to `Kernel.system` unchanged.
|
|
85
|
+
#
|
|
86
|
+
# @option opts [Boolean] :allow_failure
|
|
87
|
+
# Indicates whether the command is allowed to fail.
|
|
88
|
+
# If true, the method will return false instead of raising an exception when the
|
|
89
|
+
# command exits with a non-zero status.
|
|
90
|
+
#
|
|
91
|
+
# @see https://docs.ruby-lang.org/en/3.4/Process.html#module-Process-label-Execution+Options
|
|
92
|
+
#
|
|
93
|
+
# @example pwd
|
|
30
94
|
#
|
|
31
95
|
# Cmd = Cinnabar::Command
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
96
|
+
# opts = {chdir: '/tmp', allow_failure: true}
|
|
97
|
+
# Cmd.run_cmd(%w[pwd], opts:)
|
|
98
|
+
#
|
|
99
|
+
# @example pass env
|
|
100
|
+
#
|
|
101
|
+
# Cmd = Cinnabar::Command
|
|
102
|
+
# cmd_arr = %w[sh -c] << 'printf $WW'
|
|
103
|
+
# env_hash = {WW: 2}
|
|
104
|
+
# Cmd.run_cmd(cmd_arr, env_hash)
|
|
105
|
+
#
|
|
106
|
+
# @return [Boolean] Returns true if the command succeeds (exit status 0),
|
|
107
|
+
# or false if it fails and `allow_failure` is true.
|
|
108
|
+
# @raise [RuntimeError] Raises an error if the command fails and `allow_failure` is false.
|
|
109
|
+
def run_cmd(cmd_arr, env_hash = nil, opts: {})
|
|
110
|
+
'Running system command'.log_dbg
|
|
111
|
+
cmd_arr.log_info
|
|
112
|
+
"opts: #{opts}".log_dbg
|
|
113
|
+
|
|
114
|
+
allow_failure = opts.delete(:allow_failure) || false
|
|
115
|
+
exception = !allow_failure
|
|
116
|
+
"exception: #{exception}".log_dbg
|
|
117
|
+
options = opts.merge({ exception: })
|
|
118
|
+
|
|
119
|
+
final_env = normalize_env(env_hash)
|
|
120
|
+
if final_env.nil?
|
|
121
|
+
Kernel.system(*cmd_arr, options)
|
|
122
|
+
else
|
|
123
|
+
Kernel.system(final_env, *cmd_arr, options)
|
|
38
124
|
end
|
|
39
125
|
end
|
|
40
126
|
|
|
41
|
-
#
|
|
127
|
+
# @param hash [#to_h]
|
|
128
|
+
# @return [Hash{String => String}, nil] a hash where both keys and values are strings
|
|
129
|
+
def normalize_env(hash)
|
|
130
|
+
return nil if hash.nil?
|
|
131
|
+
return nil if hash.respond_to?(:empty?) && hash.empty?
|
|
132
|
+
|
|
133
|
+
hash.to_h { |k, v| [k.to_s, v.to_s] }
|
|
134
|
+
.tap { "normalized_env:#{_1}".log_dbg }
|
|
135
|
+
end
|
|
42
136
|
|
|
43
|
-
#
|
|
137
|
+
# Launch a command asynchronously (non-blocking) and return its stdout stream and process waiter.
|
|
44
138
|
#
|
|
45
|
-
# This
|
|
46
|
-
#
|
|
139
|
+
# This is a sugar over **Open3.popen2**, intended to start a subprocess
|
|
140
|
+
# and **immediately** hand back:
|
|
47
141
|
#
|
|
48
|
-
#
|
|
142
|
+
# 1. an `IO` for reading the command's stdout, and
|
|
143
|
+
# 2. a `Process::Waiter` (a thread-like object) that can be awaited later.
|
|
49
144
|
#
|
|
50
|
-
#
|
|
145
|
+
# - If `:stdin_data` is provided, the data will be written to
|
|
146
|
+
# the child's stdin and the stdin will be closed.
|
|
147
|
+
# - When `:stdin_data` is absent, stdin is simply closed and
|
|
148
|
+
# the method returns immediately without blocking on output.
|
|
149
|
+
#
|
|
150
|
+
# @param cmd_arr [Array<String>] The command and its arguments (e.g., `%w[printf hello]`).
|
|
151
|
+
# @param env_hash [#to_h] Optional environment variables;
|
|
152
|
+
# keys/values will be normalized by {#normalize_env} before being passed to the child.
|
|
153
|
+
#
|
|
154
|
+
# @param opts [Hash] Additional options.
|
|
155
|
+
#
|
|
156
|
+
# - Only the following keys are extracted and handled explicitly;
|
|
157
|
+
# - :stdin_data
|
|
158
|
+
# - :binmode
|
|
159
|
+
# - :stdin_binmode
|
|
160
|
+
# - :stdout_binmode
|
|
161
|
+
#
|
|
162
|
+
# all other keys are passed through to **Open3.popen2** unchanged.
|
|
163
|
+
#
|
|
164
|
+
# @option opts [String, #readpartial] :stdin_data
|
|
165
|
+
# Data to write to the child's stdin. If it responds to `#readpartial`,
|
|
166
|
+
# it will be streamed via **IO.copy_stream**;
|
|
167
|
+
#
|
|
168
|
+
# @option opts [Boolean] :binmode
|
|
169
|
+
# When `true`, set both stdin and stdout to binary mode (useful for binary data).
|
|
170
|
+
#
|
|
171
|
+
# @option opts [Boolean] :stdin_binmode
|
|
172
|
+
# Sets only stdin to binary mode.
|
|
173
|
+
#
|
|
174
|
+
# @option opts [Boolean] :stdout_binmode
|
|
175
|
+
# Sets only stdout to binary mode.
|
|
176
|
+
#
|
|
177
|
+
# @return [Array(IO, Process::Waiter)] A pair `[stdout_io, waiter]`:
|
|
178
|
+
#
|
|
179
|
+
# - `stdout_io` is an `IO` for reading **stdout**
|
|
180
|
+
# - `waiter` is a `Process::Waiter`;
|
|
181
|
+
# - call `waiter.value` to get `Process::Status`;
|
|
182
|
+
# - or `waiter.join` to block until the process exits.
|
|
183
|
+
#
|
|
184
|
+
# @raise [StandardError] Reraises any non-`Errno::EPIPE` exception encountered while writing to stdin.
|
|
185
|
+
# `Errno::EPIPE` is logged and swallowed.
|
|
186
|
+
#
|
|
187
|
+
# @example start a process and later wait for it
|
|
188
|
+
#
|
|
189
|
+
# Cmd = Cinnabar::Command
|
|
190
|
+
# stdout_fd, waiter = Cmd.async_run(['sh', '-c', 'echo hello; sleep 1; echo done'])
|
|
191
|
+
#
|
|
192
|
+
# output, status = Cmd.wait_with_output(stdout_fd, waiter)
|
|
193
|
+
#
|
|
194
|
+
# @example pass stdin data
|
|
51
195
|
#
|
|
52
196
|
# Cmd = Cinnabar::Command
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
197
|
+
# opts = {stdin_data: "Run in the background" }
|
|
198
|
+
# io_and_waiter = Cmd.async_run(%w[wc -m], opts:)
|
|
199
|
+
#
|
|
200
|
+
# output, status = Cmd.wait_with_output(*io_and_waiter)
|
|
201
|
+
# status.success? #=> true
|
|
202
|
+
# output.to_i == 21 #=> true
|
|
203
|
+
#
|
|
204
|
+
# @see wait_with_output
|
|
205
|
+
# @see run
|
|
206
|
+
def async_run(cmd_arr, env_hash = nil, opts: {}) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity
|
|
207
|
+
"Asynchronously executing system command: #{cmd_arr}".log_dbg
|
|
208
|
+
"opts: #{opts}".log_dbg
|
|
209
|
+
|
|
210
|
+
stdin_data = opts.delete(:stdin_data)
|
|
211
|
+
binmode = opts.delete(:binmode)
|
|
212
|
+
stdin_binmode = opts.delete(:stdin_binmode)
|
|
213
|
+
stdout_binmode = opts.delete(:stdout_binmode)
|
|
214
|
+
|
|
215
|
+
'async_run() does not support the :allow_failure option.'.log_warn if opts.delete(:allow_failure)
|
|
216
|
+
|
|
217
|
+
final_env = normalize_env(env_hash)
|
|
218
|
+
|
|
219
|
+
stdin, stdout, waiter =
|
|
220
|
+
if final_env.nil?
|
|
221
|
+
Open3.popen2(*cmd_arr, opts)
|
|
222
|
+
else
|
|
223
|
+
Open3.popen2(final_env, *cmd_arr, opts)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
if binmode
|
|
227
|
+
stdin.binmode
|
|
228
|
+
stdout.binmode
|
|
229
|
+
else
|
|
230
|
+
stdin.binmode if stdin_binmode
|
|
231
|
+
stdout.binmode if stdout_binmode
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Non-blocking: no stdin to write; return immediately
|
|
235
|
+
unless stdin_data
|
|
236
|
+
stdin.close
|
|
237
|
+
return [stdout, waiter]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
begin
|
|
241
|
+
if stdin_data.respond_to? :readpartial
|
|
242
|
+
IO.copy_stream(stdin_data, stdin)
|
|
243
|
+
else
|
|
244
|
+
stdin.write stdin_data
|
|
245
|
+
end
|
|
246
|
+
rescue Errno::EPIPE => e
|
|
247
|
+
e.log_err
|
|
248
|
+
rescue StandardError => e
|
|
249
|
+
"Failed to write stdin data: #{e}".log_err
|
|
250
|
+
Kernel.raise e
|
|
251
|
+
ensure
|
|
252
|
+
stdin.close
|
|
64
253
|
end
|
|
254
|
+
[stdout, waiter]
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Waits for a process to finish and reads all remaining output from its stdout.
|
|
258
|
+
#
|
|
259
|
+
# @param io_fd [IO] The IO object connected to the process's stdout (or combined stdout & stderr).
|
|
260
|
+
# @param waiter [Process::Waiter] The waiter thread returned by **Open3.popen2** or **Open3.popen2e**.
|
|
261
|
+
#
|
|
262
|
+
# @return [Array(String, Process::Status)] A two-element array:
|
|
263
|
+
#
|
|
264
|
+
# - The full output read from `io_fd`.
|
|
265
|
+
# - The **Process::Status** object representing the process's exit status.
|
|
266
|
+
#
|
|
267
|
+
# @example Wait for process and capture output
|
|
268
|
+
#
|
|
269
|
+
# require 'sinlog'
|
|
270
|
+
# using Sinlog::Refin
|
|
271
|
+
#
|
|
272
|
+
# Cmd = Cinnabar::Command
|
|
273
|
+
#
|
|
274
|
+
# fd, waiter = %w[ruby -e].push('sleep 2; puts "OK"')
|
|
275
|
+
# .then { Cmd.async_run(_1) }
|
|
276
|
+
#
|
|
277
|
+
# "You can now do other things without waiting for the process to complete.".log_dbg
|
|
278
|
+
#
|
|
279
|
+
# "blocking wait".log_info
|
|
280
|
+
# output, status = Cmd.wait_with_output(fd, waiter)
|
|
281
|
+
#
|
|
282
|
+
# "Exit code: #{status.exitstatus}".log_warn unless status.success?
|
|
283
|
+
# "Output:\n#{output}".log_info
|
|
284
|
+
#
|
|
285
|
+
# @note This method blocks until the process exits and all output is read.
|
|
286
|
+
# @see async_run
|
|
287
|
+
def wait_with_output(io_fd, waiter)
|
|
288
|
+
status = waiter.value
|
|
289
|
+
output = io_fd.read
|
|
290
|
+
io_fd.close
|
|
291
|
+
[output, status]
|
|
65
292
|
end
|
|
66
|
-
# ---------------
|
|
67
293
|
end
|
|
68
294
|
|
|
69
295
|
module Cinnabar::Command
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
296
|
+
# The foundation of {ArrRefin} and {ArrMixin}
|
|
297
|
+
# @see Cinnabar::Command.run
|
|
298
|
+
# @see Cinnabar::Command.async_run
|
|
299
|
+
# @see Cinnabar::Command.run_cmd
|
|
300
|
+
module ArrExt
|
|
301
|
+
# Executes the command synchronously (blocking) and returns its standard output.
|
|
302
|
+
#
|
|
303
|
+
# @note self [`Array<String>`]: The command and its arguments (e.g., `%w[printf hello]`).
|
|
304
|
+
#
|
|
305
|
+
# @param env_hash [#to_h] Environment variables to pass to the command.
|
|
306
|
+
# @param opts [Hash]
|
|
307
|
+
#
|
|
308
|
+
# - Only the `:allow_failure` is extracted and handled explicitly;
|
|
309
|
+
# - all other keys are passed through to **Open3.capture2** unchanged.
|
|
310
|
+
#
|
|
311
|
+
# @raise [StandardError] when `allow_failure: false` and the process exits with non-zero status
|
|
312
|
+
#
|
|
313
|
+
# @return [String, nil] the standard output of the command.
|
|
314
|
+
# @see Cinnabar::Command.run
|
|
315
|
+
#
|
|
316
|
+
# @example pass stdin data
|
|
317
|
+
#
|
|
318
|
+
# using Cinnabar::Command::ArrRefin
|
|
319
|
+
# # OR: include Cinnabar::Command::ArrMixin
|
|
320
|
+
#
|
|
321
|
+
# opts = {allow_failure: true, stdin_data: "Hello\nWorld\n"}
|
|
322
|
+
# output = %w[wc -l].run(opts:)
|
|
323
|
+
# output.to_i == 2 unless output.nil? #=> true
|
|
324
|
+
#
|
|
325
|
+
# @note This method blocks until the process completes.
|
|
326
|
+
def run(env_hash = nil, opts: {})
|
|
327
|
+
Cinnabar::Command.run(self, env_hash, opts:)
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Starts a command asynchronously using this `Array<String>`.
|
|
331
|
+
#
|
|
332
|
+
# @note self [`Array<String>`]: The command and its arguments (e.g., `%w[printf hello]`).
|
|
333
|
+
#
|
|
334
|
+
# @param env_hash [#to_h] Optional environment variables.
|
|
335
|
+
# @param opts [Hash]
|
|
336
|
+
#
|
|
337
|
+
# @see Cinnabar::Command.async_run
|
|
338
|
+
#
|
|
339
|
+
# @return [Array(IO, Process::Waiter)] a pair `[stdout_io, waiter]`
|
|
340
|
+
#
|
|
341
|
+
# @example pass stdin data
|
|
342
|
+
#
|
|
343
|
+
# using Cinnabar::Command::ArrRefin
|
|
344
|
+
# # OR: include Cinnabar::Command::ArrMixin
|
|
345
|
+
#
|
|
346
|
+
# opts = {stdin_data: "Hello\nWorld\n"}
|
|
347
|
+
# io_and_waiter = %w[wc -l].async_run(opts:)
|
|
348
|
+
# output, status = Cinnabar::Command.wait_with_output *io_and_waiter
|
|
349
|
+
# output.to_i == 2 #=> true
|
|
350
|
+
def async_run(env_hash = nil, opts: {})
|
|
351
|
+
Cinnabar::Command.async_run(self, env_hash, opts:)
|
|
73
352
|
end
|
|
74
353
|
|
|
75
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
354
|
+
# @note self [`Array<String>`]: The command and its arguments (e.g., `%w[printf hello]`).
|
|
355
|
+
#
|
|
356
|
+
# @example pwd
|
|
357
|
+
#
|
|
358
|
+
# using Cinnabar::Command::ArrRefin
|
|
359
|
+
# # OR: include Cinnabar::Command::ArrMixin
|
|
360
|
+
#
|
|
361
|
+
# opts = {chdir: '/tmp', allow_failure: true}
|
|
362
|
+
# status = %w[pwd].run_cmd(opts:)
|
|
363
|
+
#
|
|
364
|
+
# @example pass env
|
|
365
|
+
#
|
|
366
|
+
# using Cinnabar::Command::ArrRefin
|
|
367
|
+
#
|
|
368
|
+
# env_hash = {WW: 2}
|
|
369
|
+
# status =
|
|
370
|
+
# %w[sh -c]
|
|
371
|
+
# .push('printf $WW')
|
|
372
|
+
# .run_cmd(env_hash)
|
|
373
|
+
#
|
|
374
|
+
# status == true
|
|
375
|
+
#
|
|
376
|
+
# @return [Boolean]
|
|
377
|
+
# @see Cinnabar::Command.run_cmd
|
|
378
|
+
def run_cmd(env_hash = nil, opts: {}) # rubocop:disable Style/OptionalBooleanParameter
|
|
379
|
+
Cinnabar::Command.run_cmd(self, env_hash, opts:)
|
|
78
380
|
end
|
|
79
381
|
end
|
|
80
382
|
|
|
81
383
|
# ---------------------
|
|
82
384
|
|
|
83
|
-
#
|
|
385
|
+
# Monkey patching: Array#run, Array#async_run, Array#run_cmd
|
|
386
|
+
#
|
|
387
|
+
# @example run
|
|
388
|
+
#
|
|
389
|
+
# include Cinnabar::Command::ArrMixin
|
|
390
|
+
#
|
|
391
|
+
# stdout = %w[printf World].run
|
|
392
|
+
# stdout == "World" #=> true
|
|
393
|
+
#
|
|
394
|
+
# @example async_run
|
|
395
|
+
#
|
|
396
|
+
# include Cinnabar::Command::ArrMixin
|
|
397
|
+
#
|
|
398
|
+
# fd, waiter = %w[ruby -e].push('sleep 2; puts "OK"').async_run
|
|
399
|
+
#
|
|
400
|
+
# status = waiter.value
|
|
401
|
+
# status.success? #=> true
|
|
402
|
+
#
|
|
403
|
+
# output = fd.read.chomp
|
|
404
|
+
# fd.close
|
|
405
|
+
# output == 'OK' #=> true
|
|
84
406
|
#
|
|
85
|
-
#
|
|
407
|
+
# @example async_run + wait_with_output
|
|
86
408
|
#
|
|
87
409
|
# include Cinnabar::Command::ArrMixin
|
|
410
|
+
# include Cinnabar::Command::TaskArrMixin
|
|
88
411
|
#
|
|
89
|
-
# %w[
|
|
412
|
+
# task = %w[ruby -e].push('sleep 2; puts "OK"').async_run
|
|
90
413
|
#
|
|
91
|
-
#
|
|
414
|
+
# output, status = task.wait_with_output
|
|
415
|
+
#
|
|
416
|
+
# status.success? #=> true
|
|
417
|
+
# output.chomp == 'OK' #=> true
|
|
418
|
+
#
|
|
419
|
+
# @see ArrExt
|
|
92
420
|
module ArrMixin
|
|
93
|
-
def self.included(_host) = ::Array.include
|
|
421
|
+
def self.included(_host) = ::Array.include ArrExt
|
|
94
422
|
end
|
|
95
423
|
|
|
96
|
-
# Refinements: Array#run, Array#
|
|
424
|
+
# Refinements: Array#run, Array#async_run, Array#run_cmd
|
|
97
425
|
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
# == Simple
|
|
426
|
+
# @example run
|
|
101
427
|
#
|
|
102
428
|
# using Cinnabar::Command::ArrRefin
|
|
103
429
|
#
|
|
104
|
-
#
|
|
105
|
-
#
|
|
430
|
+
# stdout =
|
|
431
|
+
# %w[ruby -e]
|
|
432
|
+
# .push('print 2')
|
|
433
|
+
# .run
|
|
434
|
+
#
|
|
435
|
+
# stdout.to_i == 2 #=> true
|
|
106
436
|
#
|
|
107
|
-
#
|
|
437
|
+
# @example run(opts:)
|
|
108
438
|
#
|
|
109
439
|
# using Cinnabar::Command::ArrRefin
|
|
440
|
+
#
|
|
441
|
+
# opts = { allow_failure: true, stdin_data: "Hello" }
|
|
442
|
+
#
|
|
443
|
+
# stdout = %w[wc -m].run(opts:)
|
|
444
|
+
#
|
|
445
|
+
# stdout.to_i == 5 #=> true
|
|
446
|
+
#
|
|
447
|
+
# @example Argvise + run_async
|
|
448
|
+
#
|
|
449
|
+
# require 'argvise'
|
|
450
|
+
# require 'cinnabar'
|
|
451
|
+
#
|
|
110
452
|
# using Argvise::HashRefin
|
|
453
|
+
# using Cinnabar::Command::ArrRefin
|
|
454
|
+
# using Cinnabar::Command::TaskArrRefin
|
|
111
455
|
#
|
|
112
|
-
#
|
|
456
|
+
# task = {
|
|
113
457
|
# cargo: (),
|
|
114
|
-
#
|
|
458
|
+
# b: (),
|
|
459
|
+
# r: true,
|
|
115
460
|
# target: "wasm32-wasip2"
|
|
116
|
-
# }
|
|
117
|
-
#
|
|
461
|
+
# }
|
|
462
|
+
# .to_argv
|
|
463
|
+
# .run_async
|
|
464
|
+
#
|
|
465
|
+
# stdout, status = task.wait_with_output
|
|
466
|
+
# status.success? #=> true
|
|
118
467
|
#
|
|
468
|
+
# @see ArrExt
|
|
119
469
|
module ArrRefin
|
|
120
470
|
refine ::Array do
|
|
121
|
-
import_methods
|
|
471
|
+
import_methods ArrExt
|
|
122
472
|
end
|
|
123
473
|
end
|
|
124
474
|
end
|
|
125
475
|
|
|
126
476
|
module Cinnabar::Command
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
Cinnabar::Command.wait_task.call(self)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
# ---------------------
|
|
134
|
-
|
|
135
|
-
# Monkey Patching: Integer#wait_task
|
|
477
|
+
# The foundation of {TaskArrRefin} and {TaskArrMixin}
|
|
478
|
+
# @see Cinnabar::Command.wait_with_output
|
|
136
479
|
#
|
|
137
|
-
#
|
|
480
|
+
# @example simple
|
|
138
481
|
#
|
|
139
|
-
#
|
|
482
|
+
# using Cinnabar::Command::ArrRefin
|
|
483
|
+
# using Cinnabar::Command::TaskArrRefin
|
|
484
|
+
# # OR: include Cinnabar::Command::TaskArrMixin
|
|
485
|
+
#
|
|
486
|
+
# task = %w[ruby -e]
|
|
487
|
+
# .push('sleep 2; puts "OK"')
|
|
488
|
+
# .async_run
|
|
140
489
|
#
|
|
141
|
-
#
|
|
142
|
-
#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
490
|
+
# stdout, status = task.wait_with_output
|
|
491
|
+
# status.success? #=> true
|
|
492
|
+
module TaskArrExt
|
|
493
|
+
def wait_with_output
|
|
494
|
+
Cinnabar::Command.wait_with_output(*self)
|
|
495
|
+
end
|
|
146
496
|
end
|
|
147
497
|
|
|
148
|
-
# Refinement:
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
#
|
|
154
|
-
# pid = %w[sleep 3].run_in_bg
|
|
155
|
-
# p "wait 3s"
|
|
156
|
-
# pid.wait_task
|
|
157
|
-
module IntRefin
|
|
158
|
-
refine ::Integer do
|
|
159
|
-
import_methods IntegerExt
|
|
498
|
+
# Refinement: Array#wait_with_output
|
|
499
|
+
# @see TaskArrExt
|
|
500
|
+
module TaskArrRefin
|
|
501
|
+
refine ::Array do
|
|
502
|
+
import_methods TaskArrExt
|
|
160
503
|
end
|
|
161
504
|
end
|
|
505
|
+
|
|
506
|
+
# Monkey Patching: Array#wait_with_output
|
|
507
|
+
# @see TaskArrExt
|
|
508
|
+
module TaskArrMixin
|
|
509
|
+
def self.included(_host) = ::Array.include TaskArrExt
|
|
510
|
+
end
|
|
162
511
|
end
|
data/lib/cinnabar/net.rb
CHANGED
|
@@ -5,19 +5,33 @@ module Cinnabar::Downloader
|
|
|
5
5
|
|
|
6
6
|
require 'open-uri'
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
DEFAULT_DL_OPTS = {
|
|
9
|
+
out_dir: 'tmp', file_name: nil,
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
# @example
|
|
9
13
|
#
|
|
10
14
|
# url = 'https://docs.ruby-lang.org/en/3.4/OpenURI.html'
|
|
11
15
|
# opts = { out_dir: "tmp", file_name: "doc.html" }
|
|
12
16
|
# DL = Cinnabar::Downloader
|
|
13
17
|
# DL.download(url, opts)
|
|
14
18
|
#
|
|
15
|
-
#
|
|
19
|
+
# @param url [String] e.g., **https://url.local**
|
|
20
|
+
#
|
|
21
|
+
# @param opts [Hash] Options for customizing the download behavior.
|
|
22
|
+
# e.g., `{out_dir: 'download', file_name: nil, headers: {'User-Agent' => "aria2/1.37.0"}}`
|
|
16
23
|
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
|
|
24
|
+
# @option opts [String] :out_dir
|
|
25
|
+
# Directory where the downloaded file will be saved.
|
|
26
|
+
# @option opts [String, nil] :file_name
|
|
27
|
+
# Name of the output file. If nil, it will be inferred from the URL.
|
|
28
|
+
# @option opts [Hash{String => String}] :headers
|
|
29
|
+
# Optional HTTP headers to include in the request.
|
|
30
|
+
# @return [Integer]
|
|
31
|
+
def download(url, opts = {})
|
|
32
|
+
opts = DEFAULT_DL_OPTS.merge(opts)
|
|
20
33
|
out_dir, file_name, headers = opts.values_at(:out_dir, :file_name, :headers)
|
|
34
|
+
out_dir = '.' if out_dir.nil?
|
|
21
35
|
|
|
22
36
|
headers = build_headers(headers)
|
|
23
37
|
parsed_url = Kernel.URI(url)
|
|
@@ -30,7 +44,7 @@ module Cinnabar::Downloader
|
|
|
30
44
|
.then { IO.copy_stream(_1, file_path.to_s) }
|
|
31
45
|
end
|
|
32
46
|
|
|
33
|
-
#
|
|
47
|
+
# @return [Hash]
|
|
34
48
|
def build_headers(headers)
|
|
35
49
|
base_headers = {
|
|
36
50
|
'User-Agent' => 'Mozilla/5.0 (Linux; aarch64 Wayland; rv:138.0) Gecko/20100101 Firefox/138.0',
|
|
@@ -38,7 +52,7 @@ module Cinnabar::Downloader
|
|
|
38
52
|
base_headers.merge(headers || {}).transform_keys(&:to_s)
|
|
39
53
|
end
|
|
40
54
|
|
|
41
|
-
#
|
|
55
|
+
# @return [String]
|
|
42
56
|
def determine_filename(file_name, parsed_url)
|
|
43
57
|
filename = file_name || File.basename(parsed_url.path || '')
|
|
44
58
|
case filename.strip
|
|
@@ -47,7 +61,7 @@ module Cinnabar::Downloader
|
|
|
47
61
|
end
|
|
48
62
|
end
|
|
49
63
|
|
|
50
|
-
#
|
|
64
|
+
# @return [Pathname]
|
|
51
65
|
def setup_file_path(out_dir, file_name)
|
|
52
66
|
Kernel.Pathname(out_dir)
|
|
53
67
|
.tap(&:mkpath)
|
|
@@ -56,37 +70,53 @@ module Cinnabar::Downloader
|
|
|
56
70
|
end
|
|
57
71
|
|
|
58
72
|
module Cinnabar::Downloader
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
73
|
+
# The foundation of {StrRefin} and {StrMixin}
|
|
74
|
+
module StrExt
|
|
75
|
+
# @see Cinnabar::Downloader.download
|
|
76
|
+
#
|
|
77
|
+
# @param opts [Hash] Options for customizing the download behavior.
|
|
78
|
+
# e.g., `{out_dir: 'download', file_name: nil, headers: {'User-Agent' => "aria2/1.37.0"}}`
|
|
79
|
+
#
|
|
80
|
+
# @option opts [String] :out_dir
|
|
81
|
+
# @option opts [String, nil] :file_name
|
|
82
|
+
# @option opts [Hash{String => String}] :headers
|
|
83
|
+
# @return [Integer]
|
|
84
|
+
def download(opts = {})
|
|
85
|
+
Cinnabar::Downloader.download(self, opts)
|
|
62
86
|
end
|
|
63
87
|
end
|
|
64
88
|
|
|
65
89
|
# -------------
|
|
66
90
|
|
|
67
|
-
#
|
|
91
|
+
# @example
|
|
68
92
|
#
|
|
69
93
|
# include Cinnabar::Downloader::StrMixin
|
|
70
94
|
#
|
|
71
|
-
# url = 'https://docs.ruby-lang.org'
|
|
95
|
+
# url = 'https://docs.ruby-lang.org/en/master'
|
|
72
96
|
#
|
|
73
97
|
# url.download
|
|
74
|
-
# # OR: url.download(out_dir: "tmp", file_name: "custom.html")
|
|
98
|
+
# # OR: url.download({out_dir: "tmp", file_name: "custom.html"})
|
|
99
|
+
#
|
|
100
|
+
# @see Cinnabar::Downloader.download
|
|
101
|
+
# @see StrExt
|
|
75
102
|
module StrMixin
|
|
76
|
-
def self.included(_host) = ::String.include
|
|
103
|
+
def self.included(_host) = ::String.include StrExt
|
|
77
104
|
end
|
|
78
105
|
|
|
79
|
-
#
|
|
106
|
+
# @example
|
|
80
107
|
#
|
|
81
108
|
# using Cinnabar::Downloader::StrRefin
|
|
82
109
|
#
|
|
83
|
-
# url = 'https://docs.ruby-lang.org/en/master
|
|
110
|
+
# url = 'https://docs.ruby-lang.org/en/master'
|
|
84
111
|
#
|
|
85
112
|
# url.download
|
|
86
|
-
# # OR: url.download(out_dir: "/tmp", file_name: "index.html")
|
|
113
|
+
# # OR: url.download({out_dir: "/tmp", file_name: "index.html"})
|
|
114
|
+
#
|
|
115
|
+
# @see Cinnabar::Downloader.download
|
|
116
|
+
# @see StrExt
|
|
87
117
|
module StrRefin
|
|
88
118
|
refine ::String do
|
|
89
|
-
import_methods
|
|
119
|
+
import_methods StrExt
|
|
90
120
|
end
|
|
91
121
|
end
|
|
92
122
|
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# NOTE: This file can be run independently;
|
|
4
|
+
# Although **00_pre.rb** has already imported the relevant libraries, they still need to be imported here.
|
|
5
|
+
require 'pathname'
|
|
6
|
+
|
|
7
|
+
module Cinnabar
|
|
8
|
+
# Build a Proc that converts a directory-like value into a {Kernel.Pathname}.
|
|
9
|
+
#
|
|
10
|
+
# This is handy when you want to pass a converter into higher-order APIs
|
|
11
|
+
# (e.g., map/filter pipelines).
|
|
12
|
+
#
|
|
13
|
+
# @param dir [String, #to_s] a directory path (or any object convertible to String)
|
|
14
|
+
# @return [Proc] a lambda that maps `dir` to `Pathname(dir)`
|
|
15
|
+
#
|
|
16
|
+
# @example Convert a list of directories to Pathname objects
|
|
17
|
+
#
|
|
18
|
+
# conv = Cinnabar.to_path_proc
|
|
19
|
+
# %w[/tmp /var].map(&conv)
|
|
20
|
+
# #=> [#<Pathname:/tmp>, #<Pathname:/var>]
|
|
21
|
+
def self.to_path_proc = ->(dir) { Pathname(dir) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Adds `String#to_path` as a convenience helper to convert strings into {Kernel.Pathname}.
|
|
25
|
+
#
|
|
26
|
+
# This module supports two integration styles:
|
|
27
|
+
#
|
|
28
|
+
# - {Cinnabar::StrToPath::Mixin}: globally extends {String} (monkey patch).
|
|
29
|
+
# - {Cinnabar::StrToPath::Refin}: lexically-scoped extension via refinements.
|
|
30
|
+
#
|
|
31
|
+
# @note This feature relies on Ruby's {Pathname} class.
|
|
32
|
+
# Make sure `require "pathname"` is loaded before calling `to_path`.
|
|
33
|
+
module Cinnabar::StrToPath
|
|
34
|
+
# Implementation of the `#to_path` method intended to be mixed into {String}.
|
|
35
|
+
module Ext
|
|
36
|
+
# Convert the receiver (a String) into a {Pathname}.
|
|
37
|
+
#
|
|
38
|
+
# @return [Pathname] `Pathname(self)`
|
|
39
|
+
#
|
|
40
|
+
# @example
|
|
41
|
+
#
|
|
42
|
+
# "lib".to_path
|
|
43
|
+
# #=> #<Pathname:lib>
|
|
44
|
+
def to_path = ::Kernel.Pathname(self)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# This is a "mixin switch": `include Cinnabar::StrToPath::Mixin` will modify
|
|
48
|
+
# {::String} for the entire process (i.e., a monkey patch).
|
|
49
|
+
#
|
|
50
|
+
# @note Side effect: this will affect *all* strings in the process, including
|
|
51
|
+
# third-party code. If you want scoped behavior, prefer {Refin}.
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
#
|
|
55
|
+
# include Cinnabar::StrToPath::Mixin
|
|
56
|
+
#
|
|
57
|
+
# __dir__.to_path
|
|
58
|
+
# # Same as `Pathname(__dir__)`
|
|
59
|
+
module Mixin
|
|
60
|
+
# Hook invoked when this module is included.
|
|
61
|
+
#
|
|
62
|
+
# @param _host [Module] the including host (unused)
|
|
63
|
+
# @return [void]
|
|
64
|
+
def self.included(_host) = ::String.include Ext
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Adds `String#to_path` via Ruby refinements (lexically scoped).
|
|
68
|
+
#
|
|
69
|
+
# This avoids global monkey patches. The method is only visible within scopes
|
|
70
|
+
# where `using Cinnabar::StrToPath::Refin` is active.
|
|
71
|
+
#
|
|
72
|
+
# @example
|
|
73
|
+
#
|
|
74
|
+
# using Cinnabar::StrToPath::Refin
|
|
75
|
+
#
|
|
76
|
+
# __dir__.to_path
|
|
77
|
+
# # Same as `Pathname(__dir__)`
|
|
78
|
+
module Refin
|
|
79
|
+
# Refinement for {String} to import {Ext#to_path}.
|
|
80
|
+
refine ::String do
|
|
81
|
+
import_methods Ext
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
module Cinnabar::Path
|
|
87
|
+
module_function
|
|
88
|
+
|
|
89
|
+
# Appends a directory to Ruby's load path (`$LOAD_PATH` / `$:`)
|
|
90
|
+
# if it is not already included.
|
|
91
|
+
#
|
|
92
|
+
# @param dir [String] directory path to add into `$LOAD_PATH`
|
|
93
|
+
# @return [void]
|
|
94
|
+
# @example
|
|
95
|
+
#
|
|
96
|
+
# Cinnabar::Path.append_load_path '/opt/ruby/4.0.0/lib/ruby/4.0.0'
|
|
97
|
+
def append_load_path(dir)
|
|
98
|
+
# $: is an alias of $LOAD_PATH
|
|
99
|
+
$: << dir unless $:.include?(dir)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Finds the gem's "lib directory" (via `gem which -V`) and append it to $LOAD_PATH.
|
|
103
|
+
#
|
|
104
|
+
# This method maintains a small cache file to avoid running `gem which` repeatedly.
|
|
105
|
+
# Cache format (per line):
|
|
106
|
+
# <gem_name><two spaces><lib_dir>
|
|
107
|
+
#
|
|
108
|
+
# If the gem cannot be located, it will try to install it and retry up to `max_retries`.
|
|
109
|
+
#
|
|
110
|
+
# @param gem_name [String, Symbol] gem name to locate (default: 'logger')
|
|
111
|
+
# @param cache_file [String] cache file path (default: 'tmp/load_path.txt')
|
|
112
|
+
# @param max_retries [Integer] maximum retries for gem install + re-check (default: 2)
|
|
113
|
+
# @return [void]
|
|
114
|
+
def find_and_append_load_path(gem_name = 'logger', cache_file: 'tmp/load_path.txt', max_retries: 2)
|
|
115
|
+
pkg = gem_name.to_s
|
|
116
|
+
cache_data = decode_cache_file(cache_file)
|
|
117
|
+
|
|
118
|
+
# If cached, append immediately and return.
|
|
119
|
+
case val = cache_data&.[](pkg)
|
|
120
|
+
when nil then ()
|
|
121
|
+
else return append_load_path(val)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Locate the gem's lib directory, installing the gem if necessary.
|
|
125
|
+
lib_dir = gem_dir_with_retry(pkg, max_retries)
|
|
126
|
+
|
|
127
|
+
# Update cache and write it back to disk.
|
|
128
|
+
cache_data[pkg] = lib_dir
|
|
129
|
+
encoded = encode_cache_hash(cache_data)
|
|
130
|
+
|
|
131
|
+
# Ensure parent directory exists, then write the cache file.
|
|
132
|
+
Kernel.Pathname(cache_file)
|
|
133
|
+
.tap { _1.dirname.mkpath }
|
|
134
|
+
.write(encoded)
|
|
135
|
+
|
|
136
|
+
# Finally, append the located directory to $LOAD_PATH.
|
|
137
|
+
append_load_path(lib_dir)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Decodes cache file into a Hash.
|
|
141
|
+
#
|
|
142
|
+
# It ignores:
|
|
143
|
+
#
|
|
144
|
+
# - leading spaces (lstrip)
|
|
145
|
+
# - empty lines
|
|
146
|
+
# - comment lines that start with '#'
|
|
147
|
+
#
|
|
148
|
+
# Each line is split by "two spaces" into:
|
|
149
|
+
# `key value`
|
|
150
|
+
#
|
|
151
|
+
# @param file [String] cache file path
|
|
152
|
+
# @return [Hash{String => String}] mapping from gem name to lib dir
|
|
153
|
+
def decode_cache_file(file)
|
|
154
|
+
# If cache file does not exist, treat as empty cache.
|
|
155
|
+
return {} unless File.exist?(file)
|
|
156
|
+
|
|
157
|
+
File.foreach(file)
|
|
158
|
+
.lazy
|
|
159
|
+
.map(&:lstrip) # allow indentation; normalize leading spaces
|
|
160
|
+
.map(&:chomp) # remove trailing newline
|
|
161
|
+
.reject(&:empty?) # drop blank lines
|
|
162
|
+
.reject { _1.start_with? '#' } # drop comments
|
|
163
|
+
.map { |line| line.split(' ', 2) } # split into [key, value] by two spaces
|
|
164
|
+
.to_h
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Encodes a Hash into the cache file format.
|
|
168
|
+
#
|
|
169
|
+
# @param data [Hash] mapping from gem name to lib dir
|
|
170
|
+
# @return [String] encoded cache content
|
|
171
|
+
# @raise [ArgumentError] if `data` is not a Hash
|
|
172
|
+
#
|
|
173
|
+
# @example
|
|
174
|
+
#
|
|
175
|
+
# CiPath = Cinnabar::Path
|
|
176
|
+
#
|
|
177
|
+
# gem_home = "#{Dir.home}/.local/share/gem"
|
|
178
|
+
# data = {
|
|
179
|
+
# "logger" => "#{gem_home}/gems/logger-1.7.0/lib",
|
|
180
|
+
# "irb" => "#{gem_home}/gems/irb-1.16.0/lib",
|
|
181
|
+
# "reline" => "#{gem_home}/gems/reline-0.6.3/lib",
|
|
182
|
+
# }
|
|
183
|
+
# str = CiPath.encode_cache_hash(data)
|
|
184
|
+
def encode_cache_hash(data)
|
|
185
|
+
Kernel.raise ArgumentError, 'data must be a hash' unless data.is_a? ::Hash
|
|
186
|
+
|
|
187
|
+
# Use "two spaces" as a stable delimiter (same as decode).
|
|
188
|
+
data.map { |k, v| "#{k} #{v}" }.join("\n")
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Resolves gem's lib directory by invoking:
|
|
192
|
+
# `gem which -V <pkg>`
|
|
193
|
+
#
|
|
194
|
+
# `gem which -V` prints the resolved file path; we strip it and take its dirname.
|
|
195
|
+
#
|
|
196
|
+
# @param pkg [String] gem name
|
|
197
|
+
# @return [String] directory containing the resolved file
|
|
198
|
+
# @raise [RuntimeError] if gem which returns empty
|
|
199
|
+
#
|
|
200
|
+
# @note Please do not use `Gem::Specification` in this method,
|
|
201
|
+
# as this function and the script must remain compatible with `--disable=gems`.
|
|
202
|
+
def gem_dir(pkg)
|
|
203
|
+
path = IO.popen(%w[gem which -V] << pkg.to_s, &:read).to_s.strip
|
|
204
|
+
Kernel.raise "gem which returned empty for #{pkg}" if path.empty?
|
|
205
|
+
|
|
206
|
+
File.dirname(path)
|
|
207
|
+
end
|
|
208
|
+
private_class_method :gem_dir
|
|
209
|
+
|
|
210
|
+
# Locates the lib directory for the given gem_name; if it fails, retries by installing the gem.
|
|
211
|
+
#
|
|
212
|
+
# Behavior:
|
|
213
|
+
#
|
|
214
|
+
# - If `gem_dir` raises, print a warning and run `gem install <pkg>`.
|
|
215
|
+
# - Retry up to `max_retries`.
|
|
216
|
+
#
|
|
217
|
+
# @param gem_name [String, Symbol] gem name
|
|
218
|
+
# @param max_retries [Integer] max retry count (default: 2)
|
|
219
|
+
# @return [String] resolved lib directory
|
|
220
|
+
# @raise [RuntimeError] when install fails or retries exceed max
|
|
221
|
+
#
|
|
222
|
+
#
|
|
223
|
+
# @example
|
|
224
|
+
#
|
|
225
|
+
# CiPath = Cinnabar::Path
|
|
226
|
+
#
|
|
227
|
+
# dir_str = CiPath.gem_dir_with_retry("logger")
|
|
228
|
+
def gem_dir_with_retry(gem_name, max_retries = 2)
|
|
229
|
+
pkg = gem_name.to_s
|
|
230
|
+
attempts = 0
|
|
231
|
+
|
|
232
|
+
begin
|
|
233
|
+
logger_dir = gem_dir(pkg)
|
|
234
|
+
rescue StandardError => e
|
|
235
|
+
# Inform user about the failure and the planned automatic install attempt.
|
|
236
|
+
#
|
|
237
|
+
# Do not use `Sinlog.warn` or any "advanced" logger here!
|
|
238
|
+
# As this function is intended for lower-level APIs.
|
|
239
|
+
Kernel.warn "[WARN] #{e}; Try installing #{pkg}"
|
|
240
|
+
|
|
241
|
+
# Attempt to install; raise if installation fails.
|
|
242
|
+
Kernel.system "gem install #{pkg}" or Kernel.raise 'Failed to install'
|
|
243
|
+
|
|
244
|
+
attempts += 1
|
|
245
|
+
Kernel.raise 'Already retried 3 times' if attempts > max_retries
|
|
246
|
+
|
|
247
|
+
retry
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
logger_dir
|
|
251
|
+
end
|
|
252
|
+
# private_class_method :gem_dir_with_retry
|
|
253
|
+
end
|
data/lib/cinnabar/pipe.rb
CHANGED
|
@@ -17,7 +17,7 @@ module Cinnabar::FnPipe
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
module Cinnabar::FnPipe
|
|
20
|
-
module
|
|
20
|
+
module Ext
|
|
21
21
|
def ▷(other) # rubocop:disable Naming/MethodName
|
|
22
22
|
Cinnabar::FnPipe.▷(self, other)
|
|
23
23
|
end
|
|
@@ -29,7 +29,7 @@ module Cinnabar::FnPipe
|
|
|
29
29
|
#
|
|
30
30
|
# Monkey Patching: Object#▷
|
|
31
31
|
#
|
|
32
|
-
#
|
|
32
|
+
# @example
|
|
33
33
|
#
|
|
34
34
|
# include Cinnabar::FnPipe::Mixin
|
|
35
35
|
#
|
|
@@ -40,14 +40,14 @@ module Cinnabar::FnPipe
|
|
|
40
40
|
# 2.▷ :puts
|
|
41
41
|
# #=> 2
|
|
42
42
|
module Mixin
|
|
43
|
-
def self.included(_host) = ::Object.include
|
|
43
|
+
def self.included(_host) = ::Object.include Ext
|
|
44
44
|
end
|
|
45
45
|
|
|
46
46
|
# Function Pipe
|
|
47
47
|
#
|
|
48
48
|
# Refinement: Object#▷
|
|
49
49
|
#
|
|
50
|
-
#
|
|
50
|
+
# @example
|
|
51
51
|
#
|
|
52
52
|
# using Cinnabar::FnPipe::Refin
|
|
53
53
|
#
|
|
@@ -63,7 +63,7 @@ module Cinnabar::FnPipe
|
|
|
63
63
|
#
|
|
64
64
|
module Refin
|
|
65
65
|
refine ::Object do
|
|
66
|
-
import_methods
|
|
66
|
+
import_methods Ext
|
|
67
67
|
end
|
|
68
68
|
end
|
|
69
69
|
end
|
data/lib/cinnabar/version.rb
CHANGED
data/lib/cinnabar.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cinnabar
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- 2moe
|
|
@@ -29,13 +29,16 @@ extensions: []
|
|
|
29
29
|
extra_rdoc_files: []
|
|
30
30
|
files:
|
|
31
31
|
- ".rubocop.yml"
|
|
32
|
+
- ".yardopts"
|
|
32
33
|
- License
|
|
34
|
+
- docs/ChangeLog.md
|
|
33
35
|
- docs/Readme-zh.md
|
|
34
36
|
- docs/Readme.md
|
|
35
37
|
- lib/cinnabar.rb
|
|
36
38
|
- lib/cinnabar/00_pre.rb
|
|
37
39
|
- lib/cinnabar/cmd_runner.rb
|
|
38
40
|
- lib/cinnabar/net.rb
|
|
41
|
+
- lib/cinnabar/path.rb
|
|
39
42
|
- lib/cinnabar/pipe.rb
|
|
40
43
|
- lib/cinnabar/version.rb
|
|
41
44
|
homepage: https://github.com/2moe/cinnabar
|
|
@@ -43,6 +46,7 @@ licenses:
|
|
|
43
46
|
- Apache-2.0
|
|
44
47
|
metadata:
|
|
45
48
|
homepage_uri: https://github.com/2moe/cinnabar
|
|
49
|
+
documentation_uri: https://2moe.github.io/cinnabar
|
|
46
50
|
rdoc_options: []
|
|
47
51
|
require_paths:
|
|
48
52
|
- lib
|
|
@@ -57,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
57
61
|
- !ruby/object:Gem::Version
|
|
58
62
|
version: '0'
|
|
59
63
|
requirements: []
|
|
60
|
-
rubygems_version:
|
|
64
|
+
rubygems_version: 4.0.3
|
|
61
65
|
specification_version: 4
|
|
62
66
|
summary: CI Utils
|
|
63
67
|
test_files: []
|