tasktree 0.0.19__py3-none-any.whl → 0.0.20__py3-none-any.whl

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/__init__.py CHANGED
@@ -15,6 +15,7 @@ from tasktree.graph import (
15
15
  get_implicit_inputs,
16
16
  resolve_dependency_output_references,
17
17
  resolve_execution_order,
18
+ resolve_self_references,
18
19
  )
19
20
  from tasktree.hasher import hash_args, hash_task, make_cache_key
20
21
  from tasktree.parser import Recipe, Task, find_recipe_file, parse_arg_spec, parse_recipe
@@ -31,6 +32,7 @@ __all__ = [
31
32
  "get_implicit_inputs",
32
33
  "resolve_dependency_output_references",
33
34
  "resolve_execution_order",
35
+ "resolve_self_references",
34
36
  "hash_args",
35
37
  "hash_task",
36
38
  "make_cache_key",
tasktree/cli.py CHANGED
@@ -14,7 +14,7 @@ from rich.tree import Tree
14
14
 
15
15
  from tasktree import __version__
16
16
  from tasktree.executor import Executor
17
- from tasktree.graph import build_dependency_tree, resolve_execution_order, resolve_dependency_output_references
17
+ from tasktree.graph import build_dependency_tree, resolve_execution_order, resolve_dependency_output_references, resolve_self_references
18
18
  from tasktree.hasher import hash_task, hash_args
19
19
  from tasktree.parser import Recipe, find_recipe_file, parse_arg_spec, parse_recipe
20
20
  from tasktree.state import StateManager
@@ -464,9 +464,17 @@ def _execute_dynamic_task(args: list[str], force: bool = False, only: bool = Fal
464
464
  # This is important for correct state pruning after template substitution
465
465
  execution_order = resolve_execution_order(recipe, task_name, args_dict)
466
466
 
467
- # Resolve dependency output references in topological order
468
- # This substitutes {{ dep.*.outputs.* }} templates before execution
469
- resolve_dependency_output_references(recipe, execution_order)
467
+ try:
468
+ # Resolve dependency output references in topological order
469
+ # This substitutes {{ dep.*.outputs.* }} templates before execution
470
+ resolve_dependency_output_references(recipe, execution_order)
471
+
472
+ # Resolve self-references in topological order
473
+ # This substitutes {{ self.inputs.* }} and {{ self.outputs.* }} templates
474
+ resolve_self_references(recipe, execution_order)
475
+ except ValueError as e:
476
+ console.print(f"[red]Error in task template: {e}[/red]")
477
+ raise typer.Exit(1)
470
478
 
471
479
  # Prune state based on tasks that will actually execute (with their specific arguments)
472
480
  # This ensures template-substituted dependencies are handled correctly
tasktree/executor.py CHANGED
@@ -14,7 +14,7 @@ from pathlib import Path
14
14
  from typing import Any
15
15
 
16
16
  from tasktree import docker as docker_module
17
- from tasktree.graph import get_implicit_inputs, resolve_execution_order, resolve_dependency_output_references
17
+ from tasktree.graph import get_implicit_inputs, resolve_execution_order, resolve_dependency_output_references, resolve_self_references
18
18
  from tasktree.hasher import hash_args, hash_task, make_cache_key
19
19
  from tasktree.parser import Recipe, Task, Environment
20
20
  from tasktree.state import StateManager, TaskState
@@ -437,6 +437,10 @@ class Executor:
437
437
  # This substitutes {{ dep.*.outputs.* }} templates before execution
438
438
  resolve_dependency_output_references(self.recipe, execution_order)
439
439
 
440
+ # Resolve self-references in topological order
441
+ # This substitutes {{ self.inputs.* }} and {{ self.outputs.* }} templates
442
+ resolve_self_references(self.recipe, execution_order)
443
+
440
444
  # Single phase: Check and execute incrementally
441
445
  statuses: dict[str, TaskStatus] = {}
442
446
  for name, task_args in execution_order:
@@ -861,7 +865,15 @@ class Executor:
861
865
  Returns:
862
866
  List of input glob patterns
863
867
  """
864
- all_inputs = list(task.inputs)
868
+ # Extract paths from inputs (handle both anonymous strings and named dicts)
869
+ all_inputs = []
870
+ for inp in task.inputs:
871
+ if isinstance(inp, str):
872
+ all_inputs.append(inp)
873
+ elif isinstance(inp, dict):
874
+ # Named input - extract the path value(s)
875
+ all_inputs.extend(inp.values())
876
+
865
877
  implicit_inputs = get_implicit_inputs(self.recipe, task)
866
878
  all_inputs.extend(implicit_inputs)
867
879
  return all_inputs
tasktree/graph.py CHANGED
@@ -414,6 +414,67 @@ def resolve_dependency_output_references(
414
414
  resolved_tasks[task_name] = task
415
415
 
416
416
 
417
+ def resolve_self_references(
418
+ recipe: Recipe,
419
+ ordered_tasks: list[tuple[str, dict[str, Any]]],
420
+ ) -> None:
421
+ """Resolve {{ self.inputs.name }} and {{ self.outputs.name }} references.
422
+
423
+ This function walks through tasks and resolves self-references to task's own
424
+ inputs/outputs. Must be called AFTER resolve_dependency_output_references()
425
+ so that dependency outputs are already resolved in output paths.
426
+
427
+ Args:
428
+ recipe: Recipe containing task definitions
429
+ ordered_tasks: List of (task_name, args) tuples in topological order
430
+
431
+ Raises:
432
+ ValueError: If self-reference cannot be resolved (missing name, etc.)
433
+
434
+ Example:
435
+ If task.cmd contains "{{ self.inputs.src }}" and task has input {src: "*.txt"},
436
+ it will be resolved to "*.txt" (literal string, no glob expansion).
437
+ """
438
+ from tasktree.substitution import substitute_self_references
439
+
440
+ for task_name, task_args in ordered_tasks:
441
+ task = recipe.tasks.get(task_name)
442
+ if task is None:
443
+ continue
444
+
445
+ # Resolve self-references in command
446
+ if task.cmd:
447
+ task.cmd = substitute_self_references(
448
+ task.cmd,
449
+ task_name,
450
+ task._input_map,
451
+ task._output_map,
452
+ )
453
+
454
+ # Resolve self-references in working_dir
455
+ if task.working_dir:
456
+ task.working_dir = substitute_self_references(
457
+ task.working_dir,
458
+ task_name,
459
+ task._input_map,
460
+ task._output_map,
461
+ )
462
+
463
+ # Resolve self-references in argument defaults
464
+ if task.args:
465
+ for arg_spec in task.args:
466
+ if isinstance(arg_spec, dict):
467
+ for arg_name, arg_details in arg_spec.items():
468
+ if isinstance(arg_details, dict) and "default" in arg_details:
469
+ if isinstance(arg_details["default"], str):
470
+ arg_details["default"] = substitute_self_references(
471
+ arg_details["default"],
472
+ task_name,
473
+ task._input_map,
474
+ task._output_map,
475
+ )
476
+
477
+
417
478
  def get_implicit_inputs(recipe: Recipe, task: Task) -> list[str]:
418
479
  """Get implicit inputs for a task based on its dependencies.
419
480
 
tasktree/parser.py CHANGED
@@ -57,7 +57,7 @@ class Task:
57
57
  cmd: str
58
58
  desc: str = ""
59
59
  deps: list[str | dict[str, Any]] = field(default_factory=list) # Can be strings or dicts with args
60
- inputs: list[str] = field(default_factory=list)
60
+ inputs: list[str | dict[str, str]] = field(default_factory=list) # Can be strings or dicts with named inputs
61
61
  outputs: list[str | dict[str, str]] = field(default_factory=list) # Can be strings or dicts with named outputs
62
62
  working_dir: str = ""
63
63
  args: list[str | dict[str, Any]] = field(default_factory=list) # Can be strings or dicts (each dict has single key: arg name)
@@ -69,6 +69,10 @@ class Task:
69
69
  _output_map: dict[str, str] = field(init=False, default_factory=dict, repr=False) # name → path mapping
70
70
  _anonymous_outputs: list[str] = field(init=False, default_factory=list, repr=False) # unnamed outputs
71
71
 
72
+ # Internal fields for efficient input lookup (built in __post_init__)
73
+ _input_map: dict[str, str] = field(init=False, default_factory=dict, repr=False) # name → path mapping
74
+ _anonymous_inputs: list[str] = field(init=False, default_factory=list, repr=False) # unnamed inputs
75
+
72
76
  def __post_init__(self):
73
77
  """Ensure lists are always lists and build output maps."""
74
78
  if isinstance(self.deps, str):
@@ -132,6 +136,45 @@ class Task:
132
136
  f"Task '{self.name}': Output at index {idx} must be a string or dict, got {type(output).__name__}: {output}"
133
137
  )
134
138
 
139
+ # Build input maps for efficient lookup
140
+ self._input_map = {}
141
+ self._anonymous_inputs = []
142
+
143
+ for idx, input_item in enumerate(self.inputs):
144
+ if isinstance(input_item, dict):
145
+ # Named input: validate and store
146
+ if len(input_item) != 1:
147
+ raise ValueError(
148
+ f"Task '{self.name}': Named input at index {idx} must have exactly one key-value pair, got {len(input_item)}: {input_item}"
149
+ )
150
+
151
+ name, path = next(iter(input_item.items()))
152
+
153
+ if not isinstance(path, str):
154
+ raise ValueError(
155
+ f"Task '{self.name}': Named input '{name}' must have a string path, got {type(path).__name__}: {path}"
156
+ )
157
+
158
+ if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', name):
159
+ raise ValueError(
160
+ f"Task '{self.name}': Named input '{name}' must be a valid identifier "
161
+ f"(letters, numbers, underscores, cannot start with number)"
162
+ )
163
+
164
+ if name in self._input_map:
165
+ raise ValueError(
166
+ f"Task '{self.name}': Duplicate input name '{name}' at index {idx}"
167
+ )
168
+
169
+ self._input_map[name] = path
170
+ elif isinstance(input_item, str):
171
+ # Anonymous input: just store
172
+ self._anonymous_inputs.append(input_item)
173
+ else:
174
+ raise ValueError(
175
+ f"Task '{self.name}': Input at index {idx} must be a string or dict, got {type(input_item).__name__}: {input_item}"
176
+ )
177
+
135
178
 
136
179
  @dataclass
137
180
  class DependencySpec:
@@ -309,7 +352,21 @@ class Recipe:
309
352
  task.cmd = substitute_variables(task.cmd, self.evaluated_variables)
310
353
  task.desc = substitute_variables(task.desc, self.evaluated_variables)
311
354
  task.working_dir = substitute_variables(task.working_dir, self.evaluated_variables)
312
- task.inputs = [substitute_variables(inp, self.evaluated_variables) for inp in task.inputs]
355
+
356
+ # Substitute variables in inputs (handle both string and dict inputs)
357
+ resolved_inputs = []
358
+ for inp in task.inputs:
359
+ if isinstance(inp, str):
360
+ resolved_inputs.append(substitute_variables(inp, self.evaluated_variables))
361
+ elif isinstance(inp, dict):
362
+ # Named input: substitute the path value
363
+ resolved_dict = {}
364
+ for name, path in inp.items():
365
+ resolved_dict[name] = substitute_variables(path, self.evaluated_variables)
366
+ resolved_inputs.append(resolved_dict)
367
+ else:
368
+ resolved_inputs.append(inp)
369
+ task.inputs = resolved_inputs
313
370
 
314
371
  # Substitute variables in outputs (handle both string and dict outputs)
315
372
  resolved_outputs = []
@@ -1473,8 +1530,15 @@ def collect_reachable_variables(
1473
1530
  # Search in inputs
1474
1531
  if task.inputs:
1475
1532
  for input_pattern in task.inputs:
1476
- for match in var_pattern.finditer(input_pattern):
1477
- variables.add(match.group(1))
1533
+ if isinstance(input_pattern, str):
1534
+ for match in var_pattern.finditer(input_pattern):
1535
+ variables.add(match.group(1))
1536
+ elif isinstance(input_pattern, dict):
1537
+ # Named input - check the path value
1538
+ for input_path in input_pattern.values():
1539
+ if isinstance(input_path, str):
1540
+ for match in var_pattern.finditer(input_path):
1541
+ variables.add(match.group(1))
1478
1542
 
1479
1543
  # Search in outputs
1480
1544
  if task.outputs:
tasktree/substitution.py CHANGED
@@ -12,13 +12,19 @@ from typing import Any
12
12
  # Pattern matches: {{ prefix.name }} with optional whitespace
13
13
  # Groups: (1) prefix (var|arg|env|tt), (2) name (identifier)
14
14
  PLACEHOLDER_PATTERN = re.compile(
15
- r'\{\{\s*(var|arg|env|tt)\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
15
+ r'\{\{\s*(var|arg|env|tt)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
16
16
  )
17
17
 
18
18
  # Pattern matches: {{ dep.task_name.outputs.output_name }} with optional whitespace
19
19
  # Groups: (1) task_name (can include dots for namespacing), (2) output_name (identifier)
20
20
  DEP_OUTPUT_PATTERN = re.compile(
21
- r'\{\{\s*dep\s*\.\s*([a-zA-Z_][a-zA-Z0-9_.-]*)\s*\.\s*outputs\s*\.\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
21
+ r'\{\{\s*dep\.([a-zA-Z_][a-zA-Z0-9_.-]*)\.outputs\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
22
+ )
23
+
24
+ # Pattern matches: {{ self.(inputs|outputs).name }} with optional whitespace
25
+ # Groups: (1) field (inputs|outputs), (2) name (identifier)
26
+ SELF_REFERENCE_PATTERN = re.compile(
27
+ r'\{\{\s*self\.(inputs|outputs)\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}'
22
28
  )
23
29
 
24
30
 
@@ -372,3 +378,70 @@ def substitute_dependency_outputs(
372
378
  return dep_task._output_map[output_name]
373
379
 
374
380
  return DEP_OUTPUT_PATTERN.sub(replacer, text)
381
+
382
+
383
+ def substitute_self_references(
384
+ text: str,
385
+ task_name: str,
386
+ input_map: dict[str, str],
387
+ output_map: dict[str, str],
388
+ ) -> str:
389
+ """Substitute {{ self.inputs.name }} and {{ self.outputs.name }} placeholders.
390
+
391
+ This function resolves references to the task's own named inputs and outputs.
392
+ Only named entries are accessible; anonymous inputs/outputs cannot be referenced.
393
+ The substitution is literal string replacement - no glob expansion or path resolution.
394
+
395
+ Args:
396
+ text: Text containing {{ self.* }} placeholders
397
+ task_name: Name of current task (for error messages)
398
+ input_map: Dictionary mapping input names to path strings
399
+ output_map: Dictionary mapping output names to path strings
400
+
401
+ Returns:
402
+ Text with all {{ self.* }} placeholders replaced with literal path strings
403
+
404
+ Raises:
405
+ ValueError: If referenced name doesn't exist in input_map or output_map
406
+
407
+ Example:
408
+ >>> input_map = {"src": "*.txt"}
409
+ >>> output_map = {"dest": "out/result.txt"}
410
+ >>> substitute_self_references(
411
+ ... "cp {{ self.inputs.src }} {{ self.outputs.dest }}",
412
+ ... "copy",
413
+ ... input_map,
414
+ ... output_map
415
+ ... )
416
+ 'cp *.txt out/result.txt'
417
+ """
418
+ def replacer(match: re.Match) -> str:
419
+ field = match.group(1) # "inputs" or "outputs"
420
+ name = match.group(2)
421
+
422
+ # Select appropriate map
423
+ if field == "inputs":
424
+ name_map = input_map
425
+ field_display = "input"
426
+ else: # field == "outputs"
427
+ name_map = output_map
428
+ field_display = "output"
429
+
430
+ # Check if name exists in map
431
+ if name not in name_map:
432
+ available = list(name_map.keys())
433
+ if available:
434
+ available_msg = ", ".join(available)
435
+ else:
436
+ available_msg = f"(none - all {field} are anonymous)"
437
+
438
+ raise ValueError(
439
+ f"Task '{task_name}' references {field_display} '{name}' "
440
+ f"but has no {field_display} named '{name}'.\n"
441
+ f"Available named {field}: {available_msg}\n"
442
+ f"Hint: Define named {field} like: {field}: [{{ {name}: 'path/to/file' }}]"
443
+ )
444
+
445
+ return name_map[name]
446
+
447
+ return SELF_REFERENCE_PATTERN.sub(replacer, text)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tasktree
3
- Version: 0.0.19
3
+ Version: 0.0.20
4
4
  Summary: A task automation tool with incremental execution
5
5
  Requires-Python: >=3.11
6
6
  Requires-Dist: click>=8.1.0
@@ -833,6 +833,241 @@ Hint: Define named outputs like: outputs: [{ missing: 'path/to/file' }]
833
833
  - **Deployment pipelines**: Reference exact artifacts to deploy
834
834
  - **Configuration propagation**: Pass generated config files through build stages
835
835
 
836
+ ### Self-References
837
+
838
+ Tasks can reference their own named inputs and outputs using `{{ self.inputs.name }}` and `{{ self.outputs.name }}` templates. This eliminates repetition when paths contain variables or when tasks have multiple inputs/outputs.
839
+
840
+ **Named Inputs and Outputs:**
841
+
842
+ Just like dependency output references, inputs and outputs can have names:
843
+
844
+ ```yaml
845
+ tasks:
846
+ process:
847
+ inputs:
848
+ - src: "data/input.json" # Named input
849
+ - config: "config.yaml" # Named input
850
+ outputs:
851
+ - result: "output/result.json" # Named output
852
+ - log: "output/process.log" # Named output
853
+ cmd: |
854
+ process-tool \
855
+ --input {{ self.inputs.src }} \
856
+ --config {{ self.inputs.config }} \
857
+ --output {{ self.outputs.result }} \
858
+ --log {{ self.outputs.log }}
859
+ ```
860
+
861
+ **Syntax:**
862
+
863
+ - **Defining named inputs**: `inputs: [{ name: "path/to/file" }]`
864
+ - **Defining named outputs**: `outputs: [{ name: "path/to/file" }]`
865
+ - **Referencing inputs**: `{{ self.inputs.input_name }}`
866
+ - **Referencing outputs**: `{{ self.outputs.output_name }}`
867
+ - **Mixed format**: Can combine named and anonymous inputs/outputs in the same task
868
+
869
+ **Why Use Self-References?**
870
+
871
+ Self-references follow the DRY (Don't Repeat Yourself) principle:
872
+
873
+ ```yaml
874
+ # Without self-references - repetitive
875
+ tasks:
876
+ build:
877
+ inputs: [src/app-{{ var.version }}.c]
878
+ outputs: [build/app-{{ var.version }}.o]
879
+ cmd: gcc src/app-{{ var.version }}.c -o build/app-{{ var.version }}.o
880
+
881
+ # With self-references - DRY
882
+ tasks:
883
+ build:
884
+ inputs:
885
+ - source: src/app-{{ var.version }}.c
886
+ outputs:
887
+ - object: build/app-{{ var.version }}.o
888
+ cmd: gcc {{ self.inputs.source }} -o {{ self.outputs.object }}
889
+ ```
890
+
891
+ **Working with Variables:**
892
+
893
+ Self-references work seamlessly with variables:
894
+
895
+ ```yaml
896
+ variables:
897
+ platform: linux
898
+ arch: x86_64
899
+
900
+ tasks:
901
+ compile:
902
+ inputs:
903
+ - src: src/{{ var.platform }}/main.c
904
+ - header: include/{{ var.arch }}/defs.h
905
+ outputs:
906
+ - binary: build/{{ var.platform }}-{{ var.arch }}/app
907
+ cmd: |
908
+ gcc {{ self.inputs.src }} \
909
+ -include {{ self.inputs.header }} \
910
+ -o {{ self.outputs.binary }}
911
+ ```
912
+
913
+ Variables are evaluated first, then self-references substitute the expanded paths.
914
+
915
+ **Working with Arguments:**
916
+
917
+ Self-references work with parameterized tasks:
918
+
919
+ ```yaml
920
+ tasks:
921
+ deploy:
922
+ args: [environment]
923
+ inputs:
924
+ - config: configs/{{ arg.environment }}/app.yaml
925
+ outputs:
926
+ - deployed: deployed-{{ arg.environment }}.yaml
927
+ cmd: |
928
+ validate {{ self.inputs.config }}
929
+ deploy {{ self.inputs.config }} > {{ self.outputs.deployed }}
930
+ ```
931
+
932
+ ```bash
933
+ tt deploy production # Uses configs/production/app.yaml
934
+ tt deploy staging # Uses configs/staging/app.yaml
935
+ ```
936
+
937
+ **Working with Dependency Outputs:**
938
+
939
+ Self-references and dependency references can be used together:
940
+
941
+ ```yaml
942
+ tasks:
943
+ build:
944
+ outputs:
945
+ - artifact: dist/app.js
946
+ cmd: webpack build
947
+
948
+ package:
949
+ deps: [build]
950
+ inputs:
951
+ - manifest: package.json
952
+ outputs:
953
+ - tarball: release.tar.gz
954
+ cmd: tar czf {{ self.outputs.tarball }} \
955
+ {{ self.inputs.manifest }} \
956
+ {{ dep.build.outputs.artifact }}
957
+ ```
958
+
959
+ **Mixed Named and Anonymous:**
960
+
961
+ Tasks can mix named and anonymous inputs/outputs:
962
+
963
+ ```yaml
964
+ tasks:
965
+ build:
966
+ inputs:
967
+ - config: build.yaml # Named - can reference
968
+ - src/**/*.c # Anonymous - tracked but not referenceable
969
+ outputs:
970
+ - binary: bin/app # Named - can reference
971
+ - bin/app.debug # Anonymous - tracked but not referenceable
972
+ cmd: build-tool --config {{ self.inputs.config }} --output {{ self.outputs.binary }}
973
+ ```
974
+
975
+ **Error Messages:**
976
+
977
+ If you reference a non-existent input or output:
978
+
979
+ ```yaml
980
+ tasks:
981
+ build:
982
+ inputs:
983
+ - src: input.txt
984
+ cmd: cat {{ self.inputs.missing }} # Error!
985
+ ```
986
+
987
+ You'll get a clear error before execution:
988
+
989
+ ```
990
+ Task 'build' references input 'missing' but has no input named 'missing'.
991
+ Available named inputs: src
992
+ Hint: Define named inputs like: inputs: [{ missing: 'path/to/file' }]
993
+ ```
994
+
995
+ Similarly, if you try to reference an anonymous input:
996
+
997
+ ```yaml
998
+ tasks:
999
+ build:
1000
+ inputs: [file.txt] # Anonymous input
1001
+ cmd: cat {{ self.inputs.src }} # Error!
1002
+ ```
1003
+
1004
+ You'll get:
1005
+
1006
+ ```
1007
+ Task 'build' references input 'src' but has no input named 'src'.
1008
+ Available named inputs: (none - all inputs are anonymous)
1009
+ Hint: Define named inputs like: inputs: [{ src: 'file.txt' }]
1010
+ ```
1011
+
1012
+ **Key Behaviors:**
1013
+
1014
+ - **Template resolution**: Self-references are resolved during dependency graph planning
1015
+ - **Substitution order**: Variables → Dependency outputs → Self-references → Arguments/Environment
1016
+ - **Fail-fast validation**: Errors are caught before execution begins
1017
+ - **Clear error messages**: Lists available names if reference doesn't exist
1018
+ - **Backward compatible**: Existing anonymous inputs/outputs work unchanged
1019
+ - **State tracking**: Works correctly with incremental execution and freshness checks
1020
+
1021
+ **Limitations:**
1022
+
1023
+ - **Anonymous not referenceable**: Only named inputs/outputs can be referenced
1024
+ - **Case sensitive**: `{{ self.inputs.Src }}` and `{{ self.inputs.src }}` are different
1025
+ - **Argument defaults**: Self-references in argument defaults are not supported (arguments are evaluated before self-references)
1026
+
1027
+ **Use Cases:**
1028
+
1029
+ - **Eliminate repetition**: Define complex paths once, use them multiple times
1030
+ - **Variable composition**: Combine variables with self-references for clean commands
1031
+ - **Multiple inputs/outputs**: Reference specific files when tasks have many
1032
+ - **Complex build pipelines**: Keep commands readable with named artifacts
1033
+ - **Glob patterns**: Use self-references with glob patterns for dynamic inputs
1034
+
1035
+ **Example: Multi-Stage Build:**
1036
+
1037
+ ```yaml
1038
+ variables:
1039
+ version: "2.1.0"
1040
+ platform: "linux"
1041
+
1042
+ tasks:
1043
+ prepare:
1044
+ outputs:
1045
+ - builddir: build/{{ var.platform }}-{{ var.version }}
1046
+ cmd: mkdir -p {{ self.outputs.builddir }}
1047
+
1048
+ compile:
1049
+ deps: [prepare]
1050
+ inputs:
1051
+ - source: src/main.c
1052
+ - headers: include/*.h
1053
+ outputs:
1054
+ - object: build/{{ var.platform }}-{{ var.version }}/main.o
1055
+ cmd: |
1056
+ gcc -c {{ self.inputs.source }} \
1057
+ -I include \
1058
+ -o {{ self.outputs.object }}
1059
+
1060
+ link:
1061
+ deps: [compile]
1062
+ outputs:
1063
+ - executable: build/{{ var.platform }}-{{ var.version }}/app
1064
+ - symbols: build/{{ var.platform }}-{{ var.version }}/app.sym
1065
+ cmd: |
1066
+ gcc build/{{ var.platform }}-{{ var.version }}/main.o \
1067
+ -o {{ self.outputs.executable }}
1068
+ objcopy --only-keep-debug {{ self.outputs.executable }} {{ self.outputs.symbols }}
1069
+ ```
1070
+
836
1071
 
837
1072
  ### Private Tasks
838
1073
 
@@ -0,0 +1,14 @@
1
+ tasktree/__init__.py,sha256=m7fLsPUft99oB_XXr4dOu2yUWu74zVutkw1-3zGrG5Y,1227
2
+ tasktree/cli.py,sha256=S5ypqQlvCxdAvlBfO8TJZhvMoc086wqgvmOm8220678,21220
3
+ tasktree/docker.py,sha256=qvja8G63uAcC73YMVY739egda1_CcBtoqzm0qIJU_Q8,14443
4
+ tasktree/executor.py,sha256=g4mHtoO3wVIxyqNALIdJOEwlEqRkSY0eY-6sl2jF-IA,46463
5
+ tasktree/graph.py,sha256=T78JH0whP7VquEvtOVN-8ePyHNcseTQoEouijDrgmkw,22663
6
+ tasktree/hasher.py,sha256=o7Akd_AgGkAsnv9biK0AcbhlcqUQ9ne5y_6r4zoFaw0,5493
7
+ tasktree/parser.py,sha256=R0swEkKBPGeijLHxD1CbQjtoKVn2gRJadsZuyKj1sdM,97922
8
+ tasktree/state.py,sha256=Cktl4D8iDZVd55aO2LqVyPrc-BnljkesxxkcMcdcfOY,3541
9
+ tasktree/substitution.py,sha256=FhtFI0ciK9bQxPdORvpf1coa59XxizKiBiUwHJp0PtI,16811
10
+ tasktree/types.py,sha256=R_YAyO5bMLB6XZnkMRT7VAtlkA_Xx6xu0aIpzQjrBXs,4357
11
+ tasktree-0.0.20.dist-info/METADATA,sha256=C8cdZtyz-dgiVoN1H2hy7wQNAA-K784WYFfqlZEOXnA,50237
12
+ tasktree-0.0.20.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
+ tasktree-0.0.20.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
14
+ tasktree-0.0.20.dist-info/RECORD,,
@@ -1,14 +0,0 @@
1
- tasktree/__init__.py,sha256=N-dZcggJDe4WaloC1MdEh0oMTHlUpBN9AEcAm6leDf4,1167
2
- tasktree/cli.py,sha256=_GzqQIk2z2Hz17bkTOWhtNW4X4_Ijxn-uEL-WZGt8LM,20858
3
- tasktree/docker.py,sha256=qvja8G63uAcC73YMVY739egda1_CcBtoqzm0qIJU_Q8,14443
4
- tasktree/executor.py,sha256=7pzcH2wLWMZPk3hwhzWgz18RVkIPFCKdu23MboWUQs4,45914
5
- tasktree/graph.py,sha256=9O5LByzMYa8ccedznqKBTb0Xe9N_aajSR1cAcb8zGQE,20366
6
- tasktree/hasher.py,sha256=o7Akd_AgGkAsnv9biK0AcbhlcqUQ9ne5y_6r4zoFaw0,5493
7
- tasktree/parser.py,sha256=PVgtGORCpnkb8wcXHFfsyVqDhJ3PwzwCqO3VWiuLQl4,94777
8
- tasktree/state.py,sha256=Cktl4D8iDZVd55aO2LqVyPrc-BnljkesxxkcMcdcfOY,3541
9
- tasktree/substitution.py,sha256=3-gdvHbBwPkQPflx3GVSpEEa0vTL_ivdcMIba77gtJc,14225
10
- tasktree/types.py,sha256=R_YAyO5bMLB6XZnkMRT7VAtlkA_Xx6xu0aIpzQjrBXs,4357
11
- tasktree-0.0.19.dist-info/METADATA,sha256=KTLp0p1Mkzzvah-Hy15KrXnTcKju5dlQyvDvEdnDOmg,43609
12
- tasktree-0.0.19.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
- tasktree-0.0.19.dist-info/entry_points.txt,sha256=lQINlvRYnimvteBbnhH84A9clTg8NnpEjCWqWkqg8KE,40
14
- tasktree-0.0.19.dist-info/RECORD,,