taski 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/.standard.yml +9 -0
- data/README.md +189 -32
- data/Rakefile +7 -1
- data/examples/complex_example.rb +107 -0
- data/examples/readme_example.rb +30 -0
- data/lib/taski/dependency_analyzer.rb +172 -0
- data/lib/taski/exceptions.rb +14 -0
- data/lib/taski/logger.rb +202 -0
- data/lib/taski/reference.rb +40 -0
- data/lib/taski/task/base.rb +60 -0
- data/lib/taski/task/define_api.rb +138 -0
- data/lib/taski/task/dependency_resolver.rb +138 -0
- data/lib/taski/task/exports_api.rb +31 -0
- data/lib/taski/task/instance_management.rb +176 -0
- data/lib/taski/version.rb +1 -1
- data/lib/taski.rb +34 -137
- data/sig/taski.rbs +97 -2
- metadata +30 -5
- data/lib/taski/utils.rb +0 -53
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "monitor"
|
4
|
+
|
5
|
+
module Taski
|
6
|
+
class Task
|
7
|
+
class << self
|
8
|
+
# === Lifecycle Management ===
|
9
|
+
|
10
|
+
# Build this task and all its dependencies
|
11
|
+
def build
|
12
|
+
resolve_dependencies.reverse_each do |task_class|
|
13
|
+
task_class.ensure_instance_built
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Clean this task and all its dependencies in reverse order
|
18
|
+
def clean
|
19
|
+
resolve_dependencies.each do |task_class|
|
20
|
+
# Get existing instance or create new one for cleaning
|
21
|
+
instance = task_class.instance_variable_get(:@__task_instance) || task_class.new
|
22
|
+
instance.clean
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Reset task instance and cached data to prevent memory leaks
|
27
|
+
# @return [self] Returns self for method chaining
|
28
|
+
def reset!
|
29
|
+
build_monitor.synchronize do
|
30
|
+
@__task_instance = nil
|
31
|
+
@__defined_values = nil
|
32
|
+
@__defined_for_resolve = nil
|
33
|
+
clear_thread_local_state
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
# Refresh task state (currently just resets)
|
39
|
+
# @return [self] Returns self for method chaining
|
40
|
+
def refresh
|
41
|
+
reset!
|
42
|
+
end
|
43
|
+
|
44
|
+
# === Instance Management ===
|
45
|
+
|
46
|
+
# Ensure task instance is built (public because called from build)
|
47
|
+
# @return [Task] The built task instance
|
48
|
+
def ensure_instance_built
|
49
|
+
# Use double-checked locking pattern for thread safety
|
50
|
+
return @__task_instance if @__task_instance
|
51
|
+
|
52
|
+
build_monitor.synchronize do
|
53
|
+
# Check again after acquiring lock
|
54
|
+
return @__task_instance if @__task_instance
|
55
|
+
|
56
|
+
# Prevent infinite recursion using thread-local storage
|
57
|
+
thread_key = build_thread_key
|
58
|
+
if Thread.current[thread_key]
|
59
|
+
# Build dependency path for better error message
|
60
|
+
cycle_path = build_current_dependency_path
|
61
|
+
raise CircularDependencyError, build_runtime_circular_dependency_message(cycle_path)
|
62
|
+
end
|
63
|
+
|
64
|
+
Thread.current[thread_key] = true
|
65
|
+
begin
|
66
|
+
build_dependencies
|
67
|
+
@__task_instance = build_instance
|
68
|
+
ensure
|
69
|
+
Thread.current[thread_key] = false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
@__task_instance
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
# === Core Helper Methods ===
|
79
|
+
|
80
|
+
# Get or create build monitor for thread safety
|
81
|
+
# @return [Monitor] Thread-safe monitor object
|
82
|
+
def build_monitor
|
83
|
+
@__build_monitor ||= Monitor.new
|
84
|
+
end
|
85
|
+
|
86
|
+
# Generate thread key for recursion detection
|
87
|
+
# @return [String] Thread key for this task
|
88
|
+
def build_thread_key
|
89
|
+
"#{name}#{THREAD_KEY_SUFFIX}"
|
90
|
+
end
|
91
|
+
|
92
|
+
# Build and configure task instance
|
93
|
+
# @return [Task] Built task instance
|
94
|
+
def build_instance
|
95
|
+
instance = new
|
96
|
+
build_start_time = Time.now
|
97
|
+
begin
|
98
|
+
Taski.logger.task_build_start(name.to_s, dependencies: @dependencies || [])
|
99
|
+
instance.build
|
100
|
+
duration = Time.now - build_start_time
|
101
|
+
Taski.logger.task_build_complete(name.to_s, duration: duration)
|
102
|
+
instance
|
103
|
+
rescue => e
|
104
|
+
duration = Time.now - build_start_time
|
105
|
+
# Log the error with full context
|
106
|
+
Taski.logger.task_build_failed(name.to_s, error: e, duration: duration)
|
107
|
+
raise TaskBuildError, "Failed to build task #{name}: #{e.message}"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Clear thread-local state for this task
|
112
|
+
def clear_thread_local_state
|
113
|
+
Thread.current.keys.each do |key|
|
114
|
+
Thread.current[key] = nil if key.to_s.include?(build_thread_key)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# === Dependency Management ===
|
119
|
+
|
120
|
+
# Build all dependencies of this task
|
121
|
+
def build_dependencies
|
122
|
+
resolve_dependencies
|
123
|
+
|
124
|
+
(@dependencies || []).each do |dep|
|
125
|
+
dep_class = extract_class(dep)
|
126
|
+
next if dep_class == self
|
127
|
+
|
128
|
+
dep_class.ensure_instance_built if dep_class.respond_to?(:ensure_instance_built)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Build current dependency path from thread-local storage
|
133
|
+
# @return [Array<Class>] Array of classes in the current build path
|
134
|
+
def build_current_dependency_path
|
135
|
+
path = []
|
136
|
+
Thread.current.keys.each do |key|
|
137
|
+
if key.to_s.end_with?(THREAD_KEY_SUFFIX) && Thread.current[key]
|
138
|
+
class_name = key.to_s.sub(THREAD_KEY_SUFFIX, "")
|
139
|
+
begin
|
140
|
+
path << Object.const_get(class_name)
|
141
|
+
rescue NameError
|
142
|
+
# Skip if class not found
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
path << self
|
147
|
+
end
|
148
|
+
|
149
|
+
# Build runtime circular dependency error message
|
150
|
+
# @param cycle_path [Array<Class>] The circular dependency path
|
151
|
+
# @return [String] Formatted error message
|
152
|
+
def build_runtime_circular_dependency_message(cycle_path)
|
153
|
+
path_names = cycle_path.map { |klass| klass.name || klass.to_s }
|
154
|
+
|
155
|
+
message = "Circular dependency detected!\n"
|
156
|
+
message += "Cycle: #{path_names.join(" → ")}\n\n"
|
157
|
+
message += "The dependency chain is:\n"
|
158
|
+
|
159
|
+
cycle_path.each_cons(2).with_index do |(from, to), index|
|
160
|
+
message += " #{index + 1}. #{from.name} is trying to build → #{to.name}\n"
|
161
|
+
end
|
162
|
+
|
163
|
+
message += "\nThis creates an infinite loop that cannot be resolved."
|
164
|
+
message
|
165
|
+
end
|
166
|
+
|
167
|
+
# Extract class from dependency hash
|
168
|
+
# @param dep [Hash] Dependency information
|
169
|
+
# @return [Class] The dependency class
|
170
|
+
def extract_class(dep)
|
171
|
+
klass = dep[:klass]
|
172
|
+
klass.is_a?(Reference) ? klass.deref : klass
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
data/lib/taski/version.rb
CHANGED
data/lib/taski.rb
CHANGED
@@ -1,143 +1,40 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "monitor"
|
4
|
+
|
5
|
+
# Load core components
|
3
6
|
require_relative "taski/version"
|
7
|
+
require_relative "taski/exceptions"
|
8
|
+
require_relative "taski/logger"
|
9
|
+
require_relative "taski/reference"
|
10
|
+
require_relative "taski/dependency_analyzer"
|
11
|
+
|
12
|
+
# Load Task class components
|
13
|
+
require_relative "taski/task/base"
|
14
|
+
require_relative "taski/task/exports_api"
|
15
|
+
require_relative "taski/task/define_api"
|
16
|
+
require_relative "taski/task/instance_management"
|
17
|
+
require_relative "taski/task/dependency_resolver"
|
4
18
|
|
5
19
|
module Taski
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
def define(name, block, **options)
|
28
|
-
@dependencies ||= []
|
29
|
-
@definitions ||= {}
|
30
|
-
|
31
|
-
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
32
|
-
def self.#{name}
|
33
|
-
__resolve__[__callee__] ||= false
|
34
|
-
if __resolve__[__callee__]
|
35
|
-
# already resolved
|
36
|
-
else
|
37
|
-
__resolve__[__callee__] = true
|
38
|
-
throw :unresolved, [self, __callee__]
|
39
|
-
end
|
40
|
-
end
|
41
|
-
RUBY
|
42
|
-
|
43
|
-
classes = []
|
44
|
-
loop do
|
45
|
-
klass, task = catch(:unresolved) do
|
46
|
-
block.call
|
47
|
-
nil
|
48
|
-
end
|
49
|
-
|
50
|
-
if klass.nil?
|
51
|
-
classes.each do |task_class|
|
52
|
-
task_class[:klass].class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
53
|
-
__resolve__ = {}
|
54
|
-
RUBY
|
55
|
-
end
|
56
|
-
|
57
|
-
break
|
58
|
-
else
|
59
|
-
classes << { klass:, task: }
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
@dependencies += classes
|
64
|
-
@definitions[name] = { block:, options:, classes: }
|
65
|
-
end
|
66
|
-
|
67
|
-
def build
|
68
|
-
resolve_dependencies.reverse.each do |task_class|
|
69
|
-
task_class.new.build
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
def clean
|
74
|
-
resolve_dependencies.each do |task_class|
|
75
|
-
task_class.new.clean
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def refresh
|
80
|
-
# TODO
|
81
|
-
end
|
82
|
-
|
83
|
-
def resolve(queue, resolved)
|
84
|
-
@dependencies.each do |task|
|
85
|
-
if task[:klass].is_a?(Reference)
|
86
|
-
task[:klass].deref
|
87
|
-
else
|
88
|
-
task[:klass]
|
89
|
-
end => task_class
|
90
|
-
|
91
|
-
# increase priority
|
92
|
-
if resolved.include?(task_class)
|
93
|
-
resolved.delete(task_class)
|
94
|
-
end
|
95
|
-
queue << task_class
|
96
|
-
end
|
97
|
-
|
98
|
-
# override
|
99
|
-
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
100
|
-
def self.ref(klass)
|
101
|
-
Object.const_get(klass)
|
102
|
-
end
|
103
|
-
RUBY
|
104
|
-
|
105
|
-
@definitions.each do |name, (_block, _options)|
|
106
|
-
# override
|
107
|
-
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
108
|
-
def self.#{name}
|
109
|
-
@__#{name} ||= @definitions[:#{name}][:block].call
|
110
|
-
end
|
111
|
-
RUBY
|
112
|
-
|
113
|
-
define_method(name) do
|
114
|
-
unless instance_variable_defined?("@__#{name}")
|
115
|
-
instance_variable_set("@__#{name}", self.class.send(name))
|
116
|
-
end
|
117
|
-
instance_variable_get("@__#{name}")
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
self
|
122
|
-
end
|
123
|
-
|
124
|
-
private
|
125
|
-
|
126
|
-
def __resolve__
|
127
|
-
@__resolve__ ||= {}
|
128
|
-
end
|
129
|
-
|
130
|
-
def resolve_dependencies
|
131
|
-
queue = [self]
|
132
|
-
resolved = []
|
133
|
-
|
134
|
-
while queue.any?
|
135
|
-
resolved << task_class = queue.shift
|
136
|
-
task_class.resolve(queue, resolved)
|
137
|
-
end
|
138
|
-
|
139
|
-
resolved
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
20
|
+
# Main module for the Taski task framework
|
21
|
+
#
|
22
|
+
# Taski provides a framework for defining and managing task dependencies
|
23
|
+
# with two complementary APIs:
|
24
|
+
# 1. Exports API - Export instance variables as class methods (static dependencies)
|
25
|
+
# 2. Define API - Define lazy-evaluated values with dynamic dependency resolution
|
26
|
+
#
|
27
|
+
# Use Define API when:
|
28
|
+
# - Dependencies change based on runtime conditions
|
29
|
+
# - Environment-specific configurations
|
30
|
+
# - Feature flags determine which classes to use
|
31
|
+
# - Complex conditional logic determines dependencies
|
32
|
+
#
|
33
|
+
# Features:
|
34
|
+
# - Automatic dependency resolution (static and dynamic)
|
35
|
+
# - Static analysis of method dependencies
|
36
|
+
# - Runtime dependency resolution for conditional logic
|
37
|
+
# - Thread-safe task building
|
38
|
+
# - Circular dependency detection
|
39
|
+
# - Memory leak prevention
|
143
40
|
end
|
data/sig/taski.rbs
CHANGED
@@ -1,4 +1,99 @@
|
|
1
1
|
module Taski
|
2
2
|
VERSION: String
|
3
|
-
|
4
|
-
|
3
|
+
|
4
|
+
# Custom exceptions
|
5
|
+
class CircularDependencyError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
class TaskAnalysisError < StandardError
|
9
|
+
end
|
10
|
+
|
11
|
+
class TaskBuildError < StandardError
|
12
|
+
end
|
13
|
+
|
14
|
+
# Reference class for task references
|
15
|
+
class Reference
|
16
|
+
@klass: String
|
17
|
+
|
18
|
+
def initialize: (String klass) -> void
|
19
|
+
def deref: () -> Class
|
20
|
+
def ==: (untyped other) -> bool
|
21
|
+
def inspect: () -> String
|
22
|
+
end
|
23
|
+
|
24
|
+
# Main Task class
|
25
|
+
class Task
|
26
|
+
# Constants
|
27
|
+
THREAD_KEY_SUFFIX: String
|
28
|
+
TASKI_ANALYZING_DEFINE_KEY: Symbol
|
29
|
+
ANALYZED_METHODS: Array[Symbol]
|
30
|
+
|
31
|
+
# Hook methods
|
32
|
+
def self.method_added: (Symbol method_name) -> void
|
33
|
+
|
34
|
+
# Public API methods
|
35
|
+
def self.exports: (*Symbol names) -> void
|
36
|
+
def self.define: (Symbol name) { () -> untyped } -> void
|
37
|
+
def self.build: () -> void
|
38
|
+
def self.clean: () -> void
|
39
|
+
def self.reset!: () -> self
|
40
|
+
def self.refresh: () -> self
|
41
|
+
def self.resolve: (Array[untyped] queue, Array[untyped] resolved) -> self
|
42
|
+
def self.ref: (String | Class klass) -> (Reference | Class)
|
43
|
+
def self.ensure_instance_built: () -> Task
|
44
|
+
|
45
|
+
# Define API methods
|
46
|
+
def self.__resolve__: () -> Hash[Symbol, untyped]
|
47
|
+
def self.resolve_dependencies: () -> void
|
48
|
+
|
49
|
+
# Instance methods
|
50
|
+
def build: () -> void
|
51
|
+
def clean: () -> void
|
52
|
+
|
53
|
+
# Allow dynamic method definitions
|
54
|
+
def method_missing: (Symbol name, *untyped args) ?{ (*untyped) -> untyped } -> untyped
|
55
|
+
def respond_to_missing?: (Symbol name, bool include_private) -> bool
|
56
|
+
|
57
|
+
# Allow dynamic class method definitions
|
58
|
+
def self.method_missing: (Symbol name, *untyped args) ?{ (*untyped) -> untyped } -> untyped
|
59
|
+
def self.respond_to_missing?: (Symbol name, bool include_private) -> bool
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Private class methods for dependency resolution
|
64
|
+
def self.resolve_queue: (Array[Class] queue, Array[Class] resolved) -> Array[Class]
|
65
|
+
def self.detect_circular_dependencies: (Array[Class] queue, Array[Class] resolved) -> void
|
66
|
+
def self.build_instance: (Class task_class) -> void
|
67
|
+
|
68
|
+
# Private class methods for Define API
|
69
|
+
def self.create_defined_method: (Symbol name) { () -> untyped } -> void
|
70
|
+
def self.create_ref_method_if_needed: () -> void
|
71
|
+
def self.method_defined_for_define?: (Symbol method_name) -> bool
|
72
|
+
def self.mark_method_as_defined: (Symbol method_name) -> void
|
73
|
+
end
|
74
|
+
|
75
|
+
# Dependency analyzer module
|
76
|
+
module DependencyAnalyzer
|
77
|
+
def self.analyze_method: (Class klass, Symbol method_name) -> Array[Class]
|
78
|
+
|
79
|
+
# Task dependency visitor for AST analysis
|
80
|
+
class TaskDependencyVisitor < Prism::Visitor
|
81
|
+
@dependencies: Array[Class]
|
82
|
+
|
83
|
+
def initialize: () -> void
|
84
|
+
def dependencies: () -> Array[Class]
|
85
|
+
def visit_call_node: (Prism::CallNode node) -> void
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def extract_class_from_constant: (String constant_name) -> Class?
|
90
|
+
def safe_constantize: (String name) -> Class?
|
91
|
+
def extract_class_from_ref_call: (Prism::CallNode node) -> Class?
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def self.parse_method_code: (Class klass, Symbol method_name) -> Prism::ParseResult?
|
97
|
+
def self.extract_dependencies_from_ast: (Prism::Node ast) -> Array[Class]
|
98
|
+
end
|
99
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,28 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: taski
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ahogappa
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-
|
11
|
-
dependencies:
|
10
|
+
date: 2025-06-21 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: prism
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '1.4'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '1.4'
|
12
26
|
description: Taski is a Ruby-based task runner currently under development. It allows
|
13
27
|
you to define small, composable tasks along with the outputs they depend on. Taski
|
14
28
|
statically resolves dependencies and executes tasks in the correct topological order,
|
@@ -21,11 +35,22 @@ executables: []
|
|
21
35
|
extensions: []
|
22
36
|
extra_rdoc_files: []
|
23
37
|
files:
|
38
|
+
- ".standard.yml"
|
24
39
|
- LICENSE
|
25
40
|
- README.md
|
26
41
|
- Rakefile
|
42
|
+
- examples/complex_example.rb
|
43
|
+
- examples/readme_example.rb
|
27
44
|
- lib/taski.rb
|
28
|
-
- lib/taski/
|
45
|
+
- lib/taski/dependency_analyzer.rb
|
46
|
+
- lib/taski/exceptions.rb
|
47
|
+
- lib/taski/logger.rb
|
48
|
+
- lib/taski/reference.rb
|
49
|
+
- lib/taski/task/base.rb
|
50
|
+
- lib/taski/task/define_api.rb
|
51
|
+
- lib/taski/task/dependency_resolver.rb
|
52
|
+
- lib/taski/task/exports_api.rb
|
53
|
+
- lib/taski/task/instance_management.rb
|
29
54
|
- lib/taski/version.rb
|
30
55
|
- sig/taski.rbs
|
31
56
|
homepage: https://github.com/ahogappa/taski
|
@@ -41,7 +66,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
66
|
requirements:
|
42
67
|
- - ">="
|
43
68
|
- !ruby/object:Gem::Version
|
44
|
-
version: 3.
|
69
|
+
version: 3.2.0
|
45
70
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
46
71
|
requirements:
|
47
72
|
- - ">="
|
data/lib/taski/utils.rb
DELETED
@@ -1,53 +0,0 @@
|
|
1
|
-
require 'fileutils'
|
2
|
-
require 'tmpdir'
|
3
|
-
|
4
|
-
module Taski
|
5
|
-
class Utils < FileUtils
|
6
|
-
def rm
|
7
|
-
|
8
|
-
end
|
9
|
-
|
10
|
-
def rm_f
|
11
|
-
end
|
12
|
-
|
13
|
-
def rm_rf
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
def cp
|
18
|
-
|
19
|
-
end
|
20
|
-
|
21
|
-
def cp_r
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
def mkdir
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
def mkdir_p
|
30
|
-
|
31
|
-
end
|
32
|
-
|
33
|
-
def mktmpdir
|
34
|
-
|
35
|
-
end
|
36
|
-
|
37
|
-
def cmd(command, info = nil, ret = false)
|
38
|
-
puts "exec: #{info}" if info
|
39
|
-
puts command
|
40
|
-
|
41
|
-
if ret
|
42
|
-
ret = `#{command}`.chomp
|
43
|
-
if $?.exited?
|
44
|
-
ret
|
45
|
-
else
|
46
|
-
raise "Failed to execute command: #{command}"
|
47
|
-
end
|
48
|
-
else
|
49
|
-
system command, exception: true
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|