From d5d4142d58f64c8fa806a16d682174cdc365f473 Mon Sep 17 00:00:00 2001
From: Thomas Walker Lynch
Date: Tue, 13 Oct 2026 04:06:00 +0000
Subject: [PATCH] promote of made only
---
Harmony_path-and-staging_patch | 234 ++++++++++++
README.md | 18 +-
administrator/tool/new-project | 70 +++-
.../document/naming_file-and-directory.html | 2 -
developer/tool/build_component/copy | 8 +-
developer/tool/promote | 60 ++-
document/introduction_Harmony.html | 358 ++++++------------
...role-and-workflow_product-development.html | 12 +-
setup | 7 +-
shared/tool/setup | 27 +-
10 files changed, 480 insertions(+), 316 deletions(-)
create mode 100755 Harmony_path-and-staging_patch
diff --git a/Harmony_path-and-staging_patch b/Harmony_path-and-staging_patch
new file mode 100755
index 0000000..52cb66a
--- /dev/null
+++ b/Harmony_path-and-staging_patch
@@ -0,0 +1,234 @@
+#!/usr/bin/env python3
+# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
+
+"""
+Harmony_path-and-staging_patch
+
+One-time patch for the Harmony skeleton. Run it from the top directory of
+the project, after checkpointing with git.
+
+ 1. shared/tool/setup
+ Sources the project dependency .init files first, then adds shared/tool,
+ shared/made, shared/authored, so the search order becomes
+ shared/authored, shared/made, shared/tool, project dependencies.
+ 2. setup
+ Adds the role's tool directory after the shared setup has run, so it is
+ searched before everything else. Removes the earlier PATH injection,
+ which put it behind the shared directories.
+ 3. developer/tool/build_component/copy
+ Stages into scratchpad/made/, like the make component.
+ 4. developer/tool/promote
+ Copies from scratchpad/made into consumer/, so build intermediates
+ such as scratchpad/build/object stay on the scratchpad. Its setup hint
+ now says ./setup developer.
+
+Every expected piece of text is checked before any file is written. If one
+is missing, the patch reports it and changes nothing.
+
+Usage: Harmony_path-and-staging_patch
+ help | -h | --help print this message (also printed when given no arguments)
+ preview print what would change, change nothing
+ apply make the changes
+"""
+
+import sys ,re
+from pathlib import Path
+
+EDITS = {
+ "shared/tool/setup": [
+ (
+'''# --------------------------------------------------------------------------------
+# PATH & Plugin Sourcing
+# precedence: last defined, first discovered
+
+ PATH="$REPO_HOME/shared/authored:$PATH"
+ PATH="$REPO_HOME/shared/made:$PATH"
+ PATH="$REPO_HOME/shared/tool:$PATH"
+
+ # Remove duplicates
+ clean_path() {
+ PATH=$(echo ":$PATH" | awk -v RS=: -v ORS=: '!seen[$0]++' | sed 's/^://; s/:$//')
+ }
+ clean_path
+
+ # Plugin Sourcing: Dynamically load all the linked project configurations
+ for init_file in "$REPO_HOME/shared/project-dependency/"*.init; do
+ if [[ -f "$init_file" ]]; then
+ source "$init_file"
+ fi
+ done
+'''
+ ,
+'''# --------------------------------------------------------------------------------
+# PATH & Plugin Sourcing
+# Each addition goes to the front, so the last added is searched first.
+# Resulting search order, most specific first:
+# shared/authored, shared/made, shared/tool, project dependencies, the rest.
+# The top-level setup then puts the role's tool directory in front of these.
+
+ # Project dependencies: source each .init file. Each prepends its own entries.
+ for init_file in "$REPO_HOME/shared/project-dependency/"*.init; do
+ if [[ -f "$init_file" ]]; then
+ source "$init_file"
+ fi
+ done
+
+ PATH="$REPO_HOME/shared/tool:$PATH"
+ PATH="$REPO_HOME/shared/made:$PATH"
+ PATH="$REPO_HOME/shared/authored:$PATH"
+
+ # Remove duplicates, keeping the first occurrence
+ clean_path() {
+ PATH=$(echo ":$PATH" | awk -v RS=: -v ORS=: '!seen[$0]++' | sed 's/^://; s/:$//')
+ }
+ clean_path
+'''
+ )
+ ]
+ ,"setup": [
+ (
+''' # Python-domain logic: PATH injection
+ path_parts = env.get("PATH", "").split(os.pathsep)
+ if tool_dir not in path_parts:
+ env["PATH"] = f"{tool_dir}{os.pathsep}{env.get('PATH', '')}"
+
+'''
+ ,
+''
+ )
+ ,(
+""" 'if [[ -f "shared/authored/setup" ]]; then source "shared/authored/setup"; fi',
+"""
+ ,
+""" 'if [[ -f "shared/authored/setup" ]]; then source "shared/authored/setup"; fi',
+ '# the role tool directory is searched before everything the shared setup added',
+ f'PATH="{tool_dir}:$PATH"; export PATH',
+"""
+ )
+ ]
+ ,"developer/tool/build_component/copy": [
+ ('all Stages files from authored to scratchpad.'
+ ,'all Stages files from authored to scratchpad/made/.')
+ ,('clean Removes the specified namespace from scratchpad.'
+ ,'clean Removes scratchpad/made/.')
+ ,(''' src_root = os.path.join("authored", namespace_dir)
+ dst_root = os.path.join("scratchpad", namespace)'''
+ ,''' src_root = os.path.join("authored", namespace_dir)
+ dst_root = os.path.join("scratchpad", "made", namespace)''')
+ ,(''' dst_root = os.path.join("scratchpad", namespace)
+ if os.path.exists(dst_root):'''
+ ,''' dst_root = os.path.join("scratchpad", "made", namespace)
+ if os.path.exists(dst_root):''')
+ ]
+ ,"developer/tool/promote": [
+ ('write Writes promoted files from scratchpad into consumer. Only updates newer files.'
+ ,'write Copies what is staged in scratchpad/made into consumer. Only updates newer files.')
+ ,('diff List missing, orphaned, or out of sync files between scratchpad and consumer.'
+ ,'diff List missing, orphaned, or out of sync files between scratchpad/made and consumer.')
+ ,('"Enter the project with: . setup developer\\n"'
+ ,'"Enter the developer role with: ./setup developer\\n"')
+ ,(''' ensure_dir(dst_root, DEFAULT_DIR_MODE, dry=dry)
+ src_root = dpath("scratchpad")'''
+ ,''' ensure_dir(dst_root, DEFAULT_DIR_MODE, dry=dry)
+ src_root = dpath("scratchpad", "made")''')
+ ,(''' dst_root = cpath()
+ src_root = dpath("scratchpad")
+
+ src_files = set()'''
+ ,''' dst_root = cpath()
+ src_root = dpath("scratchpad", "made")
+
+ src_files = set()''')
+ ,('Orphaned in consumer (missing in scratchpad)'
+ ,'Orphaned in consumer (missing in scratchpad/made)')
+ ,('newer in scratchpad)'
+ ,'newer in scratchpad/made)')
+ ,('Consumer matches developer scratchpad.'
+ ,'Consumer matches developer scratchpad/made.')
+ ]
+}
+
+def print_help() -> int:
+ print(__doc__.strip())
+ return 0
+
+def tolerant(old: str):
+ """A pattern for old that ignores spaces and tabs at the ends of lines."""
+ lines = old.split("\n")
+ last = len(lines) - 1
+ return re.compile("\n".join(
+ re.escape(l.rstrip(" \t")) + ("" if i == last and l == "" else "[ \t]*")
+ for i ,l in enumerate(lines)
+ ))
+
+def plan(root: Path):
+ """Returns (new_texts ,already ,problems). new_texts maps a path to its new
+ text; already lists the files that are already patched."""
+ new_texts = {}
+ already = []
+ problems = []
+ for rel ,edits in EDITS.items():
+ f = root / rel
+ if not f.is_file():
+ problems.append(f"{rel}: file not found")
+ continue
+ text = f.read_text(encoding="utf-8")
+ applied = 0
+ for n ,(old ,new) in enumerate(edits ,1):
+ # The finished form is checked first: an edit's old text can lie
+ # inside its new text, and would otherwise be applied twice.
+ if new != "" and len(tolerant(new).findall(text)) == 1:
+ applied += 1
+ continue
+ count = len(tolerant(old).findall(text))
+ if count == 1:
+ text = tolerant(old).sub(lambda m: new ,text)
+ continue
+ if new == "" and count == 0:
+ applied += 1
+ continue
+ problems.append(f"{rel}: edit {n}: expected text found {count} times, not once")
+ if applied == len(edits):
+ already.append(rel)
+ elif applied:
+ problems.append(f"{rel}: partly patched ({applied} of {len(edits)} edits already present)")
+ else:
+ new_texts[rel] = text
+ return new_texts ,already ,problems
+
+def work(root: Path ,preview: bool) -> int:
+ print(f"Harmony_path-and-staging_patch {'preview' if preview else 'apply'} in {root}")
+ new_texts ,already ,problems = plan(root)
+ if problems:
+ for p in problems: print(f" error {p}")
+ print("Nothing was changed.")
+ return 1
+ for rel in already:
+ print(f" already patched {rel}")
+ for rel ,text in new_texts.items():
+ f = root / rel
+ if preview:
+ print(f" would change {rel} ({len(EDITS[rel])} edits)")
+ else:
+ mode = f.stat().st_mode
+ f.write_text(text ,encoding="utf-8")
+ f.chmod(mode)
+ print(f" changed {rel} ({len(EDITS[rel])} edits)")
+ if preview: print("preview only: nothing was changed")
+ return 0
+
+def CLI(argv=None) -> int:
+ if argv is None: argv = sys.argv[1:]
+ if not argv or argv[0] in ("help" ,"-h" ,"--help"): return print_help()
+ if len(argv) != 1 or argv[0] not in ("preview" ,"apply"):
+ print(f"Error: unknown command '{' '.join(argv)}'.\n" ,file=sys.stderr)
+ print_help()
+ return 1
+ root = Path.cwd()
+ if not (root / "shared" / "tool" / "setup").is_file():
+ print("Error: run this from the top directory of the project." ,file=sys.stderr)
+ return 1
+ return work(root ,argv[0] == "preview")
+
+if __name__ == "__main__":
+ sys.exit(CLI())
diff --git a/README.md b/README.md
index dd6f2c0..d0fa797 100644
--- a/README.md
+++ b/README.md
@@ -19,20 +19,8 @@ To create a Harmony-based project, the project administrator performs these step
## Viewing the Documentation
-To view the project documentation with its intended formatting, a person must provide the RT style library. One way to do this is to clone the styling repository side-by-side with your new project directory, and then to link it into the third-party directory.
+The documents are HTML written against the RT-Style library. With nothing set up, a document loads RT-Style from https://style.ReasoningTechnology.com, so it formats whenever the machine is online.
-From the parent directory of your new project, clone the required style repository:
+To read offline, or against a local copy of RT-Style, put a built RT-Style project on the machine and open its consumer/Manuscript/setup.html once in Chrome, from disk. Chrome then loads RT-Style from that copy for every local document. Opening another copy of setup.html, then reloading the document, will switch to using that other copy.
-```bash
-cd ..
-git clone -b release_v1 https://github.com/Thomas-Walker-Lynch/RT-style-JS_public
-```
-
-Then, from the root of your new project repository, link it:
-
-```bash
-cd shared/linked-project
-ln -s ../../../RT-style-JS_public RT-style-JS_public
-```
-
-Find an introductory document at `document/introduction_Harmony.html'. After the style library is installed, clicking on it in file navigator should open it in a browser.
+Find an introductory document at `document/introduction_Harmony.html`.
diff --git a/administrator/tool/new-project b/administrator/tool/new-project
index 5394bed..c949ddb 100755
--- a/administrator/tool/new-project
+++ b/administrator/tool/new-project
@@ -9,12 +9,15 @@ the Harmony skeleton architecture.
The script executes the following sequential lifecycle:
1. Validates the execution environment (requires an active role session).
- 2. Safely extracts a clean copy of all Git-tracked files using an archive stream.
- 3. Renames the boilerplate 0pus_Harmony anchor file to reflect the new project.
- 4. Injects an initial version tracking entry into the shared configuration space.
- 5. Initializes a fresh Git repository bound to the core-developer_branch.
- 6. Stages and commits the pristine skeleton infrastructure as a baseline.
- 7. Maps any upstream remote network targets passed via CLI parameters.
+ 2. Checks the git status of the skeleton. The new project is made from the
+ last commit, so uncommitted changes and untracked files would be left
+ out. When there are any, lists them and asks whether to continue.
+ 3. Safely extracts a clean copy of all Git-tracked files using an archive stream.
+ 4. Renames the boilerplate 0pus_Harmony anchor file to reflect the new project.
+ 5. Injects an initial version tracking entry into the shared configuration space.
+ 6. Initializes a fresh Git repository bound to the core-developer_branch.
+ 7. Stages and commits the pristine skeleton infrastructure as a baseline.
+ 8. Maps any upstream remote network targets passed via CLI parameters.
"""
import sys ,os ,subprocess
@@ -28,14 +31,31 @@ def get_Z_date() -> str:
except Exception:
return "1970-01-01"
-def work(project_name: str ,remotes: list[str]) -> int:
- """Orchestrates the assembly of the new skeleton instance."""
- repo_home = os.environ.get("REPO_HOME")
- if not repo_home:
- print("Error: REPO_HOME is not set. A person must run this from within a sourced role environment." ,file=sys.stderr)
- return 1
+def uncommitted(repo_path: Path) -> list[str] | None:
+ """Returns the lines of 'git status --porcelain' for the skeleton: one per
+ file with uncommitted changes or untracked. None when git cannot report."""
+ try:
+ res = subprocess.run(
+ ["git" ,"status" ,"--porcelain" ,"--untracked-files=normal"]
+ ,cwd=str(repo_path) ,capture_output=True ,text=True ,check=True
+ )
+ except Exception:
+ return None
+ return [line for line in res.stdout.splitlines() if line.strip()]
+
+def confirm(question: str) -> bool:
+ """Asks a yes or no question at the terminal. Without a terminal to ask
+ at, the answer is no."""
+ if not sys.stdin.isatty():
+ return False
+ try:
+ answer = input(question + " [y/N] ")
+ except EOFError:
+ return False
+ return answer.strip().lower() in ("y" ,"yes")
- repo_path = Path(repo_home).resolve()
+def work(repo_path: Path ,project_name: str ,remotes: list[str]) -> int:
+ """Orchestrates the assembly of the new skeleton instance."""
# Resolve destination relative to the parent space of the current repository
dest_path = Path(project_name)
@@ -129,7 +149,29 @@ def CLI(argv=None) -> int:
print("Error: project name is required." ,file=sys.stderr)
return 1
- return work(project_name ,remotes)
+ repo_home = os.environ.get("REPO_HOME")
+ if not repo_home:
+ print("Error: REPO_HOME is not set. A person must run this from within a sourced role environment." ,file=sys.stderr)
+ return 1
+ repo_path = Path(repo_home).resolve()
+
+ status = uncommitted(repo_path)
+ if status is None:
+ print(f"Error: could not read the git status of {repo_path}." ,file=sys.stderr)
+ return 1
+ if status:
+ shown = status[:20]
+ print(f"Warning: {repo_path} has {len(status)} uncommitted or untracked file(s):")
+ for line in shown:
+ print(" " + line)
+ if len(status) > len(shown):
+ print(f" ... and {len(status) - len(shown)} more (see 'git status')")
+ print("The new project is made from the last commit, so these will not be in it.")
+ if not confirm("Continue anyway?"):
+ print("Stopped. Nothing was made. Commit or stash the changes, then run new-project again.")
+ return 1
+
+ return work(repo_path ,project_name ,remotes)
if __name__ == "__main__":
sys.exit(CLI())
diff --git a/developer/document/naming_file-and-directory.html b/developer/document/naming_file-and-directory.html
index 8adc549..c597855 100644
--- a/developer/document/naming_file-and-directory.html
+++ b/developer/document/naming_file-and-directory.html
@@ -1,6 +1,4 @@
-
-
diff --git a/developer/tool/build_component/copy b/developer/tool/build_component/copy
index 2939ef3..0a1998c 100755
--- a/developer/tool/build_component/copy
+++ b/developer/tool/build_component/copy
@@ -10,8 +10,8 @@ sys.path.insert(0, os.path.join(os.path.abspath("tool"), "build_component"))
import sync_engine
HELP = """usage: copy {all|clean|help}
- all Stages files from authored to scratchpad.
- clean Removes the specified namespace from scratchpad.
+ all Stages files from authored to scratchpad/made/.
+ clean Removes scratchpad/made/.
help Shows this message.
"""
@@ -24,7 +24,7 @@ def cmd_all():
sys.exit(1)
src_root = os.path.join("authored", namespace_dir)
- dst_root = os.path.join("scratchpad", namespace)
+ dst_root = os.path.join("scratchpad", "made", namespace)
sync_engine.execute_sync(src_root, dst_root, prefix="copy", apply_modes=False)
@@ -34,7 +34,7 @@ def cmd_clean():
print("copy: error: NAMESPACE environment variable must be set.", file=sys.stderr)
sys.exit(1)
- dst_root = os.path.join("scratchpad", namespace)
+ dst_root = os.path.join("scratchpad", "made", namespace)
if os.path.exists(dst_root):
shutil.rmtree(dst_root)
print(f"copy: Cleaned {dst_root}")
diff --git a/developer/tool/promote b/developer/tool/promote
index 56e8cea..7292374 100755
--- a/developer/tool/promote
+++ b/developer/tool/promote
@@ -18,16 +18,22 @@ sys.path.insert(0, os.path.join(SCRIPT_DIR, "build_component"))
import sync_engine
HELP = """usage: promote {write|clean|ls|diff|help|dry write}
- write Writes promoted files from scratchpad into consumer. Only updates newer files.
+ write Copies what is staged in scratchpad/made into consumer. Only updates newer files.
clean Remove all contents of the consumer directory (preserves .gitignore).
ls List consumer as an indented tree: PERMS OWNER NAME.
- diff List missing, orphaned, or out of sync files between scratchpad and consumer.
+ diff List missing, orphaned, or out of sync files between scratchpad/made and consumer.
help Show this message.
dry write Preview what write would do without modifying the filesystem.
+
+Promoted files are read-only: 400, or 500 when executable. Directories below
+consumer/ are 500, so not even the owner can add, replace or remove a file in
+them; write and clean open them (700) for the duration and lock them again.
+consumer/ itself stays 700, as it holds the .gitignore git manages.
"""
SETUP_MUST_BE = "developer/tool/setup"
DEFAULT_DIR_MODE = 0o700
+LOCKED_DIR_MODE = 0o500
def exit_with_status(msg, code=1):
print(f"promote: {msg}", file=sys.stderr)
@@ -38,7 +44,7 @@ def assert_setup():
if setup_val != SETUP_MUST_BE:
hint = (
"SETUP is not 'developer/tool/setup'.\n"
- "Enter the project with: . setup developer\n"
+ "Enter the developer role with: ./setup developer\n"
"That script exports: ROLE=developer; SETUP=$ROLE/tool/setup"
)
exit_with_status(f"bad environment: SETUP='{setup_val}'. {hint}")
@@ -65,6 +71,18 @@ def ensure_dir(path_str, mode=DEFAULT_DIR_MODE, dry=False):
os.makedirs(path_str, exist_ok=True)
ensure_mode(path_str, mode)
+def consumer_subdirs(root_dp):
+ """Every directory below root_dp; root_dp itself is not included."""
+ found = []
+ for TM_root, TM_dirs, TM_files in os.walk(root_dp):
+ for TM_d in TM_dirs:
+ found.append(os.path.join(TM_root, TM_d))
+ return found
+
+def set_subdir_modes(root_dp, mode):
+ for TM_d in consumer_subdirs(root_dp):
+ ensure_mode(TM_d, mode)
+
def filemode(m):
try: return stat.filemode(m)
except Exception: return oct(m & 0o777)
@@ -123,20 +141,26 @@ def list_tree(root_dp):
def cmd_write(dry=False):
dst_root = cpath()
ensure_dir(dst_root, DEFAULT_DIR_MODE, dry=dry)
- src_root = dpath("scratchpad")
-
- sync_engine.execute_sync(
- src_root,
- dst_root,
- prefix="promote",
- dry=dry,
- apply_modes=True,
- ignore_list=[".gitignore"]
- )
+ src_root = dpath("scratchpad", "made")
+
+ # Files are replaced by writing a temporary file beside them and renaming
+ # it into place, which needs write permission on the directory.
+ if not dry: set_subdir_modes(dst_root, DEFAULT_DIR_MODE)
+ try:
+ sync_engine.execute_sync(
+ src_root,
+ dst_root,
+ prefix="promote",
+ dry=dry,
+ apply_modes=True,
+ ignore_list=[".gitignore"]
+ )
+ finally:
+ if not dry: set_subdir_modes(dst_root, LOCKED_DIR_MODE)
def cmd_diff():
dst_root = cpath()
- src_root = dpath("scratchpad")
+ src_root = dpath("scratchpad", "made")
src_files = set()
if os.path.isdir(src_root):
@@ -166,7 +190,7 @@ def cmd_diff():
print(f"Pending promotion (missing in consumer): {TM_rel}")
found_diff = True
elif TM_rel not in src_files:
- print(f"Orphaned in consumer (missing in scratchpad): {TM_rel}")
+ print(f"Orphaned in consumer (missing in scratchpad/made): {TM_rel}")
found_diff = True
else:
src_abs = os.path.join(src_root, TM_rel)
@@ -177,18 +201,20 @@ def cmd_diff():
dst_mtime = os.stat(dst_abs).st_mtime
if src_mtime > dst_mtime:
- print(f"Pending update (contents differ, newer in scratchpad): {TM_rel}")
+ print(f"Pending update (contents differ, newer in scratchpad/made): {TM_rel}")
else:
print(f"Contents differ (locally modified in consumer): {TM_rel}")
found_diff = True
if not found_diff:
- print("No differences found. Consumer matches developer scratchpad.")
+ print("No differences found. Consumer matches developer scratchpad/made.")
def cmd_clean():
consumer_root_dir = cpath()
if not os.path.isdir(consumer_root_dir):
return
+ # Removing an entry needs write permission on the directory holding it.
+ set_subdir_modes(consumer_root_dir, DEFAULT_DIR_MODE)
for TM_name in os.listdir(consumer_root_dir):
if TM_name == ".gitignore":
continue
diff --git a/document/introduction_Harmony.html b/document/introduction_Harmony.html
index e6ea9b4..432c63b 100644
--- a/document/introduction_Harmony.html
+++ b/document/introduction_Harmony.html
@@ -22,9 +22,9 @@
diff --git a/document/role-and-workflow_product-development.html b/document/role-and-workflow_product-development.html
index a15ab9c..411b732 100644
--- a/document/role-and-workflow_product-development.html
+++ b/document/role-and-workflow_product-development.html
@@ -40,13 +40,13 @@
These roles interact directly with the repository. To enter a workspace, change directory to the top-level of the project and source the setup file for the desired role:
- > . setup administrator
- > . setup developer
- > . setup tester
- > . setup consumer
+ > ./setup administrator
+ > ./setup developer
+ > ./setup tester
+ > ./setup consumer
-
- It is common for a person to have multiple terminal sessions or IDEs open, each running under a different role environment.
+
+
This will run a new shell with the proper environment setting. It is common for a person to have multiple shells, terminal sessions, or IDEs open, each running under a different role environment. Development and tests is a common pair, though tests are intended to be production tests. The developer has an experiment directory for experiments.
Administrator role
diff --git a/setup b/setup
index f750f1d..8ab56c8 100755
--- a/setup
+++ b/setup
@@ -30,11 +30,6 @@ def main():
print(f"ROLE_HOME {role_home}")
- # Python-domain logic: PATH injection
- path_parts = env.get("PATH", "").split(os.pathsep)
- if tool_dir not in path_parts:
- env["PATH"] = f"{tool_dir}{os.pathsep}{env.get('PATH', '')}"
-
# Python-domain logic: Directory validation
if not os.path.isdir(role):
print(f"setup: failed to locate directory for role '{role}'")
@@ -47,6 +42,8 @@ def main():
'if [[ -f ~/.bashrc ]]; then source ~/.bashrc; fi',
'if [[ -f "shared/tool/setup" ]]; then source "shared/tool/setup"; fi',
'if [[ -f "shared/authored/setup" ]]; then source "shared/authored/setup"; fi',
+ '# the role tool directory is searched before everything the shared setup added',
+ f'PATH="{tool_dir}:$PATH"; export PATH',
f'cd "{role}"',
'if [[ -f "tool/setup" ]]; then',
' source "tool/setup"',
diff --git a/shared/tool/setup b/shared/tool/setup
index 10fb4cd..a7a66aa 100644
--- a/shared/tool/setup
+++ b/shared/tool/setup
@@ -57,25 +57,28 @@ umask 0077
# --------------------------------------------------------------------------------
# PATH & Plugin Sourcing
-# precedence: last defined, first discovered
-
- PATH="$REPO_HOME/shared/authored:$PATH"
- PATH="$REPO_HOME/shared/made:$PATH"
- PATH="$REPO_HOME/shared/tool:$PATH"
+# Each addition goes to the front, so the last added is searched first.
+# Resulting search order, most specific first:
+# shared/authored, shared/made, shared/tool, project dependencies, the rest.
+# The top-level setup then puts the role's tool directory in front of these.
- # Remove duplicates
- clean_path() {
- PATH=$(echo ":$PATH" | awk -v RS=: -v ORS=: '!seen[$0]++' | sed 's/^://; s/:$//')
- }
- clean_path
-
- # Plugin Sourcing: Dynamically load all the linked project configurations
+ # Project dependencies: source each .init file. Each prepends its own entries.
for init_file in "$REPO_HOME/shared/project-dependency/"*.init; do
if [[ -f "$init_file" ]]; then
source "$init_file"
fi
done
+ PATH="$REPO_HOME/shared/tool:$PATH"
+ PATH="$REPO_HOME/shared/made:$PATH"
+ PATH="$REPO_HOME/shared/authored:$PATH"
+
+ # Remove duplicates, keeping the first occurrence
+ clean_path() {
+ PATH=$(echo ":$PATH" | awk -v RS=: -v ORS=: '!seen[$0]++' | sed 's/^://; s/:$//')
+ }
+ clean_path
+
export PATH
# --------------------------------------------------------------------------------
--
2.20.1