tasktree 0.0.8__tar.gz → 0.0.10__tar.gz
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.
- {tasktree-0.0.8 → tasktree-0.0.10}/PKG-INFO +92 -1
- {tasktree-0.0.8 → tasktree-0.0.10}/README.md +91 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/pyproject.toml +1 -1
- {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/cli.py +54 -6
- {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/docker.py +17 -9
- {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/executor.py +85 -12
- tasktree-0.0.10/src/tasktree/graph.py +266 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/hasher.py +46 -2
- {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/parser.py +280 -15
- {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/substitution.py +5 -2
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_cli_options.py +1 -1
- tasktree-0.0.10/tests/integration/test_docker_build_args.py +79 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_end_to_end.py +3 -3
- tasktree-0.0.10/tests/integration/test_parameterized_dependencies.yaml +27 -0
- tasktree-0.0.10/tests/integration/test_parameterized_deps_execution.py +191 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_variables.py +1 -1
- tasktree-0.0.10/tests/unit/test_dependency_parsing.py +154 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_docker.py +159 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_executor.py +15 -15
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_graph.py +9 -7
- tasktree-0.0.10/tests/unit/test_list_formatting.py +342 -0
- tasktree-0.0.10/tests/unit/test_parameterized_graph.py +155 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_parser.py +67 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_substitution.py +2 -2
- tasktree-0.0.8/src/tasktree/graph.py +0 -168
- {tasktree-0.0.8 → tasktree-0.0.10}/.claude/settings.local.json +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/.github/workflows/claude-code-review.yml +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/.github/workflows/claude.yml +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/.github/workflows/release.yml +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/.github/workflows/test.yml +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/.gitignore +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/.python-version +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/CLAUDE.md +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/example/source.txt +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/example/tasktree.yaml +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/requirements/implemented/01-basic-variables.md +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/requirements/implemented/02-env-variable-type.md +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/requirements/implemented/03-direct-env-substitution.md +0 -0
- {tasktree-0.0.8/requirements → tasktree-0.0.10/requirements/implemented}/04-file-read-variables.md +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/requirements/implemented/bug-report-dependency-triggering.md +0 -0
- {tasktree-0.0.8/requirements/future → tasktree-0.0.10/requirements/implemented}/docker-task-environments.md +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/requirements/implemented/shell-environment-requirements.md +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/schema/README.md +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/schema/tasktree-schema.json +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/schema/vscode-settings-snippet.json +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/src/__init__.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/__init__.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/state.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/tasks.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/src/tasktree/types.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tasktree.yaml +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/e2e/__init__.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/e2e/test_docker_basic.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/e2e/test_docker_environment.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/e2e/test_docker_ownership.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/e2e/test_docker_volumes.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_arg_choices.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_arg_min_max.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_builtin_variables.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_clean_state.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_dependency_execution.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_exported_args.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_input_detection.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_missing_outputs.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_nested_imports.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_state_persistence.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/integration/test_working_directory.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_cli.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_environment_tracking.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_hasher.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_state.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_tasks.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/tests/unit/test_types.py +0 -0
- {tasktree-0.0.8 → tasktree-0.0.10}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tasktree
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
4
4
|
Summary: A task automation tool with incremental execution
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Requires-Dist: click>=8.1.0
|
|
@@ -594,6 +594,97 @@ If an exported argument with a default isn't available as an environment variabl
|
|
|
594
594
|
2. The CLI automatically applies defaults before execution
|
|
595
595
|
3. You can explicitly provide the value: `tt deploy prod-server port=8080`
|
|
596
596
|
|
|
597
|
+
### Parameterized Dependencies
|
|
598
|
+
|
|
599
|
+
Dependencies can invoke tasks with specific arguments, enabling flexible and reusable task graphs:
|
|
600
|
+
|
|
601
|
+
**Syntax:**
|
|
602
|
+
|
|
603
|
+
```yaml
|
|
604
|
+
tasks:
|
|
605
|
+
# Task with parameters
|
|
606
|
+
process:
|
|
607
|
+
args: [mode, verbose=false]
|
|
608
|
+
cmd: echo "mode={{arg.mode}} verbose={{arg.verbose}}"
|
|
609
|
+
|
|
610
|
+
# Simple dependency (uses defaults)
|
|
611
|
+
consumer1:
|
|
612
|
+
deps: [process] # Equivalent to: process(mode must be provided)
|
|
613
|
+
cmd: echo "done"
|
|
614
|
+
|
|
615
|
+
# Positional arguments
|
|
616
|
+
consumer2:
|
|
617
|
+
deps:
|
|
618
|
+
- process: [debug, true] # Maps to: mode=debug, verbose=true
|
|
619
|
+
cmd: echo "done"
|
|
620
|
+
|
|
621
|
+
# Named arguments
|
|
622
|
+
consumer3:
|
|
623
|
+
deps:
|
|
624
|
+
- process: {mode: release, verbose: false}
|
|
625
|
+
cmd: echo "done"
|
|
626
|
+
|
|
627
|
+
# Multiple invocations with different args
|
|
628
|
+
multi_build:
|
|
629
|
+
deps:
|
|
630
|
+
- process: [debug]
|
|
631
|
+
- process: [release]
|
|
632
|
+
cmd: echo "All builds complete"
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
**Key behaviors:**
|
|
636
|
+
|
|
637
|
+
- **Simple string form** (`- task_name`): Uses task defaults for all arguments. Required arguments must have defaults or task invocation fails.
|
|
638
|
+
- **Positional form** (`- task_name: [arg1, arg2]`): Arguments mapped by position. Can omit trailing args if they have defaults.
|
|
639
|
+
- **Named form** (`- task_name: {arg1: val1}`): Arguments mapped by name. Can omit any arg with a default.
|
|
640
|
+
- **Multiple invocations**: Same task with different arguments creates separate graph nodes, each executing independently.
|
|
641
|
+
- **Normalization**: All forms normalized to named arguments with defaults filled before execution.
|
|
642
|
+
- **Cache separation**: `process(debug)` and `process(release)` cache independently.
|
|
643
|
+
|
|
644
|
+
**Restrictions:**
|
|
645
|
+
|
|
646
|
+
- **No empty lists**: `- task: []` is invalid (use `- task` instead)
|
|
647
|
+
- **No mixed positional and named**: Choose one form per dependency
|
|
648
|
+
- **Single-key dicts**: `{task1: [x], task2: [y]}` is invalid (multi-key not allowed)
|
|
649
|
+
|
|
650
|
+
**Validation:**
|
|
651
|
+
|
|
652
|
+
Validation happens at graph construction time with clear error messages:
|
|
653
|
+
|
|
654
|
+
```
|
|
655
|
+
Task 'process' takes 2 arguments, got 3
|
|
656
|
+
Task 'build' has no argument named 'mode'
|
|
657
|
+
Task 'deploy' requires argument 'environment' (no default provided)
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
**Example use cases:**
|
|
661
|
+
|
|
662
|
+
```yaml
|
|
663
|
+
tasks:
|
|
664
|
+
# Compile for different platforms
|
|
665
|
+
compile:
|
|
666
|
+
args: [target]
|
|
667
|
+
cmd: cargo build --target {{arg.target}}
|
|
668
|
+
|
|
669
|
+
dist:
|
|
670
|
+
deps:
|
|
671
|
+
- compile: [x86_64-unknown-linux-gnu]
|
|
672
|
+
- compile: [aarch64-unknown-linux-gnu]
|
|
673
|
+
cmd: tar czf dist.tar.gz target/*/release/app
|
|
674
|
+
|
|
675
|
+
# Run tests with different configurations
|
|
676
|
+
test:
|
|
677
|
+
args: [config]
|
|
678
|
+
cmd: pytest --config={{arg.config}}
|
|
679
|
+
|
|
680
|
+
ci:
|
|
681
|
+
deps:
|
|
682
|
+
- test: [unit]
|
|
683
|
+
- test: [integration]
|
|
684
|
+
- test: [e2e]
|
|
685
|
+
cmd: echo "All tests passed"
|
|
686
|
+
```
|
|
687
|
+
|
|
597
688
|
## Environment Variables
|
|
598
689
|
|
|
599
690
|
Task Tree supports reading environment variables in two ways:
|
|
@@ -579,6 +579,97 @@ If an exported argument with a default isn't available as an environment variabl
|
|
|
579
579
|
2. The CLI automatically applies defaults before execution
|
|
580
580
|
3. You can explicitly provide the value: `tt deploy prod-server port=8080`
|
|
581
581
|
|
|
582
|
+
### Parameterized Dependencies
|
|
583
|
+
|
|
584
|
+
Dependencies can invoke tasks with specific arguments, enabling flexible and reusable task graphs:
|
|
585
|
+
|
|
586
|
+
**Syntax:**
|
|
587
|
+
|
|
588
|
+
```yaml
|
|
589
|
+
tasks:
|
|
590
|
+
# Task with parameters
|
|
591
|
+
process:
|
|
592
|
+
args: [mode, verbose=false]
|
|
593
|
+
cmd: echo "mode={{arg.mode}} verbose={{arg.verbose}}"
|
|
594
|
+
|
|
595
|
+
# Simple dependency (uses defaults)
|
|
596
|
+
consumer1:
|
|
597
|
+
deps: [process] # Equivalent to: process(mode must be provided)
|
|
598
|
+
cmd: echo "done"
|
|
599
|
+
|
|
600
|
+
# Positional arguments
|
|
601
|
+
consumer2:
|
|
602
|
+
deps:
|
|
603
|
+
- process: [debug, true] # Maps to: mode=debug, verbose=true
|
|
604
|
+
cmd: echo "done"
|
|
605
|
+
|
|
606
|
+
# Named arguments
|
|
607
|
+
consumer3:
|
|
608
|
+
deps:
|
|
609
|
+
- process: {mode: release, verbose: false}
|
|
610
|
+
cmd: echo "done"
|
|
611
|
+
|
|
612
|
+
# Multiple invocations with different args
|
|
613
|
+
multi_build:
|
|
614
|
+
deps:
|
|
615
|
+
- process: [debug]
|
|
616
|
+
- process: [release]
|
|
617
|
+
cmd: echo "All builds complete"
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
**Key behaviors:**
|
|
621
|
+
|
|
622
|
+
- **Simple string form** (`- task_name`): Uses task defaults for all arguments. Required arguments must have defaults or task invocation fails.
|
|
623
|
+
- **Positional form** (`- task_name: [arg1, arg2]`): Arguments mapped by position. Can omit trailing args if they have defaults.
|
|
624
|
+
- **Named form** (`- task_name: {arg1: val1}`): Arguments mapped by name. Can omit any arg with a default.
|
|
625
|
+
- **Multiple invocations**: Same task with different arguments creates separate graph nodes, each executing independently.
|
|
626
|
+
- **Normalization**: All forms normalized to named arguments with defaults filled before execution.
|
|
627
|
+
- **Cache separation**: `process(debug)` and `process(release)` cache independently.
|
|
628
|
+
|
|
629
|
+
**Restrictions:**
|
|
630
|
+
|
|
631
|
+
- **No empty lists**: `- task: []` is invalid (use `- task` instead)
|
|
632
|
+
- **No mixed positional and named**: Choose one form per dependency
|
|
633
|
+
- **Single-key dicts**: `{task1: [x], task2: [y]}` is invalid (multi-key not allowed)
|
|
634
|
+
|
|
635
|
+
**Validation:**
|
|
636
|
+
|
|
637
|
+
Validation happens at graph construction time with clear error messages:
|
|
638
|
+
|
|
639
|
+
```
|
|
640
|
+
Task 'process' takes 2 arguments, got 3
|
|
641
|
+
Task 'build' has no argument named 'mode'
|
|
642
|
+
Task 'deploy' requires argument 'environment' (no default provided)
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
**Example use cases:**
|
|
646
|
+
|
|
647
|
+
```yaml
|
|
648
|
+
tasks:
|
|
649
|
+
# Compile for different platforms
|
|
650
|
+
compile:
|
|
651
|
+
args: [target]
|
|
652
|
+
cmd: cargo build --target {{arg.target}}
|
|
653
|
+
|
|
654
|
+
dist:
|
|
655
|
+
deps:
|
|
656
|
+
- compile: [x86_64-unknown-linux-gnu]
|
|
657
|
+
- compile: [aarch64-unknown-linux-gnu]
|
|
658
|
+
cmd: tar czf dist.tar.gz target/*/release/app
|
|
659
|
+
|
|
660
|
+
# Run tests with different configurations
|
|
661
|
+
test:
|
|
662
|
+
args: [config]
|
|
663
|
+
cmd: pytest --config={{arg.config}}
|
|
664
|
+
|
|
665
|
+
ci:
|
|
666
|
+
deps:
|
|
667
|
+
- test: [unit]
|
|
668
|
+
- test: [integration]
|
|
669
|
+
- test: [e2e]
|
|
670
|
+
cmd: echo "All tests passed"
|
|
671
|
+
```
|
|
672
|
+
|
|
582
673
|
## Environment Variables
|
|
583
674
|
|
|
584
675
|
Task Tree supports reading environment variables in two ways:
|
|
@@ -26,6 +26,41 @@ app = typer.Typer(
|
|
|
26
26
|
console = Console()
|
|
27
27
|
|
|
28
28
|
|
|
29
|
+
def _format_task_arguments(arg_specs: list[str | dict]) -> str:
|
|
30
|
+
"""Format task arguments for display in list output.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
arg_specs: List of argument specifications from task definition (strings or dicts)
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Formatted string showing arguments with types and defaults
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
["mode", "target"] -> "mode:str target:str"
|
|
40
|
+
["mode=debug", "target=x86_64"] -> "mode:str [=debug] target:str [=x86_64]"
|
|
41
|
+
["port:int", "debug:bool=false"] -> "port:int debug:bool [=false]"
|
|
42
|
+
[{"timeout": {"type": "int", "default": 30}}] -> "timeout:int [=30]"
|
|
43
|
+
"""
|
|
44
|
+
if not arg_specs:
|
|
45
|
+
return ""
|
|
46
|
+
|
|
47
|
+
formatted_parts = []
|
|
48
|
+
for spec_str in arg_specs:
|
|
49
|
+
parsed = parse_arg_spec(spec_str)
|
|
50
|
+
|
|
51
|
+
# Format: name:type or name:type [=default]
|
|
52
|
+
# Argument names in normal intensity, types and defaults in dim
|
|
53
|
+
arg_part = f"{parsed.name}[dim]:{parsed.arg_type}[/dim]"
|
|
54
|
+
|
|
55
|
+
if parsed.default is not None:
|
|
56
|
+
# Use dim styling for the default value part
|
|
57
|
+
arg_part += f" [dim]\\[={parsed.default}][/dim]"
|
|
58
|
+
|
|
59
|
+
formatted_parts.append(arg_part)
|
|
60
|
+
|
|
61
|
+
return " ".join(formatted_parts)
|
|
62
|
+
|
|
63
|
+
|
|
29
64
|
def _list_tasks(tasks_file: Optional[str] = None):
|
|
30
65
|
"""List all available tasks with descriptions."""
|
|
31
66
|
recipe = _get_recipe(tasks_file)
|
|
@@ -33,14 +68,27 @@ def _list_tasks(tasks_file: Optional[str] = None):
|
|
|
33
68
|
console.print("[red]No recipe file found (tasktree.yaml, tasktree.yml, tt.yaml, or *.tasks)[/red]")
|
|
34
69
|
raise typer.Exit(1)
|
|
35
70
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
71
|
+
# Calculate maximum task name length for fixed-width column
|
|
72
|
+
max_task_name_len = max(len(name) for name in recipe.task_names()) if recipe.task_names() else 0
|
|
73
|
+
|
|
74
|
+
# Create borderless table with three columns
|
|
75
|
+
table = Table(show_edge=False, show_header=False, box=None, padding=(0, 2))
|
|
76
|
+
|
|
77
|
+
# Command column: fixed width to accommodate longest task name
|
|
78
|
+
table.add_column("Command", style="bold cyan", no_wrap=True, width=max_task_name_len)
|
|
79
|
+
|
|
80
|
+
# Arguments column: allow wrapping with sensible max width
|
|
81
|
+
table.add_column("Arguments", style="white", max_width=60)
|
|
82
|
+
|
|
83
|
+
# Description column: allow wrapping with sensible max width
|
|
84
|
+
table.add_column("Description", style="white", max_width=80)
|
|
39
85
|
|
|
40
86
|
for task_name in sorted(recipe.task_names()):
|
|
41
87
|
task = recipe.get_task(task_name)
|
|
42
88
|
desc = task.desc if task else ""
|
|
43
|
-
|
|
89
|
+
args_formatted = _format_task_arguments(task.args) if task else ""
|
|
90
|
+
|
|
91
|
+
table.add_row(task_name, args_formatted, desc)
|
|
44
92
|
|
|
45
93
|
console.print(table)
|
|
46
94
|
|
|
@@ -351,9 +399,9 @@ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = Fal
|
|
|
351
399
|
state.load()
|
|
352
400
|
executor = Executor(recipe, state)
|
|
353
401
|
|
|
354
|
-
# Prune state before execution (compute hashes with effective environment)
|
|
402
|
+
# Prune state before execution (compute hashes with effective environment and dependencies)
|
|
355
403
|
valid_hashes = {
|
|
356
|
-
hash_task(t.cmd, t.outputs, t.working_dir, t.args, executor._get_effective_env_name(t))
|
|
404
|
+
hash_task(t.cmd, t.outputs, t.working_dir, t.args, executor._get_effective_env_name(t), t.deps)
|
|
357
405
|
for t in recipe.tasks.values()
|
|
358
406
|
}
|
|
359
407
|
state.prune(valid_hashes)
|
|
@@ -87,16 +87,24 @@ class DockerManager:
|
|
|
87
87
|
|
|
88
88
|
# Build the image
|
|
89
89
|
try:
|
|
90
|
+
docker_build_cmd = [
|
|
91
|
+
"docker",
|
|
92
|
+
"build",
|
|
93
|
+
"-t",
|
|
94
|
+
image_tag,
|
|
95
|
+
"-f",
|
|
96
|
+
str(dockerfile_path),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
# Add build args if environment has them (docker environments use dict for args)
|
|
100
|
+
if isinstance(env.args, dict):
|
|
101
|
+
for arg_name, arg_value in env.args.items():
|
|
102
|
+
docker_build_cmd.extend(["--build-arg", f"{arg_name}={arg_value}"])
|
|
103
|
+
|
|
104
|
+
docker_build_cmd.append(str(context_path))
|
|
105
|
+
|
|
90
106
|
subprocess.run(
|
|
91
|
-
|
|
92
|
-
"docker",
|
|
93
|
-
"build",
|
|
94
|
-
"-t",
|
|
95
|
-
image_tag,
|
|
96
|
-
"-f",
|
|
97
|
-
str(dockerfile_path),
|
|
98
|
-
str(context_path),
|
|
99
|
-
],
|
|
107
|
+
docker_build_cmd,
|
|
100
108
|
check=True,
|
|
101
109
|
capture_output=False, # Show build output to user
|
|
102
110
|
)
|
|
@@ -64,6 +64,62 @@ class Executor:
|
|
|
64
64
|
self.state = state_manager
|
|
65
65
|
self.docker_manager = docker_module.DockerManager(recipe.project_root)
|
|
66
66
|
|
|
67
|
+
def _has_regular_args(self, task: Task) -> bool:
|
|
68
|
+
"""Check if a task has any regular (non-exported) arguments.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
task: Task to check
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
True if task has at least one regular (non-exported) argument, False otherwise
|
|
75
|
+
"""
|
|
76
|
+
if not task.args:
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
# Check if any arg is not exported (doesn't start with $)
|
|
80
|
+
for arg_spec in task.args:
|
|
81
|
+
# Handle both string and dict arg specs
|
|
82
|
+
if isinstance(arg_spec, str):
|
|
83
|
+
# Remove default value part if present
|
|
84
|
+
arg_name = arg_spec.split('=')[0].split(':')[0].strip()
|
|
85
|
+
if not arg_name.startswith('$'):
|
|
86
|
+
return True
|
|
87
|
+
elif isinstance(arg_spec, dict):
|
|
88
|
+
# Dict format: { argname: { ... } } or { $argname: { ... } }
|
|
89
|
+
for key in arg_spec.keys():
|
|
90
|
+
if not key.startswith('$'):
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
return False
|
|
94
|
+
|
|
95
|
+
def _filter_regular_args(self, task: Task, task_args: dict[str, Any]) -> dict[str, Any]:
|
|
96
|
+
"""Filter task_args to only include regular (non-exported) arguments.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
task: Task definition
|
|
100
|
+
task_args: Dictionary of all task arguments
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dictionary containing only regular (non-exported) arguments
|
|
104
|
+
"""
|
|
105
|
+
if not task.args or not task_args:
|
|
106
|
+
return {}
|
|
107
|
+
|
|
108
|
+
# Build set of exported arg names (without the $ prefix)
|
|
109
|
+
exported_names = set()
|
|
110
|
+
for arg_spec in task.args:
|
|
111
|
+
if isinstance(arg_spec, str):
|
|
112
|
+
arg_name = arg_spec.split('=')[0].split(':')[0].strip()
|
|
113
|
+
if arg_name.startswith('$'):
|
|
114
|
+
exported_names.add(arg_name[1:]) # Remove $ prefix
|
|
115
|
+
elif isinstance(arg_spec, dict):
|
|
116
|
+
for key in arg_spec.keys():
|
|
117
|
+
if key.startswith('$'):
|
|
118
|
+
exported_names.add(key[1:]) # Remove $ prefix
|
|
119
|
+
|
|
120
|
+
# Filter out exported args
|
|
121
|
+
return {k: v for k, v in task_args.items() if k not in exported_names}
|
|
122
|
+
|
|
67
123
|
def _collect_early_builtin_variables(self, task: Task, timestamp: datetime) -> dict[str, str]:
|
|
68
124
|
"""Collect built-in variables that don't depend on working_dir.
|
|
69
125
|
|
|
@@ -277,9 +333,9 @@ class Executor:
|
|
|
277
333
|
reason="forced",
|
|
278
334
|
)
|
|
279
335
|
|
|
280
|
-
# Compute hashes (include effective environment)
|
|
336
|
+
# Compute hashes (include effective environment and dependencies)
|
|
281
337
|
effective_env = self._get_effective_env_name(task)
|
|
282
|
-
task_hash = hash_task(task.cmd, task.outputs, task.working_dir, task.args, effective_env)
|
|
338
|
+
task_hash = hash_task(task.cmd, task.outputs, task.working_dir, task.args, effective_env, task.deps)
|
|
283
339
|
args_hash = hash_args(args_dict) if args_dict else None
|
|
284
340
|
cache_key = make_cache_key(task_hash, args_hash)
|
|
285
341
|
|
|
@@ -372,22 +428,39 @@ class Executor:
|
|
|
372
428
|
# Resolve execution order
|
|
373
429
|
if only:
|
|
374
430
|
# Only execute the target task, skip dependencies
|
|
375
|
-
execution_order = [task_name]
|
|
431
|
+
execution_order = [(task_name, args_dict)]
|
|
376
432
|
else:
|
|
377
433
|
# Execute task and all dependencies
|
|
378
|
-
execution_order = resolve_execution_order(self.recipe, task_name)
|
|
434
|
+
execution_order = resolve_execution_order(self.recipe, task_name, args_dict)
|
|
379
435
|
|
|
380
436
|
# Single phase: Check and execute incrementally
|
|
381
437
|
statuses: dict[str, TaskStatus] = {}
|
|
382
|
-
for name in execution_order:
|
|
438
|
+
for name, task_args in execution_order:
|
|
383
439
|
task = self.recipe.tasks[name]
|
|
384
440
|
|
|
385
|
-
#
|
|
386
|
-
|
|
441
|
+
# Convert None to {} for internal use (None is used to distinguish simple deps in graph)
|
|
442
|
+
args_dict_for_execution = task_args if task_args is not None else {}
|
|
387
443
|
|
|
388
444
|
# Check if task needs to run (based on CURRENT filesystem state)
|
|
389
|
-
status = self.check_task_status(task,
|
|
390
|
-
|
|
445
|
+
status = self.check_task_status(task, args_dict_for_execution, force=force)
|
|
446
|
+
|
|
447
|
+
# Use a key that includes args for status tracking
|
|
448
|
+
# Only include regular (non-exported) args in status key for parameterized dependencies
|
|
449
|
+
# For the root task (invoked from CLI), status key is always just the task name
|
|
450
|
+
# For dependencies with parameterized invocations, include the regular args
|
|
451
|
+
is_root_task = (name == task_name)
|
|
452
|
+
if not is_root_task and args_dict_for_execution and self._has_regular_args(task):
|
|
453
|
+
import json
|
|
454
|
+
# Filter to only include regular (non-exported) args
|
|
455
|
+
regular_args = self._filter_regular_args(task, args_dict_for_execution)
|
|
456
|
+
if regular_args:
|
|
457
|
+
args_str = json.dumps(regular_args, sort_keys=True, separators=(",", ":"))
|
|
458
|
+
status_key = f"{name}({args_str})"
|
|
459
|
+
else:
|
|
460
|
+
status_key = name
|
|
461
|
+
else:
|
|
462
|
+
status_key = name
|
|
463
|
+
statuses[status_key] = status
|
|
391
464
|
|
|
392
465
|
# Execute immediately if needed
|
|
393
466
|
if status.will_run:
|
|
@@ -399,7 +472,7 @@ class Executor:
|
|
|
399
472
|
file=sys.stderr,
|
|
400
473
|
)
|
|
401
474
|
|
|
402
|
-
self._run_task(task,
|
|
475
|
+
self._run_task(task, args_dict_for_execution)
|
|
403
476
|
|
|
404
477
|
return statuses
|
|
405
478
|
|
|
@@ -962,9 +1035,9 @@ class Executor:
|
|
|
962
1035
|
task: Task that was executed
|
|
963
1036
|
args_dict: Arguments used for execution
|
|
964
1037
|
"""
|
|
965
|
-
# Compute hashes (include effective environment)
|
|
1038
|
+
# Compute hashes (include effective environment and dependencies)
|
|
966
1039
|
effective_env = self._get_effective_env_name(task)
|
|
967
|
-
task_hash = hash_task(task.cmd, task.outputs, task.working_dir, task.args, effective_env)
|
|
1040
|
+
task_hash = hash_task(task.cmd, task.outputs, task.working_dir, task.args, effective_env, task.deps)
|
|
968
1041
|
args_hash = hash_args(args_dict) if args_dict else None
|
|
969
1042
|
cache_key = make_cache_key(task_hash, args_hash)
|
|
970
1043
|
|