tetra 0.51.0 → 0.52.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.
- data/README.md +16 -10
- data/SPECIAL_CASES.md +3 -3
- data/lib/tetra.rb +1 -0
- data/lib/tetra/facades/bash.rb +28 -0
- data/lib/tetra/facades/process_runner.rb +8 -1
- data/lib/tetra/packages/package.rb +2 -2
- data/lib/tetra/packages/scriptable.rb +9 -16
- data/lib/tetra/project.rb +14 -3
- data/lib/tetra/ui/dry_run_subcommand.rb +16 -41
- data/lib/tetra/ui/generate_script_subcommand.rb +1 -2
- data/lib/tetra/ui/init_subcommand.rb +1 -1
- data/lib/tetra/ui/subcommand.rb +2 -2
- data/lib/tetra/version.rb +1 -1
- data/spec/lib/package_spec.rb +2 -2
- data/spec/lib/project_spec.rb +6 -6
- data/spec/lib/scriptable_spec.rb +9 -10
- metadata +3 -2
data/README.md
CHANGED
@@ -12,6 +12,7 @@ You need:
|
|
12
12
|
|
13
13
|
* [Ruby 1.9](https://www.ruby-lang.org/en/);
|
14
14
|
* [git](http://git-scm.com/);
|
15
|
+
* bash;
|
15
16
|
* a JDK that can compile whatever software you need to package;
|
16
17
|
|
17
18
|
Install `tetra` via RubyGems:
|
@@ -23,15 +24,20 @@ Install `tetra` via RubyGems:
|
|
23
24
|
Building a package with `tetra` is quite unusual — this is a deliberate choice, so don't worry. Basic steps are:
|
24
25
|
|
25
26
|
* `tetra init` a new project;
|
26
|
-
* add sources to `src/<package name>` and anything else needed for the build in `kit/` in binary form (
|
27
|
-
* execute `tetra dry-run
|
28
|
-
* execute `tetra generate-all`: tetra will
|
27
|
+
* add sources to `src/<package name>` and anything else needed for the build in `kit/` in binary form (Ant and Maven are already included);
|
28
|
+
* execute `tetra dry-run`, which will open a bash subshell. In there, build your project, and when you are done conclude quitting it with `Ctrl+D`;
|
29
|
+
* execute `tetra generate-all`: tetra will scaffold spec files and tarballs.
|
29
30
|
|
30
31
|
Done!
|
31
32
|
|
32
33
|
### How can that possibly work?
|
33
34
|
|
34
|
-
|
35
|
+
During the dry-run `tetra`:
|
36
|
+
- saves your bash history, so that it can use it later to scaffold a build script;
|
37
|
+
- keeps track of changed files, in particular produced jars, which are included in the spec's `%files` section;
|
38
|
+
- saves files downloaded from the Internet (eg. by Maven) and packs them to later allow networkless builds.
|
39
|
+
|
40
|
+
Note that with `tetra` you are not building all dependencies from source - build dependencies are aggregated in a binary-only "blob" package. While this is not ideal it is sufficient to fulfill most open source licenses and to have a repeatable, networkless build, while being a lot easier to automate.
|
35
41
|
|
36
42
|
## A commons-collections walkthrough
|
37
43
|
|
@@ -48,18 +54,18 @@ Second, place source files in the `src/` folder:
|
|
48
54
|
unzip commons-collections-3.2.1-src.zip
|
49
55
|
rm commons-collections-3.2.1-src.zip
|
50
56
|
|
51
|
-
Third, you need to show `tetra` how to build your package
|
57
|
+
Third, you need to show `tetra` how to build your package. Run `tetra dry-run` a new subshell will open, in there do anything you would normally do to build the package:
|
52
58
|
|
53
59
|
cd ../src
|
54
|
-
tetra dry-run
|
60
|
+
tetra dry-run
|
55
61
|
|
56
62
|
cd commons-collections-3.2.1-src/
|
57
63
|
tetra mvn package
|
58
64
|
|
59
|
-
|
65
|
+
^D
|
60
66
|
|
61
67
|
Note that we used `tetra mvn package` instead of `mvn package`: this will use a preloaded Maven bundled in `kit/` by default and the repository in `kit/m2`.
|
62
|
-
Also note that this being a dry-run build, sources will be brought back to their original state after
|
68
|
+
Also note that this being a dry-run build, sources will be brought back to their original state after it finishes to ensure repeatability.
|
63
69
|
|
64
70
|
Finally, generate build scripts, spec files and tarballs in the `packages/` directory:
|
65
71
|
|
@@ -75,9 +81,9 @@ An in-depth discussion of this project's motivation is available in the [MOTIVAT
|
|
75
81
|
|
76
82
|
## Status
|
77
83
|
|
78
|
-
`tetra`
|
84
|
+
`tetra` will soon hit 1.0. If you are a packager you can try to use it, any feedback would be **very** welcome!
|
79
85
|
|
80
|
-
At the moment `tetra` is tested on openSUSE.
|
86
|
+
At the moment `tetra` is tested on openSUSE and Ubuntu.
|
81
87
|
|
82
88
|
## Sources
|
83
89
|
|
data/SPECIAL_CASES.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Special cases
|
2
2
|
|
3
|
-
## Failing
|
3
|
+
## Failing dry-runs
|
4
4
|
|
5
|
-
If your build fails for whatever reason, abort it with `
|
5
|
+
If your build fails for whatever reason during a dry run, abort it with `^C^D` (`Ctrl+C` followed by `Ctrl+D`). `tetra` will restore all project files as they were before the build.
|
6
6
|
|
7
7
|
## Generating files multiple times
|
8
8
|
|
@@ -12,7 +12,7 @@ You can edit any file generated by tetra - regenerating it later will not overwr
|
|
12
12
|
|
13
13
|
You can generate single files with the following commands:
|
14
14
|
|
15
|
-
* `tetra generate-script`: (re)generates the `build.sh` file from
|
15
|
+
* `tetra generate-script`: (re)generates the `build.sh` file from commands used in the latest successful dry-run;
|
16
16
|
* `tetra generate-archive`: (re)generates the package tarball;
|
17
17
|
* `tetra generate-spec`: (re)generates the package spec;
|
18
18
|
* `tetra generate-kit`: (re)generates the kit tarball and spec;
|
data/lib/tetra.rb
CHANGED
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
module Tetra
|
4
|
+
# runs Bash with tetra-specific options
|
5
|
+
class Bash
|
6
|
+
include ProcessRunner
|
7
|
+
|
8
|
+
# runs bash in a subshell, returns list of
|
9
|
+
# commands that were run in the session
|
10
|
+
def bash
|
11
|
+
Tempfile.open("tetra-history") do |temp_file|
|
12
|
+
temp_path = temp_file.path
|
13
|
+
|
14
|
+
env = {
|
15
|
+
"HISTFILE" => temp_path, # use temporary file for history
|
16
|
+
"HISTFILESIZE" => "-1", # don't limit file size
|
17
|
+
"HISTSIZE" => "-1", # don't limit history size
|
18
|
+
"HISTTIMEFORMAT" => nil, # don't keep timestamps
|
19
|
+
"HISTCONTROL" => "", # don't skip any command
|
20
|
+
"PS1" => "\e[1;33mdry-running\e[m:\\\w\$ " # change prompt
|
21
|
+
}
|
22
|
+
|
23
|
+
run_interactive("bash --norc", env)
|
24
|
+
File.read(temp_path).split("\n").map(&:strip)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -5,7 +5,7 @@ module Tetra
|
|
5
5
|
module ProcessRunner
|
6
6
|
include Logging
|
7
7
|
|
8
|
-
# runs
|
8
|
+
# runs a noninteractive executable and returns its output as a string
|
9
9
|
# raises ExecutionFailed if the exit status is not 0
|
10
10
|
# optionally echoes the executable's output/error to standard output/error
|
11
11
|
def run(commandline, echo = false, stdin = nil)
|
@@ -34,6 +34,13 @@ module Tetra
|
|
34
34
|
out_recorder.record
|
35
35
|
end
|
36
36
|
|
37
|
+
# runs an interactive executable in a subshell
|
38
|
+
# changing environment variables
|
39
|
+
def run_interactive(command, env)
|
40
|
+
success = system(env, command)
|
41
|
+
fail ExecutionFailed.new(command, $CHILD_STATUS, nil, nil) unless success
|
42
|
+
end
|
43
|
+
|
37
44
|
# records bytes sent via "<<" for later use
|
38
45
|
# optionally echoes to another IO object
|
39
46
|
class RecordingIO
|
@@ -4,23 +4,14 @@ module Tetra
|
|
4
4
|
# generates a package build script from bash_history
|
5
5
|
module Scriptable
|
6
6
|
# returns a build script for this package
|
7
|
-
def _to_script(project
|
7
|
+
def _to_script(project)
|
8
8
|
project.from_directory do
|
9
|
-
history_lines = File.readlines(history_path).map(&:strip)
|
10
|
-
relevant_lines =
|
11
|
-
history_lines
|
12
|
-
.reverse
|
13
|
-
.take_while { |e| e.match(/tetra +dry-run +start/).nil? }
|
14
|
-
.reverse
|
15
|
-
.take_while { |e| e.match(/tetra +dry-run +finish/).nil? }
|
16
|
-
.select { |e| e.match(/^#/).nil? }
|
17
|
-
|
18
9
|
script_lines = [
|
19
10
|
"#!/bin/bash",
|
20
11
|
"set -xe",
|
21
12
|
"PROJECT_PREFIX=`readlink -e .`",
|
22
13
|
"cd #{project.latest_dry_run_directory}"
|
23
|
-
] + script_body(project
|
14
|
+
] + script_body(project)
|
24
15
|
|
25
16
|
new_content = script_lines.join("\n") + "\n"
|
26
17
|
|
@@ -34,19 +25,21 @@ module Tetra
|
|
34
25
|
end
|
35
26
|
end
|
36
27
|
|
37
|
-
# returns the script body
|
38
|
-
|
39
|
-
|
28
|
+
# returns the script body by taking the last dry-run's
|
29
|
+
# build script lines and adjusting mvn and ant's paths
|
30
|
+
def script_body(project)
|
31
|
+
lines = project.build_script_lines
|
32
|
+
ant = if lines.any? { |e| e.match(/tetra +ant/) }
|
40
33
|
path = Tetra::Kit.new(project).find_executable("ant")
|
41
34
|
Tetra::Ant.new(project.full_path, path).ant(@options)
|
42
35
|
end
|
43
36
|
|
44
|
-
mvn = if
|
37
|
+
mvn = if lines.any? { |e| e.match(/tetra +mvn/) }
|
45
38
|
mvn_path = Tetra::Kit.new(project).find_executable("mvn")
|
46
39
|
mvn = Tetra::Mvn.new("$PROJECT_PREFIX", mvn_path)
|
47
40
|
end
|
48
41
|
|
49
|
-
|
42
|
+
lines.map do |line|
|
50
43
|
if line =~ /tetra +mvn/
|
51
44
|
line.gsub(/tetra +mvn/, "#{mvn.get_mvn_commandline(['-o'])}")
|
52
45
|
elsif line =~ /tetra +ant/
|
data/lib/tetra/project.rb
CHANGED
@@ -114,9 +114,11 @@ module Tetra
|
|
114
114
|
!latest_comment.nil? && !(latest_comment =~ /tetra: dry-run-finished/)
|
115
115
|
end
|
116
116
|
|
117
|
-
# ends a dry-run assuming a successful build
|
118
|
-
# reverts sources
|
119
|
-
|
117
|
+
# ends a dry-run assuming a successful build:
|
118
|
+
# - reverts sources as before dry-run
|
119
|
+
# - saves the list of generated files in git comments
|
120
|
+
# - saves the build script lines in git comments
|
121
|
+
def finish(build_script_lines)
|
120
122
|
# keep track of changed files
|
121
123
|
start_id = @git.latest_id("tetra: dry-run-started")
|
122
124
|
changed_files = @git.changed_files("src", start_id)
|
@@ -127,6 +129,7 @@ module Tetra
|
|
127
129
|
# prepare commit comments
|
128
130
|
comments = ["Dry run finished\n", "tetra: dry-run-finished"]
|
129
131
|
comments += changed_files.map { |f| "tetra: file-changed: #{f}" }
|
132
|
+
comments += build_script_lines.map { |l| "tetra: build-script-line: #{l}" }
|
130
133
|
|
131
134
|
# if this is the first dry-run, mark sources as tarball
|
132
135
|
if @git.latest_id("tetra: dry-run-finished").nil?
|
@@ -228,6 +231,14 @@ module Tetra
|
|
228
231
|
.sort
|
229
232
|
end
|
230
233
|
|
234
|
+
def build_script_lines
|
235
|
+
@git.latest_comment("tetra: dry-run-finished")
|
236
|
+
.split("\n")
|
237
|
+
.map { |line| line[/^tetra: build-script-line: (.+)$/, 1] }
|
238
|
+
.compact
|
239
|
+
.sort
|
240
|
+
end
|
241
|
+
|
231
242
|
# archives a tarball of src/ in packages/
|
232
243
|
# the latest commit marked as tarball is taken as the version
|
233
244
|
def archive_sources
|
@@ -3,55 +3,30 @@
|
|
3
3
|
module Tetra
|
4
4
|
# tetra dry-run
|
5
5
|
class DryRunSubcommand < Tetra::Subcommand
|
6
|
-
parameter "COMMAND", "\"start\" to begin, \"finish\" to end or \"abort\" to undo changes" do |command|
|
7
|
-
if %w(start finish abort).include?(command)
|
8
|
-
command
|
9
|
-
else
|
10
|
-
fail ArgumentError, "\"#{command}\" is not valid, must be one of \"start\", \"finish\" or \"abort\""
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
6
|
def execute
|
15
7
|
checking_exceptions do
|
16
8
|
project = Tetra::Project.new(".")
|
17
|
-
send(command, project)
|
18
|
-
end
|
19
|
-
end
|
20
9
|
|
21
|
-
def start(project)
|
22
|
-
if !project.dry_running?
|
23
10
|
if project.src_patched?
|
24
|
-
puts "Some files in src/ were changed since last dry-run
|
25
|
-
puts "
|
26
|
-
puts "Dry run not started
|
11
|
+
puts "Some files in src/ were changed since last dry-run,"
|
12
|
+
puts "use \"tetra patch message\" to include changes in a patch before dry-running."
|
13
|
+
puts "Dry run not started"
|
27
14
|
else
|
28
15
|
project.dry_run
|
29
|
-
puts "
|
30
|
-
puts "
|
31
|
-
puts "If the build succeedes end this dry run with
|
32
|
-
puts "
|
33
|
-
end
|
34
|
-
else
|
35
|
-
puts "Dry-run already in progress."
|
36
|
-
puts "Use \"tetra dry-run finish\" to end it or \"tetra dry-run abort\" to undo changes."
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def finish(project)
|
41
|
-
if project.dry_running?
|
42
|
-
project.finish
|
43
|
-
puts "Dry-run finished."
|
44
|
-
else
|
45
|
-
puts "No dry-run is in progress."
|
46
|
-
end
|
47
|
-
end
|
16
|
+
puts "Dry-run started in a new bash shell."
|
17
|
+
puts "Build your project now, you can use \"tetra mvn\" and \"tetra ant\"."
|
18
|
+
puts "If the build succeedes end this dry run with ^D (Ctrl+D),"
|
19
|
+
puts "if the build does not succeed use ^C^D to abort and undo any change"
|
48
20
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
21
|
+
begin
|
22
|
+
history = Bash.new.bash
|
23
|
+
project.finish(history)
|
24
|
+
puts "Dry-run finished"
|
25
|
+
rescue ExecutionFailed
|
26
|
+
project.abort
|
27
|
+
puts "Project reverted as before dry-run"
|
28
|
+
end
|
29
|
+
end
|
55
30
|
end
|
56
31
|
end
|
57
32
|
end
|
@@ -7,9 +7,8 @@ module Tetra
|
|
7
7
|
checking_exceptions do
|
8
8
|
project = Tetra::Project.new(".")
|
9
9
|
ensure_dry_running(:has_finished, project) do
|
10
|
-
history = File.join(Dir.home, ".bash_history")
|
11
10
|
result_path, conflict_count =
|
12
|
-
Tetra::Package.new(project).to_script
|
11
|
+
Tetra::Package.new(project).to_script
|
13
12
|
print_generation_result(project, result_path, conflict_count)
|
14
13
|
end
|
15
14
|
end
|
@@ -8,7 +8,7 @@ module Tetra
|
|
8
8
|
Tetra::Project.init(".")
|
9
9
|
puts "Project inited."
|
10
10
|
puts "Add sources to src/, binary dependencies to kit/."
|
11
|
-
puts "When you are ready to test a build, use \"tetra dry-run\"
|
11
|
+
puts "When you are ready to test a build, use \"tetra dry-run\""
|
12
12
|
end
|
13
13
|
end
|
14
14
|
end
|
data/lib/tetra/ui/subcommand.rb
CHANGED
@@ -59,10 +59,10 @@ module Tetra
|
|
59
59
|
else
|
60
60
|
if (state == :is_in_progress) ||
|
61
61
|
(state == :has_finished && !dry_running && !has_finished)
|
62
|
-
puts "Please start a dry-run first, use \"tetra dry-run
|
62
|
+
puts "Please start a dry-run first, use \"tetra dry-run\""
|
63
63
|
elsif (state == :is_not_in_progress) ||
|
64
64
|
(state == :has_finished && dry_running)
|
65
|
-
puts "
|
65
|
+
puts "There is a dry-run in progress, please finish it (^D) or abort it (^C^D)"
|
66
66
|
end
|
67
67
|
end
|
68
68
|
end
|
data/lib/tetra/version.rb
CHANGED
data/spec/lib/package_spec.rb
CHANGED
@@ -11,7 +11,7 @@ describe Tetra::Package do
|
|
11
11
|
Dir.chdir(@project_path) do
|
12
12
|
FileUtils.touch(File.join("kit", "jars", "test.jar"))
|
13
13
|
end
|
14
|
-
@project.finish
|
14
|
+
@project.finish([])
|
15
15
|
|
16
16
|
@project.from_directory do
|
17
17
|
FileUtils.mkdir_p(File.join("src", "out"))
|
@@ -28,7 +28,7 @@ describe Tetra::Package do
|
|
28
28
|
FileUtils.touch(File.join("src", "out", "test#{i}.jar"))
|
29
29
|
end
|
30
30
|
|
31
|
-
@project.finish
|
31
|
+
@project.finish([])
|
32
32
|
end
|
33
33
|
|
34
34
|
FileUtils.copy(File.join("spec", "data", "nailgun", "pom.xml"), @project_path)
|
data/spec/lib/project_spec.rb
CHANGED
@@ -20,7 +20,7 @@ describe Tetra::Project do
|
|
20
20
|
|
21
21
|
it "returns a project version after dry-run" do
|
22
22
|
@project.dry_run
|
23
|
-
@project.finish
|
23
|
+
@project.finish([])
|
24
24
|
expect(@project.version).to be
|
25
25
|
end
|
26
26
|
end
|
@@ -77,7 +77,7 @@ describe Tetra::Project do
|
|
77
77
|
expect(@project.dry_running?).to be_falsey
|
78
78
|
@project.dry_run
|
79
79
|
expect(@project.dry_running?).to be_truthy
|
80
|
-
@project.finish
|
80
|
+
@project.finish([])
|
81
81
|
expect(@project.dry_running?).to be_falsey
|
82
82
|
end
|
83
83
|
end
|
@@ -87,7 +87,7 @@ describe Tetra::Project do
|
|
87
87
|
it "checks whether src is dirty" do
|
88
88
|
@project.from_directory do
|
89
89
|
@project.dry_run
|
90
|
-
@project.finish
|
90
|
+
@project.finish([])
|
91
91
|
expect(@project.src_patched?).to be_falsey
|
92
92
|
|
93
93
|
FileUtils.touch(File.join("src", "test"))
|
@@ -129,7 +129,7 @@ describe Tetra::Project do
|
|
129
129
|
FileUtils.touch(File.join("src", "test2"))
|
130
130
|
end
|
131
131
|
|
132
|
-
expect(@project.finish).to be_truthy
|
132
|
+
expect(@project.finish([])).to be_truthy
|
133
133
|
expect(@project.dry_running?).to be_falsey
|
134
134
|
|
135
135
|
@project.from_directory do
|
@@ -200,13 +200,13 @@ describe Tetra::Project do
|
|
200
200
|
File.open(File.join("src", "added_in_first_dry_run"), "w") { |f| f.write("A") }
|
201
201
|
File.open("added_outside_directory", "w") { |f| f.write("A") }
|
202
202
|
end
|
203
|
-
expect(@project.finish).to be_truthy
|
203
|
+
expect(@project.finish([])).to be_truthy
|
204
204
|
|
205
205
|
expect(@project.dry_run).to be_truthy
|
206
206
|
@project.from_directory do
|
207
207
|
File.open(File.join("src", "added_in_second_dry_run"), "w") { |f| f.write("A") }
|
208
208
|
end
|
209
|
-
expect(@project.finish).to be_truthy
|
209
|
+
expect(@project.finish([])).to be_truthy
|
210
210
|
|
211
211
|
list = @project.produced_files
|
212
212
|
expect(list).to include("added_in_second_dry_run")
|
data/spec/lib/scriptable_spec.rb
CHANGED
@@ -9,17 +9,16 @@ describe Tetra::Scriptable do
|
|
9
9
|
create_mock_project
|
10
10
|
|
11
11
|
@project.from_directory do
|
12
|
-
File.open("history", "w") do |io|
|
13
|
-
io.puts "some earlier command"
|
14
|
-
io.puts "tetra dry-run start --unwanted-options"
|
15
|
-
io.puts "cd somewhere significant"
|
16
|
-
io.puts "tetra mvn --options"
|
17
|
-
io.puts "tetra dry-run finish -a"
|
18
|
-
io.puts "some later command"
|
19
|
-
end
|
20
|
-
|
21
12
|
FileUtils.mkdir_p(File.join("src", "test-package"))
|
22
13
|
@project.dry_run
|
14
|
+
|
15
|
+
history = ["tetra dry-run start --unwanted-options",
|
16
|
+
"cd somewhere significant",
|
17
|
+
"tetra mvn --options",
|
18
|
+
"tetra dry-run finish -a"
|
19
|
+
]
|
20
|
+
|
21
|
+
@project.finish(history)
|
23
22
|
end
|
24
23
|
|
25
24
|
create_mock_executable("ant")
|
@@ -34,7 +33,7 @@ describe Tetra::Scriptable do
|
|
34
33
|
it "generates a build script from the history" do
|
35
34
|
@project.from_directory do
|
36
35
|
@package = Tetra::Package.new(@project)
|
37
|
-
@package.to_script
|
36
|
+
@package.to_script
|
38
37
|
|
39
38
|
lines = File.readlines(File.join("packages", "test-project", "build.sh"))
|
40
39
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tetra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.52.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-03-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rake
|
@@ -352,6 +352,7 @@ files:
|
|
352
352
|
- lib/template/src/CONTENTS
|
353
353
|
- lib/tetra.rb
|
354
354
|
- lib/tetra/facades/ant.rb
|
355
|
+
- lib/tetra/facades/bash.rb
|
355
356
|
- lib/tetra/facades/git.rb
|
356
357
|
- lib/tetra/facades/mvn.rb
|
357
358
|
- lib/tetra/facades/process_runner.rb
|