asgard 0.1.2 → 0.2.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.
- checksums.yaml +4 -4
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/CHANGELOG.md +54 -1
- data/CLAUDE.md +2 -2
- data/README.md +39 -12
- data/docs/api.md +131 -0
- data/docs/assets/css/custom.css +93 -0
- data/docs/assets/images/asgard.jpg +0 -0
- data/docs/changelog.md +100 -0
- data/docs/dependencies.md +221 -0
- data/docs/environment.md +113 -0
- data/docs/examples.md +140 -0
- data/docs/getting-started.md +180 -0
- data/docs/helpers.md +154 -0
- data/docs/index.md +85 -0
- data/docs/options.md +180 -0
- data/docs/shell.md +208 -0
- data/docs/subcommands.md +181 -0
- data/docs/task-files.md +254 -0
- data/docs/tasks.md +284 -0
- data/docs/variables.md +122 -0
- data/examples/concurrent.loki +58 -0
- data/lib/asgard/base.rb +90 -27
- data/lib/asgard/shell.rb +3 -1
- data/lib/asgard/tasks.rb +6 -0
- data/lib/asgard/version.rb +1 -1
- data/lib/asgard.rb +7 -2
- data/mkdocs.yml +164 -0
- metadata +20 -1
data/docs/tasks.md
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# Defining Tasks
|
|
2
|
+
|
|
3
|
+
Every task is a public method inside `class Tasks`. Asgard pre-defines `Tasks` as a subclass of `Asgard::Base` (which is itself a Thor subclass), so your `.loki` files just reopen the class and add methods. The full Thor DSL is available everywhere.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Basic Task
|
|
8
|
+
|
|
9
|
+
A task with no parameters and no options:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class Tasks
|
|
13
|
+
desc "hello", "Say hello"
|
|
14
|
+
def hello = sh 'echo "Hello, World!"'
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`desc` takes two arguments: the usage string and the one-line description shown in `asgard help`.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
asgard hello
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Positional Parameter with Default
|
|
27
|
+
|
|
28
|
+
Positional parameters are declared directly in the method signature. Document them in the `desc` usage string (uppercase by convention):
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class Tasks
|
|
32
|
+
desc "greet NAME", "Greet NAME; omit NAME to greet the world"
|
|
33
|
+
def greet(name = "World")
|
|
34
|
+
sh "echo 'Hello, #{name}!'"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
asgard greet # Hello, World!
|
|
41
|
+
asgard greet Alice # Hello, Alice!
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Named Options
|
|
47
|
+
|
|
48
|
+
Use `method_option` (alias: `option`) for named flags. Access them inside the method via `options[:name]`.
|
|
49
|
+
|
|
50
|
+
### All Five Option Types
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class Tasks
|
|
54
|
+
desc "compile", "Compile the project"
|
|
55
|
+
option :output, aliases: "-o", type: :string, default: "dist/", desc: "Output directory"
|
|
56
|
+
option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Enable verbose output"
|
|
57
|
+
option :jobs, aliases: "-j", type: :numeric, default: 1, desc: "Number of parallel jobs"
|
|
58
|
+
option :tags, type: :array, desc: "Build tags to apply"
|
|
59
|
+
option :defines, type: :hash, desc: "Preprocessor defines (KEY:VALUE)"
|
|
60
|
+
def compile
|
|
61
|
+
puts "Compiling → #{options[:output]} with #{options[:jobs]} job(s)"
|
|
62
|
+
puts "Tags: #{options[:tags].join(', ')}" if options[:tags]
|
|
63
|
+
puts "Defines: #{options[:defines]}" if options[:defines]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Option Types Reference
|
|
69
|
+
|
|
70
|
+
| Type | CLI Example | Ruby Value |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `:string` | `--output dist/` | `"dist/"` |
|
|
73
|
+
| `:boolean` | `--verbose` / `--no-verbose` | `true` / `false` |
|
|
74
|
+
| `:numeric` | `--jobs 4` | `4` |
|
|
75
|
+
| `:array` | `--tags foo bar baz` | `["foo", "bar", "baz"]` |
|
|
76
|
+
| `:hash` | `--defines KEY:val FOO:bar` | `{"KEY"=>"val", "FOO"=>"bar"}` |
|
|
77
|
+
|
|
78
|
+
### Common Option Keys
|
|
79
|
+
|
|
80
|
+
| Key | Description |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `aliases` | Short-form flag, e.g. `"-o"` |
|
|
83
|
+
| `type` | `:string`, `:boolean`, `:numeric`, `:array`, or `:hash` |
|
|
84
|
+
| `default` | Value used when the flag is omitted |
|
|
85
|
+
| `required` | If `true`, Thor raises an error when the flag is missing |
|
|
86
|
+
| `desc` | One-line description shown in help |
|
|
87
|
+
| `enum` | Array of allowed values; Thor validates automatically |
|
|
88
|
+
| `banner` | Placeholder shown in help for the value slot, e.g. `"SECONDS"` |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Required Option
|
|
93
|
+
|
|
94
|
+
```ruby
|
|
95
|
+
class Tasks
|
|
96
|
+
desc "deploy ENV", "Deploy to ENV"
|
|
97
|
+
option :strategy,
|
|
98
|
+
type: :string,
|
|
99
|
+
required: true,
|
|
100
|
+
enum: %w[blue-green rolling canary],
|
|
101
|
+
desc: "Deployment strategy"
|
|
102
|
+
def deploy(env = "staging")
|
|
103
|
+
sh "cap #{env} deploy --strategy #{options[:strategy]}"
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
asgard deploy # Error: required option '--strategy' is missing
|
|
110
|
+
asgard deploy --strategy rolling
|
|
111
|
+
asgard deploy production --strategy blue-green
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Enum Validation
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
class Tasks
|
|
120
|
+
desc "build", "Build the project"
|
|
121
|
+
option :env,
|
|
122
|
+
type: :string,
|
|
123
|
+
default: "development",
|
|
124
|
+
enum: %w[development staging production],
|
|
125
|
+
desc: "Target environment"
|
|
126
|
+
def build
|
|
127
|
+
sh "rake build ENV=#{options[:env]}"
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Thor validates the value against the enum and shows a helpful error if it doesn't match.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Banner
|
|
137
|
+
|
|
138
|
+
`banner` replaces the default `VALUE` placeholder in help output with a more descriptive name:
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
class Tasks
|
|
142
|
+
desc "wait", "Wait for a service to become available"
|
|
143
|
+
option :timeout, type: :numeric, default: 30, banner: "SECONDS", desc: "Give up after SECONDS"
|
|
144
|
+
def wait
|
|
145
|
+
sh "wait-for-it --timeout #{options[:timeout]}"
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Help output shows: `[--timeout=SECONDS]` instead of `[--timeout=VALUE]`.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Extended Description
|
|
155
|
+
|
|
156
|
+
`long_desc` provides detailed help shown by `asgard help <task>`. Use `\x5` at the start of a line to force a line break within the wrapped text (a Thor convention):
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
class Tasks
|
|
160
|
+
long_desc <<~DESC
|
|
161
|
+
Generates a project report covering test coverage, lint results,
|
|
162
|
+
and a dependency audit.
|
|
163
|
+
|
|
164
|
+
Pass --format to control output style. Use --since to scope the
|
|
165
|
+
report to changes after a given date.
|
|
166
|
+
|
|
167
|
+
Examples:\x5
|
|
168
|
+
asgard report --format html --since 2024-01-01\x5
|
|
169
|
+
asgard report --format json --output report.json\x5
|
|
170
|
+
asgard report --format text
|
|
171
|
+
DESC
|
|
172
|
+
desc "report", "Generate a project report"
|
|
173
|
+
option :format, type: :string, default: "text", enum: %w[text html json], desc: "Output format"
|
|
174
|
+
option :since, type: :string, banner: "DATE", desc: "Limit to changes after DATE"
|
|
175
|
+
def report
|
|
176
|
+
sh "generate-report --format #{options[:format]}"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
!!! tip
|
|
182
|
+
`desc` and `depends_on` are independent of each other — either can come first, but both must appear before the `def`.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Default Task
|
|
187
|
+
|
|
188
|
+
`default_task` declares which command runs when `asgard` is invoked with no arguments:
|
|
189
|
+
|
|
190
|
+
```ruby
|
|
191
|
+
class Tasks
|
|
192
|
+
default_task :greet
|
|
193
|
+
|
|
194
|
+
desc "greet", "Say hello (runs by default)"
|
|
195
|
+
def greet
|
|
196
|
+
puts "Hello from Asgard!"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
asgard # same as: asgard greet
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Command Aliases
|
|
208
|
+
|
|
209
|
+
`map` creates short aliases for existing tasks:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
class Tasks
|
|
213
|
+
map "-v" => "version"
|
|
214
|
+
map "--v" => "version"
|
|
215
|
+
map "t" => "test"
|
|
216
|
+
map "b" => "build"
|
|
217
|
+
|
|
218
|
+
desc "version", "Print the version"
|
|
219
|
+
def version = puts Asgard::VERSION
|
|
220
|
+
|
|
221
|
+
desc "test", "Run tests"
|
|
222
|
+
def test = sh "bundle exec rake test"
|
|
223
|
+
|
|
224
|
+
desc "build", "Build the gem"
|
|
225
|
+
def build = sh "bundle exec rake build"
|
|
226
|
+
end
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
asgard t # same as: asgard test
|
|
231
|
+
asgard b # same as: asgard build
|
|
232
|
+
asgard -v # same as: asgard version (note: --version is the built-in flag)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Formal Argument Declaration
|
|
238
|
+
|
|
239
|
+
`argument` provides rich positional-parameter metadata including type checking, enums, and help text.
|
|
240
|
+
|
|
241
|
+
!!! warning "Class-level scope"
|
|
242
|
+
`argument` is a **class-level declaration** that applies to **every task in the class**, not just the one that follows it. It is best suited for single-command CLIs or when every task in the file genuinely shares the same positional input. In multi-task files, prefer method signature parameters instead.
|
|
243
|
+
|
|
244
|
+
```ruby
|
|
245
|
+
class Tasks
|
|
246
|
+
argument :name,
|
|
247
|
+
type: :string,
|
|
248
|
+
default: "World",
|
|
249
|
+
desc: "Name to greet"
|
|
250
|
+
|
|
251
|
+
desc "hello NAME", "Say hello to NAME"
|
|
252
|
+
def hello = sh "echo 'Hello, #{name}!'"
|
|
253
|
+
end
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
For most multi-task `.loki` files, the simpler positional default pattern is safer:
|
|
257
|
+
|
|
258
|
+
```ruby
|
|
259
|
+
def hello(name = "World") = sh "echo 'Hello, #{name}!'"
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## No Commands Block
|
|
265
|
+
|
|
266
|
+
`no_commands` marks a block of methods as public helpers that are excluded from the CLI and `--help` output. They are callable from any task in the same class:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
class Tasks
|
|
270
|
+
desc "build", "Compile the project"
|
|
271
|
+
def build
|
|
272
|
+
puts "Revision: #{current_sha}"
|
|
273
|
+
sh "rake build"
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
no_commands do
|
|
277
|
+
def current_sha
|
|
278
|
+
`git rev-parse --short HEAD`.strip
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
See [Helper Methods](helpers.md) for the full guide on helpers, `private`, and cross-file sharing.
|
data/docs/variables.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Variables
|
|
2
|
+
|
|
3
|
+
`var` declares a named value that is available to all tasks in the class as a method call. Values can be static or lazily evaluated.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Static Value
|
|
8
|
+
|
|
9
|
+
Pass the value directly as the second argument:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
class Tasks
|
|
13
|
+
var :app, "myapp"
|
|
14
|
+
var :env, "production"
|
|
15
|
+
var :port, 3000
|
|
16
|
+
|
|
17
|
+
desc "info", "Print app info"
|
|
18
|
+
def info
|
|
19
|
+
puts "#{app} running on port #{port} in #{env}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Lazy Lambda
|
|
27
|
+
|
|
28
|
+
Pass a lambda (or proc) to defer evaluation until the variable is first accessed. The lambda is called once and its return value is used for all subsequent accesses:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
class Tasks
|
|
32
|
+
var :version, -> { `git describe --tags`.strip }
|
|
33
|
+
var :sha, -> { `git rev-parse --short HEAD`.strip }
|
|
34
|
+
|
|
35
|
+
desc "tag", "Create a release tag"
|
|
36
|
+
def tag = sh "git tag v#{version}"
|
|
37
|
+
|
|
38
|
+
desc "info", "Show version info"
|
|
39
|
+
def info = puts "#{version} (#{sha})"
|
|
40
|
+
end
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
!!! tip
|
|
44
|
+
Lazy lambdas are ideal for values that require a shell call or file read — they only pay the cost if the variable is actually used in the task being run.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Block Syntax
|
|
49
|
+
|
|
50
|
+
You can also use a block instead of a lambda:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
class Tasks
|
|
54
|
+
var(:build_dir) { "builds/#{app}" }
|
|
55
|
+
var(:app) { "myapp" }
|
|
56
|
+
|
|
57
|
+
desc "build", "Compile into build_dir"
|
|
58
|
+
def build = sh "rake build OUTDIR=#{build_dir}"
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
!!! note
|
|
63
|
+
The block form and the lambda form behave identically — both are stored as callables and invoked on first access.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Accessing Variables from Tasks
|
|
68
|
+
|
|
69
|
+
Variables are available as method calls from within any task body (or other method) in the same class. They are defined using `no_commands`, so they appear neither in `--help` output nor as CLI commands:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
class Tasks
|
|
73
|
+
var :app, "myapp"
|
|
74
|
+
var :version, -> { `git describe --tags`.strip }
|
|
75
|
+
var :pkg, -> { "pkg/#{app}-#{version}.gem" }
|
|
76
|
+
|
|
77
|
+
desc "build", "Build the gem"
|
|
78
|
+
def build = sh "gem build #{app}.gemspec"
|
|
79
|
+
|
|
80
|
+
desc "push", "Push the gem to RubyGems"
|
|
81
|
+
def push = sh "gem push #{pkg}"
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Variables can reference other variables in their lambdas as long as the referenced variable is also defined with `var` on the same class.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Sharing Variables Across Files
|
|
90
|
+
|
|
91
|
+
Because all `.loki` files reopen the same `class Tasks`, variables declared in one file are available in all other files loaded in the same session:
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
# config.loki
|
|
95
|
+
class Tasks
|
|
96
|
+
var :app, "myapp"
|
|
97
|
+
var :port, 8080
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# deploy.loki
|
|
101
|
+
class Tasks
|
|
102
|
+
desc "deploy", "Deploy the app"
|
|
103
|
+
def deploy = sh "cap deploy APP=#{app} PORT=#{port}"
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Naming Caution
|
|
110
|
+
|
|
111
|
+
!!! warning
|
|
112
|
+
Do not use `var` names that conflict with built-in Ruby method names, Thor DSL method names, or Asgard's own built-in methods. In particular, avoid naming a variable `version` — `Tasks` already defines `_version` (the `--version` flag handler), and a `var :version` would collide with that namespace and produce confusing behavior. Use a more specific name like `app_version` or `gem_version` instead.
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
# Avoid this — conflicts with the built-in version infrastructure:
|
|
116
|
+
# var :version, -> { "1.0.0" }
|
|
117
|
+
|
|
118
|
+
# Use this instead:
|
|
119
|
+
var :app_version, -> { `git describe --tags`.strip }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Other names to avoid: `options`, `class_options`, `shell`, `invoke`, `invoke_command`.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Demonstrates concurrent task execution via parallel depends_on groups.
|
|
3
|
+
#
|
|
4
|
+
# Each worker prints its letter repeatedly with random delays. Because
|
|
5
|
+
# worker_a, worker_b, and worker_c run in separate threads their output
|
|
6
|
+
# interleaves on stdout, proving real concurrency.
|
|
7
|
+
#
|
|
8
|
+
# Execution order:
|
|
9
|
+
# start → worker_a + worker_b + worker_c (all three concurrent) → finish
|
|
10
|
+
#
|
|
11
|
+
# Run with:
|
|
12
|
+
# asgard finish
|
|
13
|
+
#
|
|
14
|
+
# Sample output (character order varies every run):
|
|
15
|
+
# start
|
|
16
|
+
# ABCBACBACBABCBACBACB
|
|
17
|
+
# end
|
|
18
|
+
|
|
19
|
+
$stdout.sync = true # flush every print immediately across all threads
|
|
20
|
+
|
|
21
|
+
CONCURRENT_REPS = 10 # how many times each worker prints its character
|
|
22
|
+
|
|
23
|
+
class Tasks
|
|
24
|
+
desc "start", "Print start marker"
|
|
25
|
+
def start
|
|
26
|
+
puts "starting demo of concurrent task execution ..."
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
desc "worker_a", "Print 'A' repeatedly with random delays"
|
|
30
|
+
def worker_a
|
|
31
|
+
CONCURRENT_REPS.times do
|
|
32
|
+
print "A"
|
|
33
|
+
sleep rand(0.05..0.3)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
desc "worker_b", "Print 'B' repeatedly with random delays"
|
|
38
|
+
def worker_b
|
|
39
|
+
CONCURRENT_REPS.times do
|
|
40
|
+
print "B"
|
|
41
|
+
sleep rand(0.05..0.3)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
desc "worker_c", "Print 'C' repeatedly with random delays"
|
|
46
|
+
def worker_c
|
|
47
|
+
CONCURRENT_REPS.times do
|
|
48
|
+
print "C"
|
|
49
|
+
sleep rand(0.05..0.3)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
depends_on :start, [:worker_a, :worker_b, :worker_c]
|
|
54
|
+
desc "finish", "Print end marker after all workers complete"
|
|
55
|
+
def finish
|
|
56
|
+
puts "\nfini - the end of concurrent task demo"
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/asgard/base.rb
CHANGED
|
@@ -19,8 +19,10 @@ module Asgard
|
|
|
19
19
|
subclass.instance_variable_set(:@_deps, {})
|
|
20
20
|
subclass.instance_variable_set(:@_vars, {})
|
|
21
21
|
subclass.instance_variable_set(:@_pending_deps, [])
|
|
22
|
-
subclass.instance_variable_set(:@
|
|
23
|
-
subclass.instance_variable_set(:@
|
|
22
|
+
subclass.instance_variable_set(:@_running, Set.new)
|
|
23
|
+
subclass.instance_variable_set(:@_done, Set.new)
|
|
24
|
+
subclass.instance_variable_set(:@_cond, Hash.new { |h, k| h[k] = ConditionVariable.new })
|
|
25
|
+
subclass.instance_variable_set(:@_ran_mutex, Mutex.new)
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def _deps
|
|
@@ -31,8 +33,16 @@ module Asgard
|
|
|
31
33
|
@_vars ||= {}
|
|
32
34
|
end
|
|
33
35
|
|
|
34
|
-
def
|
|
35
|
-
@
|
|
36
|
+
def _running
|
|
37
|
+
@_running ||= Set.new
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def _done
|
|
41
|
+
@_done ||= Set.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def _cond
|
|
45
|
+
@_cond ||= Hash.new { |h, k| h[k] = ConditionVariable.new }
|
|
36
46
|
end
|
|
37
47
|
|
|
38
48
|
def _ran_mutex
|
|
@@ -41,7 +51,11 @@ module Asgard
|
|
|
41
51
|
|
|
42
52
|
# Reset execution tracking for a fresh asgard invocation.
|
|
43
53
|
def _reset_ran!
|
|
44
|
-
_ran_mutex.synchronize
|
|
54
|
+
_ran_mutex.synchronize do
|
|
55
|
+
@_running = Set.new
|
|
56
|
+
@_done = Set.new
|
|
57
|
+
@_cond = Hash.new { |h, k| h[k] = ConditionVariable.new }
|
|
58
|
+
end
|
|
45
59
|
end
|
|
46
60
|
|
|
47
61
|
# Translate stages into a DependencyGraph-compatible hash.
|
|
@@ -73,8 +87,12 @@ module Asgard
|
|
|
73
87
|
_vars[name.to_sym] = value
|
|
74
88
|
no_commands do
|
|
75
89
|
define_method(name) do
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
ivar = :"@__var_#{name}"
|
|
91
|
+
unless instance_variable_defined?(ivar)
|
|
92
|
+
v = self.class._vars[name.to_sym]
|
|
93
|
+
instance_variable_set(ivar, v.respond_to?(:call) ? v.call : v)
|
|
94
|
+
end
|
|
95
|
+
instance_variable_get(ivar)
|
|
78
96
|
end
|
|
79
97
|
end
|
|
80
98
|
end
|
|
@@ -90,19 +108,44 @@ module Asgard
|
|
|
90
108
|
|
|
91
109
|
# Validate the full dep graph for cycles using Dagwood::DependencyGraph.
|
|
92
110
|
def validate_deps!
|
|
111
|
+
pending = Array(@_pending_deps)
|
|
112
|
+
if pending.any?
|
|
113
|
+
raise Asgard::Error,
|
|
114
|
+
"depends_on(#{pending.join(', ')}) declared without a following task definition"
|
|
115
|
+
end
|
|
116
|
+
|
|
93
117
|
return if _deps.empty?
|
|
94
118
|
|
|
95
|
-
|
|
96
|
-
full_graph
|
|
119
|
+
all_task_names = all_commands.keys.map(&:to_sym)
|
|
120
|
+
full_graph = all_task_names.each_with_object({}) do |task, hash|
|
|
97
121
|
hash[task] = _deps.fetch(task, []).flatten
|
|
98
122
|
end
|
|
99
123
|
|
|
124
|
+
undefined = _deps.values.flatten.uniq - all_task_names
|
|
125
|
+
if undefined.any?
|
|
126
|
+
raise Asgard::Error, "undefined task(s) in depends_on: #{undefined.sort.join(', ')}"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
_deps.each do |_task, stages|
|
|
130
|
+
stages.flatten.each do |dep|
|
|
131
|
+
meth = instance_method(dep.to_s) rescue nil
|
|
132
|
+
next unless meth
|
|
133
|
+
required = meth.parameters.count { |type, _| type == :req }
|
|
134
|
+
if required > 0
|
|
135
|
+
raise Asgard::Error,
|
|
136
|
+
"task '#{dep}' has #{required} required argument(s) and cannot be used as a dependency"
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
100
141
|
Dagwood::DependencyGraph.new(full_graph).order
|
|
101
142
|
rescue TSort::Cyclic => e
|
|
102
143
|
raise Asgard::CircularDependencyError, e.message
|
|
103
144
|
end
|
|
104
145
|
|
|
105
146
|
def method_added(method_name)
|
|
147
|
+
return super unless @usage
|
|
148
|
+
|
|
106
149
|
pending = Array(@_pending_deps).dup
|
|
107
150
|
@_pending_deps = []
|
|
108
151
|
|
|
@@ -117,36 +160,56 @@ module Asgard
|
|
|
117
160
|
|
|
118
161
|
no_commands do
|
|
119
162
|
# Dispatch hook: resolves and runs all deps (in parallel where declared)
|
|
120
|
-
# before executing the target command.
|
|
121
|
-
#
|
|
163
|
+
# before executing the target command.
|
|
164
|
+
#
|
|
165
|
+
# Completion-based deduplication: a task is only marked done after its
|
|
166
|
+
# body finishes. Threads that arrive at an already-running shared dep
|
|
167
|
+
# wait on its ConditionVariable rather than proceeding immediately,
|
|
168
|
+
# preventing the race where parallel tasks start before a shared dep
|
|
169
|
+
# has actually completed.
|
|
122
170
|
def invoke_command(command, *args)
|
|
123
171
|
$DEBUG = true if options[:debug]
|
|
124
172
|
$VERBOSE = true if options[:verbose]
|
|
125
173
|
target = command.name.to_sym
|
|
126
174
|
|
|
127
175
|
should_run = self.class._ran_mutex.synchronize do
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
176
|
+
if self.class._done.include?(target)
|
|
177
|
+
false
|
|
178
|
+
elsif self.class._running.include?(target)
|
|
179
|
+
self.class._cond[target].wait(self.class._ran_mutex) until self.class._done.include?(target)
|
|
180
|
+
false
|
|
181
|
+
else
|
|
182
|
+
self.class._running.add(target)
|
|
183
|
+
true
|
|
184
|
+
end
|
|
131
185
|
end
|
|
132
186
|
return unless should_run
|
|
133
187
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
188
|
+
begin
|
|
189
|
+
stages = self.class._deps[target]
|
|
190
|
+
if stages&.any?
|
|
191
|
+
graph = self.class._build_dep_graph(stages)
|
|
192
|
+
groups = Dagwood::DependencyGraph.new(graph).parallel_order
|
|
193
|
+
|
|
194
|
+
groups.each do |group|
|
|
195
|
+
if group.size > 1
|
|
196
|
+
threads = group.map { |task| Thread.new { _run_dep(task) } }
|
|
197
|
+
errors = []
|
|
198
|
+
threads.each { |t| begin; t.join; rescue => e; errors << e; end }
|
|
199
|
+
raise errors.first if errors.any?
|
|
200
|
+
else
|
|
201
|
+
_run_dep(group.first)
|
|
202
|
+
end
|
|
145
203
|
end
|
|
146
204
|
end
|
|
147
|
-
end
|
|
148
205
|
|
|
149
|
-
|
|
206
|
+
command.run(self, *args)
|
|
207
|
+
ensure
|
|
208
|
+
self.class._ran_mutex.synchronize do
|
|
209
|
+
self.class._done.add(target)
|
|
210
|
+
self.class._cond[target].broadcast
|
|
211
|
+
end
|
|
212
|
+
end
|
|
150
213
|
end
|
|
151
214
|
|
|
152
215
|
def _run_dep(task)
|
data/lib/asgard/shell.rb
CHANGED
|
@@ -32,7 +32,9 @@ module Asgard
|
|
|
32
32
|
}
|
|
33
33
|
ext = extensions.fetch(interpreter.to_sym, ".tmp")
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
$stdout.puts script unless silent
|
|
36
|
+
|
|
37
|
+
Tempfile.create(["asgard_", ext]) do |f|
|
|
36
38
|
f.write(script)
|
|
37
39
|
f.flush
|
|
38
40
|
system(interpreter.to_s, f.path)
|
data/lib/asgard/tasks.rb
CHANGED
|
@@ -14,6 +14,12 @@ class Tasks < Asgard::Base
|
|
|
14
14
|
default: false,
|
|
15
15
|
desc: "Enable verbose output ($VERBOSE = true)"
|
|
16
16
|
|
|
17
|
+
desc "--auto-load", "Load all *.loki files from the project root before running"
|
|
18
|
+
map "--auto-load" => :_auto_load
|
|
19
|
+
def _auto_load
|
|
20
|
+
# Consumed by run! before Thor dispatch — never called directly.
|
|
21
|
+
end
|
|
22
|
+
|
|
17
23
|
desc "--version", "Show asgard version"
|
|
18
24
|
map "--version" => :_version
|
|
19
25
|
def _version
|
data/lib/asgard/version.rb
CHANGED
data/lib/asgard.rb
CHANGED
|
@@ -32,14 +32,19 @@ module Asgard
|
|
|
32
32
|
|
|
33
33
|
# Main entry point invoked by the asgard executable.
|
|
34
34
|
def self.run!(argv)
|
|
35
|
+
auto_load = argv.delete("--auto-load")
|
|
35
36
|
abort "asgard: unknown command '#{argv.first}'" if argv.first&.start_with?("_")
|
|
36
37
|
task_file = find_task_file or abort "asgard: no .loki file found in #{Dir.pwd}"
|
|
37
|
-
|
|
38
|
+
before = Asgard::Base.subclasses.dup
|
|
39
|
+
load_loki(File.dirname(task_file)) if auto_load
|
|
38
40
|
load task_file
|
|
39
|
-
|
|
41
|
+
newly_defined = Asgard::Base.subclasses - before
|
|
42
|
+
(newly_defined + [Tasks]).uniq.each(&:validate_deps!)
|
|
40
43
|
Tasks._reset_ran!
|
|
41
44
|
Tasks.start(argv)
|
|
42
45
|
rescue CircularDependencyError => e
|
|
43
46
|
abort "asgard: circular dependency — #{e.message}"
|
|
47
|
+
rescue Error => e
|
|
48
|
+
abort "asgard: #{e.message}"
|
|
44
49
|
end
|
|
45
50
|
end
|