From 483aa0537ff19cca7737806305beeb119f529a20 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Thu, 10 Sep 2026 09:50:43 +0100 Subject: [PATCH 01/26] Create test_deps_manager.py --- tests/unit/test_deps_manager.py | 47 +++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 tests/unit/test_deps_manager.py diff --git a/tests/unit/test_deps_manager.py b/tests/unit/test_deps_manager.py new file mode 100644 index 0000000..a375ec7 --- /dev/null +++ b/tests/unit/test_deps_manager.py @@ -0,0 +1,47 @@ +import subprocess +from pathlib import Path +from unittest.mock import patch + +from ebuild.deps.manager import DepsManager + + +def test_git_clone_without_branch_does_not_pass_branch(tmp_path): + dest = tmp_path / "repo" + + with patch("ebuild.deps.manager.subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess( + args=[], returncode=0, stdout="", stderr="" + ) + + DepsManager._git_clone( + "https://github.com/example/repo.git", + dest, + None, + True, + ) + + command = mock_run.call_args[0][0] + + assert "--branch" not in command + assert "master" not in command + + +def test_git_clone_with_branch_passes_branch(tmp_path): + dest = tmp_path / "repo" + + with patch("ebuild.deps.manager.subprocess.run") as mock_run: + mock_run.return_value = subprocess.CompletedProcess( + args=[], returncode=0, stdout="", stderr="" + ) + + DepsManager._git_clone( + "https://github.com/example/repo.git", + dest, + "develop", + True, + ) + + command = mock_run.call_args[0][0] + + assert "--branch" in command + assert "develop" in command From 322066fc031f3102bd650c5f4f8b0b9e17e28d8b Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Thu, 10 Sep 2026 10:19:34 +0100 Subject: [PATCH 02/26] Update manager.py --- ebuild/deps/manager.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index 0db81cc..b50673e 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -153,7 +153,9 @@ def setup( # Clone to cache effective_url = repo_cfg.get("url") or self._default_url(repo_name) - effective_branch = repo_cfg.get("branch") or "master" + Clone to cache +effective_url = repo_cfg.get("url") or self._default_url(repo_name) +effective_branch = repo_cfg.get("branch") or "master" dest = self.cache_dir / repo_name if dest.exists(): @@ -354,11 +356,14 @@ def _default_url(repo_name: str) -> str: return "" @staticmethod - def _git_clone(url: str, dest: Path, branch: str, shallow: bool) -> None: + def _git_clone(url: str, dest: Path, branch: Optional[str], shallow: bool) -> None: cmd = ["git", "clone"] if shallow: cmd.extend(["--depth", "1"]) - cmd.extend(["--branch", branch, url, str(dest)]) + if branch: + cmd.extend(["--branch", branch]) + +cmd.extend([url, str(dest)]) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: From f0995a11c5e7062f81c828bebee6f04df829005b Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Mon, 14 Sep 2026 16:47:04 +0100 Subject: [PATCH 03/26] Update manager.py --- ebuild/deps/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index b50673e..b233b7c 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -128,7 +128,7 @@ def setup( Args: repo_name: ``"eos"`` or ``"eboot"``. url: Git URL override. Falls back to config → default. - branch: Branch/tag override. Falls back to config → ``"master"``. + branch: Branch/tag override. Falls back to config; if unset, Git uses the remote default branch. path: If given, register this local path instead of cloning. shallow: Use ``--depth 1`` for faster clones (default *True*). From 314adadfe63963d46034c6277815286515ce8070 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Mon, 14 Sep 2026 16:51:30 +0100 Subject: [PATCH 04/26] Update manager.py --- ebuild/deps/manager.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index b233b7c..3004017 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -150,19 +150,18 @@ def setup( repo_cfg["path"] = str(p) self.save_config() return p - - # Clone to cache - effective_url = repo_cfg.get("url") or self._default_url(repo_name) - Clone to cache +# Clone to cache effective_url = repo_cfg.get("url") or self._default_url(repo_name) -effective_branch = repo_cfg.get("branch") or "master" +effective_branch = repo_cfg.get("branch") - dest = self.cache_dir / repo_name - if dest.exists(): - # Already cloned — optionally switch branch - self._checkout_branch(dest, effective_branch) - self.save_config() - return dest +dest = self.cache_dir / repo_name + +if dest.exists(): + # Already cloned — optionally switch branch + if effective_branch: + self._checkout_branch(dest, effective_branch) + self.save_config() + return dest self.cache_dir.mkdir(parents=True, exist_ok=True) self._git_clone(effective_url, dest, effective_branch, shallow) From 93eb697b40e9b876203c3f136d6c8e0192e7c2b0 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Mon, 14 Sep 2026 17:01:17 +0100 Subject: [PATCH 05/26] Update manager.py fix(ebuild): resolve repository default branches --- ebuild/deps/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index 3004017..ef50538 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -150,7 +150,7 @@ def setup( repo_cfg["path"] = str(p) self.save_config() return p -# Clone to cache + effective_url = repo_cfg.get("url") or self._default_url(repo_name) effective_branch = repo_cfg.get("branch") From 420f75bf135cc82d5b6e2ff602ced2ca1f27fd18 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Mon, 14 Sep 2026 22:45:32 +0100 Subject: [PATCH 06/26] Update manager.py fix(ebuild): resolve repository default branches --- ebuild/deps/manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index ef50538..af5fdca 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -155,7 +155,6 @@ def setup( effective_branch = repo_cfg.get("branch") dest = self.cache_dir / repo_name - if dest.exists(): # Already cloned — optionally switch branch if effective_branch: From c935cfc1863705584d4d7d140cb7aa6b8b2bcacd Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Mon, 14 Sep 2026 22:55:09 +0100 Subject: [PATCH 07/26] Update manager.py --- ebuild/deps/manager.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index af5fdca..7aa77e1 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -368,7 +368,32 @@ def _git_clone(url: str, dest: Path, branch: Optional[str], shallow: bool) -> No raise RuntimeError(f"Failed to clone {url}: {result.stderr.strip()}") @staticmethod - def _checkout_branch(repo_dir: Path, branch: str) -> None: +def _checkout_branch(repo_dir: Path, branch: Optional[str]) -> None: + if not branch: + return + + current = DepsManager._git_current_branch(repo_dir) + + if current != branch: + fetch = subprocess.run( + ["git", "-C", str(repo_dir), "fetch", "--all"], + capture_output=True, + text=True, + ) + if fetch.returncode != 0: + raise RuntimeError( + f"Failed to fetch {repo_dir}: {fetch.stderr.strip()}" + ) + + checkout = subprocess.run( + ["git", "-C", str(repo_dir), "checkout", branch], + capture_output=True, + text=True, + ) + if checkout.returncode != 0: + raise RuntimeError( + f"Failed to checkout {branch}: {checkout.stderr.strip()}" + ) current = DepsManager._git_current_branch(repo_dir) if current != branch: subprocess.run( From 2178ca8d96204346f9d7e0631e3eed09ce95a416 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Mon, 14 Sep 2026 22:59:28 +0100 Subject: [PATCH 08/26] Update manager.py --- ebuild/deps/manager.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index 7aa77e1..dd05b79 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -367,13 +367,38 @@ def _git_clone(url: str, dest: Path, branch: Optional[str], shallow: bool) -> No if result.returncode != 0: raise RuntimeError(f"Failed to clone {url}: {result.stderr.strip()}") - @staticmethod +@staticmethod def _checkout_branch(repo_dir: Path, branch: Optional[str]) -> None: if not branch: return current = DepsManager._git_current_branch(repo_dir) + if current != branch: + fetch = subprocess.run( + ["git", "-C", str(repo_dir), "fetch", "--all"], + capture_output=True, + text=True, + ) + if fetch.returncode != 0: + raise RuntimeError( + f"Failed to fetch {repo_dir}: {fetch.stderr.strip()}" + ) + + checkout = subprocess.run( + ["git", "-C", str(repo_dir), "checkout", branch], + capture_output=True, + text=True, + ) + if checkout.returncode != 0: + raise RuntimeError( + f"Failed to checkout {branch}: {checkout.stderr.strip()}" + ) + if not branch: + return + + current = DepsManager._git_current_branch(repo_dir) + if current != branch: fetch = subprocess.run( ["git", "-C", str(repo_dir), "fetch", "--all"], From 59a5b9663c06cf75095765405cee9c1e0b9e1258 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Mon, 14 Sep 2026 23:05:10 +0100 Subject: [PATCH 09/26] Update manager.py --- ebuild/deps/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index dd05b79..a312187 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -291,7 +291,7 @@ def status(self) -> List[Dict[str, Any]]: info: Dict[str, Any] = { "name": name, "url": repo_cfg.get("url", self._default_url(name)), - "branch": repo_cfg.get("branch", "master"), + "branch": repo_cfg.get("branch"), "config_path": repo_cfg.get("path"), "cached": cached.is_dir(), "cache_location": str(cached) if cached.is_dir() else None, From 91c695212e4f20e5eecc0a3585409e2ced3e2292 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Mon, 14 Sep 2026 23:20:47 +0100 Subject: [PATCH 10/26] Update manager.py --- ebuild/deps/manager.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index a312187..b7c9b40 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -368,12 +368,38 @@ def _git_clone(url: str, dest: Path, branch: Optional[str], shallow: bool) -> No raise RuntimeError(f"Failed to clone {url}: {result.stderr.strip()}") @staticmethod +def @staticmethod def _checkout_branch(repo_dir: Path, branch: Optional[str]) -> None: if not branch: return current = DepsManager._git_current_branch(repo_dir) + if current != branch: + fetch = subprocess.run( + ["git", "-C", str(repo_dir), "fetch", "--all"], + capture_output=True, + text=True, + ) + if fetch.returncode != 0: + raise RuntimeError( + f"Failed to fetch {repo_dir}: {fetch.stderr.strip()}" + ) + + checkout = subprocess.run( + ["git", "-C", str(repo_dir), "checkout", branch], + capture_output=True, + text=True, + ) + if checkout.returncode != 0: + raise RuntimeError( + f"Failed to checkout {branch}: {checkout.stderr.strip()}" + ) -> None: + if not branch: + return + + current = DepsManager._git_current_branch(repo_dir) + if current != branch: fetch = subprocess.run( ["git", "-C", str(repo_dir), "fetch", "--all"], From e3446d2d62580135edc2feb99cc9e507cd54212b Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Mon, 14 Sep 2026 23:41:50 +0100 Subject: [PATCH 11/26] Update manager.py --- ebuild/deps/manager.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index b7c9b40..852fd69 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -366,9 +366,7 @@ def _git_clone(url: str, dest: Path, branch: Optional[str], shallow: bool) -> No result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: raise RuntimeError(f"Failed to clone {url}: {result.stderr.strip()}") - @staticmethod -def @staticmethod def _checkout_branch(repo_dir: Path, branch: Optional[str]) -> None: if not branch: return @@ -386,6 +384,16 @@ def _checkout_branch(repo_dir: Path, branch: Optional[str]) -> None: f"Failed to fetch {repo_dir}: {fetch.stderr.strip()}" ) + checkout = subprocess.run( + ["git", "-C", str(repo_dir), "checkout", branch], + capture_output=True, + text=True, + ) + if checkout.returncode != 0: + raise RuntimeError( + f"Failed to checkout {branch}: {checkout.stderr.strip()}" + ) + checkout = subprocess.run( ["git", "-C", str(repo_dir), "checkout", branch], capture_output=True, From 1cd23f0ce257ced25c2b0c92f67ec46b24055e08 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 07:25:44 +0100 Subject: [PATCH 12/26] Update manager.py --- ebuild/deps/manager.py | 413 ++++++++++++++++++++++++++++------------- 1 file changed, 280 insertions(+), 133 deletions(-) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index 852fd69..934477a 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -71,6 +71,7 @@ def load_config(self) -> Dict[str, Any]: if EBUILD_CONFIG_PATH.exists(): with open(EBUILD_CONFIG_PATH, "r", encoding="utf-8") as f: data = yaml.safe_load(f) or {} + # Merge missing keys from defaults. Deep-copied: setup(), # set_url(), set_branch() and link() mutate these nested dicts # in place, and an aliased default would leak one instance's @@ -82,6 +83,7 @@ def load_config(self) -> Dict[str, Any]: for rname, rval in val.items(): if rname not in data["repos"]: data["repos"][rname] = copy.deepcopy(rval) + return data self._config = copy.deepcopy(DEFAULT_CONFIG) @@ -92,7 +94,12 @@ def save_config(self) -> None: """Write the current configuration to ``~/.ebuild/config.yaml``.""" ensure_ebuild_home() with open(EBUILD_CONFIG_PATH, "w", encoding="utf-8") as f: - yaml.dump(self._config, f, default_flow_style=False, sort_keys=False) + yaml.dump( + self._config, + f, + default_flow_style=False, + sort_keys=False, + ) @property def config(self) -> Dict[str, Any]: @@ -108,7 +115,11 @@ def cache_dir(self) -> Path: env = os.environ.get("EBUILD_REPOS_DIR") if env: return Path(env) - raw = self._config.get("cache_dir", str(EBUILD_REPOS_DIR)) + + raw = self._config.get( + "cache_dir", + str(EBUILD_REPOS_DIR), + ) return Path(os.path.expanduser(raw)) # ------------------------------------------------------------------ @@ -128,42 +139,69 @@ def setup( Args: repo_name: ``"eos"`` or ``"eboot"``. url: Git URL override. Falls back to config → default. - branch: Branch/tag override. Falls back to config; if unset, Git uses the remote default branch. + branch: Branch/tag override. Falls back to config; if unset, + Git uses the remote default branch. path: If given, register this local path instead of cloning. shallow: Use ``--depth 1`` for faster clones (default *True*). Returns: The resolved local path of the repo. """ - repo_cfg = self._config.setdefault("repos", {}).setdefault(repo_name, {}) + repo_cfg = self._config.setdefault( + "repos", + {}, + ).setdefault(repo_name, {}) if url: repo_cfg["url"] = url + if branch: repo_cfg["branch"] = branch if path: # Link to local repo — no clone needed p = Path(path).resolve() + if not p.is_dir(): - raise FileNotFoundError(f"Local repo path does not exist: {p}") + raise FileNotFoundError( + f"Local repo path does not exist: {p}" + ) + repo_cfg["path"] = str(p) self.save_config() return p -effective_url = repo_cfg.get("url") or self._default_url(repo_name) -effective_branch = repo_cfg.get("branch") + effective_url = ( + repo_cfg.get("url") + or self._default_url(repo_name) + ) + effective_branch = repo_cfg.get("branch") + + dest = self.cache_dir / repo_name + + if dest.exists(): + # Already cloned — optionally switch branch + if effective_branch: + self._checkout_branch( + dest, + effective_branch, + ) + + self.save_config() + return dest + + self.cache_dir.mkdir( + parents=True, + exist_ok=True, + ) -dest = self.cache_dir / repo_name -if dest.exists(): - # Already cloned — optionally switch branch - if effective_branch: - self._checkout_branch(dest, effective_branch) - self.save_config() - return dest + self._git_clone( + effective_url, + dest, + effective_branch, + shallow, + ) - self.cache_dir.mkdir(parents=True, exist_ok=True) - self._git_clone(effective_url, dest, effective_branch, shallow) self.save_config() return dest @@ -187,44 +225,62 @@ def get_repo_path( """ # 1. CLI override cli_path = self._cli_overrides.get(repo_name) + if cli_path: p = Path(cli_path).resolve() + if p.is_dir(): return p # 2. Environment variable env_var = ENV_PATH_VARS.get(repo_name) + if env_var: env_val = os.environ.get(env_var) + if env_val: p = Path(env_val).resolve() + if p.is_dir(): return p # 3. Config path override - repo_cfg = self._config.get("repos", {}).get(repo_name, {}) + repo_cfg = self._config.get( + "repos", + {}, + ).get(repo_name, {}) + config_path = repo_cfg.get("path") + if config_path: p = Path(config_path).resolve() + if p.is_dir(): return p # 4. Cached clone cached = self.cache_dir / repo_name + if cached.is_dir(): return cached - # 5. Sibling directory (try documented aliases; Linux is case-sensitive) + # 5. Sibling directory if project_dir: - names = SIBLING_DIR_NAMES.get(repo_name, (repo_name,)) + names = SIBLING_DIR_NAMES.get( + repo_name, + (repo_name,), + ) + for name in names: sibling = project_dir.parent / name + if sibling.is_dir(): return sibling - # 6. Legacy embedded core// (deprecation warning) + # 6. Legacy embedded core// if project_dir: core_path = project_dir / "core" / repo_name + if core_path.is_dir(): warnings.warn( f"Using embedded core/{repo_name}/ is deprecated. " @@ -236,44 +292,77 @@ def get_repo_path( return None - def is_available(self, repo_name: str, project_dir: Optional[Path] = None) -> bool: + def is_available( + self, + repo_name: str, + project_dir: Optional[Path] = None, + ) -> bool: """Check if *repo_name* is resolvable anywhere.""" - return self.get_repo_path(repo_name, project_dir) is not None + return ( + self.get_repo_path( + repo_name, + project_dir, + ) + is not None + ) # ------------------------------------------------------------------ # Update # ------------------------------------------------------------------ - def update(self, repo_name: Optional[str] = None) -> Dict[str, str]: + def update( + self, + repo_name: Optional[str] = None, + ) -> Dict[str, str]: """Git pull latest for one or all repos. Returns: Dict mapping repo name to result string (e.g. ``"updated"``). """ - names = [repo_name] if repo_name else list(KNOWN_REPOS) + names = ( + [repo_name] + if repo_name + else list(KNOWN_REPOS) + ) + results: Dict[str, str] = {} for name in names: repo_dir = self.cache_dir / name + if not repo_dir.is_dir(): results[name] = "not cloned" continue - repo_cfg = self._config.get("repos", {}).get(name, {}) + repo_cfg = self._config.get( + "repos", + {}, + ).get(name, {}) + if repo_cfg.get("path"): results[name] = "linked (skipped)" continue try: subprocess.run( - ["git", "-C", str(repo_dir), "pull", "--ff-only"], + [ + "git", + "-C", + str(repo_dir), + "pull", + "--ff-only", + ], capture_output=True, text=True, check=True, ) + results[name] = "updated" + except subprocess.CalledProcessError as e: - results[name] = f"failed: {e.stderr.strip()}" + results[name] = ( + f"failed: {e.stderr.strip()}" + ) return results @@ -284,23 +373,39 @@ def update(self, repo_name: Optional[str] = None) -> Dict[str, str]: def status(self) -> List[Dict[str, Any]]: """Return status info for all known repos.""" entries: List[Dict[str, Any]] = [] + for name in KNOWN_REPOS: - repo_cfg = self._config.get("repos", {}).get(name, {}) + repo_cfg = self._config.get( + "repos", + {}, + ).get(name, {}) + cached = self.cache_dir / name info: Dict[str, Any] = { "name": name, - "url": repo_cfg.get("url", self._default_url(name)), + "url": repo_cfg.get( + "url", + self._default_url(name), + ), "branch": repo_cfg.get("branch"), "config_path": repo_cfg.get("path"), "cached": cached.is_dir(), - "cache_location": str(cached) if cached.is_dir() else None, + "cache_location": ( + str(cached) + if cached.is_dir() + else None + ), } # Get current git branch/commit if cloned if cached.is_dir(): - info["git_branch"] = self._git_current_branch(cached) - info["git_commit"] = self._git_head_commit(cached) + info["git_branch"] = ( + self._git_current_branch(cached) + ) + info["git_commit"] = ( + self._git_head_commit(cached) + ) entries.append(info) @@ -310,18 +415,34 @@ def status(self) -> List[Dict[str, Any]]: # Link / unlink # ------------------------------------------------------------------ - def link(self, repo_name: str, local_path: str) -> None: + def link( + self, + repo_name: str, + local_path: str, + ) -> None: """Register a local path override (no symlink — just config entry).""" p = Path(local_path).resolve() + if not p.is_dir(): - raise FileNotFoundError(f"Path does not exist: {p}") - repo_cfg = self._config.setdefault("repos", {}).setdefault(repo_name, {}) + raise FileNotFoundError( + f"Path does not exist: {p}" + ) + + repo_cfg = self._config.setdefault( + "repos", + {}, + ).setdefault(repo_name, {}) + repo_cfg["path"] = str(p) self.save_config() def unlink(self, repo_name: str) -> None: """Remove a local path override, reverting to cache.""" - repo_cfg = self._config.get("repos", {}).get(repo_name, {}) + repo_cfg = self._config.get( + "repos", + {}, + ).get(repo_name, {}) + repo_cfg.pop("path", None) self.save_config() @@ -329,15 +450,31 @@ def unlink(self, repo_name: str) -> None: # URL / branch setters # ------------------------------------------------------------------ - def set_url(self, repo_name: str, url: str) -> None: + def set_url( + self, + repo_name: str, + url: str, + ) -> None: """Change the git URL for a repo.""" - repo_cfg = self._config.setdefault("repos", {}).setdefault(repo_name, {}) + repo_cfg = self._config.setdefault( + "repos", + {}, + ).setdefault(repo_name, {}) + repo_cfg["url"] = url self.save_config() - def set_branch(self, repo_name: str, branch: str) -> None: + def set_branch( + self, + repo_name: str, + branch: str, + ) -> None: """Change the branch/tag for a repo.""" - repo_cfg = self._config.setdefault("repos", {}).setdefault(repo_name, {}) + repo_cfg = self._config.setdefault( + "repos", + {}, + ).setdefault(repo_name, {}) + repo_cfg["branch"] = branch self.save_config() @@ -349,145 +486,155 @@ def set_branch(self, repo_name: str, branch: str) -> None: def _default_url(repo_name: str) -> str: if repo_name == "eos": return DEFAULT_EOS_REPO_URL + if repo_name == "eboot": return DEFAULT_EBOOT_REPO_URL + return "" @staticmethod - def _git_clone(url: str, dest: Path, branch: Optional[str], shallow: bool) -> None: + def _git_clone( + url: str, + dest: Path, + branch: Optional[str], + shallow: bool, + ) -> None: cmd = ["git", "clone"] - if shallow: - cmd.extend(["--depth", "1"]) - if branch: - cmd.extend(["--branch", branch]) - -cmd.extend([url, str(dest)]) - - result = subprocess.run(cmd, capture_output=True, text=True) - if result.returncode != 0: - raise RuntimeError(f"Failed to clone {url}: {result.stderr.strip()}") -@staticmethod -def _checkout_branch(repo_dir: Path, branch: Optional[str]) -> None: - if not branch: - return - - current = DepsManager._git_current_branch(repo_dir) - if current != branch: - fetch = subprocess.run( - ["git", "-C", str(repo_dir), "fetch", "--all"], - capture_output=True, - text=True, - ) - if fetch.returncode != 0: - raise RuntimeError( - f"Failed to fetch {repo_dir}: {fetch.stderr.strip()}" + if shallow: + cmd.extend( + [ + "--depth", + "1", + ] ) - checkout = subprocess.run( - ["git", "-C", str(repo_dir), "checkout", branch], - capture_output=True, - text=True, - ) - if checkout.returncode != 0: - raise RuntimeError( - f"Failed to checkout {branch}: {checkout.stderr.strip()}" + if branch: + cmd.extend( + [ + "--branch", + branch, + ] ) - checkout = subprocess.run( - ["git", "-C", str(repo_dir), "checkout", branch], - capture_output=True, - text=True, + cmd.extend( + [ + url, + str(dest), + ] ) - if checkout.returncode != 0: - raise RuntimeError( - f"Failed to checkout {branch}: {checkout.stderr.strip()}" - ) -> None: - if not branch: - return - current = DepsManager._git_current_branch(repo_dir) - - if current != branch: - fetch = subprocess.run( - ["git", "-C", str(repo_dir), "fetch", "--all"], + result = subprocess.run( + cmd, capture_output=True, text=True, ) - if fetch.returncode != 0: - raise RuntimeError( - f"Failed to fetch {repo_dir}: {fetch.stderr.strip()}" - ) - checkout = subprocess.run( - ["git", "-C", str(repo_dir), "checkout", branch], - capture_output=True, - text=True, - ) - if checkout.returncode != 0: + if result.returncode != 0: raise RuntimeError( - f"Failed to checkout {branch}: {checkout.stderr.strip()}" + f"Failed to clone {url}: " + f"{result.stderr.strip()}" ) - if not branch: - return - current = DepsManager._git_current_branch(repo_dir) - - if current != branch: - fetch = subprocess.run( - ["git", "-C", str(repo_dir), "fetch", "--all"], - capture_output=True, - text=True, + @staticmethod + def _checkout_branch( + repo_dir: Path, + branch: Optional[str], + ) -> None: + if not branch: + return + + current = DepsManager._git_current_branch( + repo_dir ) - if fetch.returncode != 0: - raise RuntimeError( - f"Failed to fetch {repo_dir}: {fetch.stderr.strip()}" - ) - checkout = subprocess.run( - ["git", "-C", str(repo_dir), "checkout", branch], - capture_output=True, - text=True, - ) - if checkout.returncode != 0: - raise RuntimeError( - f"Failed to checkout {branch}: {checkout.stderr.strip()}" - ) - current = DepsManager._git_current_branch(repo_dir) if current != branch: - subprocess.run( - ["git", "-C", str(repo_dir), "fetch", "--all"], + fetch = subprocess.run( + [ + "git", + "-C", + str(repo_dir), + "fetch", + "--all", + ], capture_output=True, text=True, ) - subprocess.run( - ["git", "-C", str(repo_dir), "checkout", branch], + + if fetch.returncode != 0: + raise RuntimeError( + f"Failed to fetch {repo_dir}: " + f"{fetch.stderr.strip()}" + ) + + checkout = subprocess.run( + [ + "git", + "-C", + str(repo_dir), + "checkout", + branch, + ], capture_output=True, text=True, ) + if checkout.returncode != 0: + raise RuntimeError( + f"Failed to checkout {branch}: " + f"{checkout.stderr.strip()}" + ) + @staticmethod - def _git_current_branch(repo_dir: Path) -> str: + def _git_current_branch( + repo_dir: Path, + ) -> str: try: result = subprocess.run( - ["git", "-C", str(repo_dir), "rev-parse", "--abbrev-ref", "HEAD"], + [ + "git", + "-C", + str(repo_dir), + "rev-parse", + "--abbrev-ref", + "HEAD", + ], capture_output=True, text=True, check=True, ) + return result.stdout.strip() - except (subprocess.CalledProcessError, FileNotFoundError): + + except ( + subprocess.CalledProcessError, + FileNotFoundError, + ): return "(unknown)" @staticmethod - def _git_head_commit(repo_dir: Path) -> str: + def _git_head_commit( + repo_dir: Path, + ) -> str: try: result = subprocess.run( - ["git", "-C", str(repo_dir), "rev-parse", "--short", "HEAD"], + [ + "git", + "-C", + str(repo_dir), + "rev-parse", + "--short", + "HEAD", + ], capture_output=True, text=True, check=True, ) + return result.stdout.strip() - except (subprocess.CalledProcessError, FileNotFoundError): + + except ( + subprocess.CalledProcessError, + FileNotFoundError, + ): return "(unknown)" From 9698c6588bd500eb35bfb88347ddcfec05cd10c5 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 07:30:33 +0100 Subject: [PATCH 13/26] Update manager.py --- ebuild/deps/manager.py | 312 +++++++++++++++++++---------------------- 1 file changed, 142 insertions(+), 170 deletions(-) diff --git a/ebuild/deps/manager.py b/ebuild/deps/manager.py index 934477a..e71c4e5 100644 --- a/ebuild/deps/manager.py +++ b/ebuild/deps/manager.py @@ -53,8 +53,10 @@ class DepsManager: 2. Environment variables ``EBUILD_EOS_PATH`` / ``EBUILD_EBOOT_PATH`` 3. ``~/.ebuild/config.yaml`` custom ``path:`` override 4. ``~/.ebuild/repos//`` (cached git clone) - 5. Sibling directory ``..//`` (workspace layout; ``eboot`` also tries ``eBoot``) - 6. Embedded ``core//`` (legacy fallback — prints deprecation warning) + 5. Sibling directory ``..//`` (workspace layout; + ``eboot`` also tries ``eBoot``) + 6. Embedded ``core//`` (legacy fallback — prints deprecation + warning) """ def __init__(self, cli_overrides: Optional[Dict[str, str]] = None) -> None: @@ -72,17 +74,17 @@ def load_config(self) -> Dict[str, Any]: with open(EBUILD_CONFIG_PATH, "r", encoding="utf-8") as f: data = yaml.safe_load(f) or {} - # Merge missing keys from defaults. Deep-copied: setup(), - # set_url(), set_branch() and link() mutate these nested dicts + # Merge missing keys from defaults. Deep-copied because setup(), + # set_url(), set_branch() and link() mutate nested dictionaries # in place, and an aliased default would leak one instance's # edits into DEFAULT_CONFIG for the rest of the process. for key, val in DEFAULT_CONFIG.items(): if key not in data: data[key] = copy.deepcopy(val) elif key == "repos" and isinstance(val, dict): - for rname, rval in val.items(): - if rname not in data["repos"]: - data["repos"][rname] = copy.deepcopy(rval) + for repo_name, repo_value in val.items(): + if repo_name not in data["repos"]: + data["repos"][repo_name] = copy.deepcopy(repo_value) return data @@ -93,6 +95,7 @@ def load_config(self) -> Dict[str, Any]: def save_config(self) -> None: """Write the current configuration to ``~/.ebuild/config.yaml``.""" ensure_ebuild_home() + with open(EBUILD_CONFIG_PATH, "w", encoding="utf-8") as f: yaml.dump( self._config, @@ -103,6 +106,7 @@ def save_config(self) -> None: @property def config(self) -> Dict[str, Any]: + """Return the current configuration.""" return self._config # ------------------------------------------------------------------ @@ -111,15 +115,12 @@ def config(self) -> Dict[str, Any]: @property def cache_dir(self) -> Path: - """Resolved cache directory (from config or env var).""" + """Resolve the cache directory from config or environment.""" env = os.environ.get("EBUILD_REPOS_DIR") if env: return Path(env) - raw = self._config.get( - "cache_dir", - str(EBUILD_REPOS_DIR), - ) + raw = self._config.get("cache_dir", str(EBUILD_REPOS_DIR)) return Path(os.path.expanduser(raw)) # ------------------------------------------------------------------ @@ -134,23 +135,24 @@ def setup( path: Optional[str] = None, shallow: bool = True, ) -> Path: - """Clone or link a repo. + """Clone or link a repository. Args: repo_name: ``"eos"`` or ``"eboot"``. - url: Git URL override. Falls back to config → default. - branch: Branch/tag override. Falls back to config; if unset, - Git uses the remote default branch. + url: Git URL override. Falls back to config, then default URL. + branch: Branch/tag override. Falls back to configured branch. + If no branch is configured, the remote repository's default + branch is used. path: If given, register this local path instead of cloning. - shallow: Use ``--depth 1`` for faster clones (default *True*). + shallow: Use ``--depth 1`` for faster clones. Returns: - The resolved local path of the repo. + The resolved local path of the repository. """ - repo_cfg = self._config.setdefault( - "repos", + repo_cfg = self._config.setdefault("repos", {}).setdefault( + repo_name, {}, - ).setdefault(repo_name, {}) + ) if url: repo_cfg["url"] = url @@ -159,41 +161,37 @@ def setup( repo_cfg["branch"] = branch if path: - # Link to local repo — no clone needed - p = Path(path).resolve() + # Link to local repo — no clone needed. + local_path = Path(path).resolve() - if not p.is_dir(): + if not local_path.is_dir(): raise FileNotFoundError( - f"Local repo path does not exist: {p}" + f"Local repo path does not exist: {local_path}" ) - repo_cfg["path"] = str(p) + repo_cfg["path"] = str(local_path) self.save_config() - return p + return local_path - effective_url = ( - repo_cfg.get("url") - or self._default_url(repo_name) - ) + effective_url = repo_cfg.get("url") or self._default_url(repo_name) + + # Do not invent a branch when one was not configured. + # Passing None to _git_clone causes git to use the remote's + # default branch. effective_branch = repo_cfg.get("branch") dest = self.cache_dir / repo_name if dest.exists(): - # Already cloned — optionally switch branch + # Already cloned — only switch branches when one was + # explicitly configured. if effective_branch: - self._checkout_branch( - dest, - effective_branch, - ) + self._checkout_branch(dest, effective_branch) self.save_config() return dest - self.cache_dir.mkdir( - parents=True, - exist_ok=True, - ) + self.cache_dir.mkdir(parents=True, exist_ok=True) self._git_clone( effective_url, @@ -218,45 +216,41 @@ def get_repo_path( Args: repo_name: ``"eos"`` or ``"eboot"``. - project_dir: Current project directory (for sibling/core fallback). + project_dir: Current project directory for sibling/core fallback. Returns: - Absolute path to the repo, or *None* if not found anywhere. + Absolute path to the repo, or ``None`` if not found. """ # 1. CLI override cli_path = self._cli_overrides.get(repo_name) if cli_path: - p = Path(cli_path).resolve() + path = Path(cli_path).resolve() - if p.is_dir(): - return p + if path.is_dir(): + return path # 2. Environment variable env_var = ENV_PATH_VARS.get(repo_name) if env_var: - env_val = os.environ.get(env_var) + env_value = os.environ.get(env_var) - if env_val: - p = Path(env_val).resolve() + if env_value: + path = Path(env_value).resolve() - if p.is_dir(): - return p + if path.is_dir(): + return path # 3. Config path override - repo_cfg = self._config.get( - "repos", - {}, - ).get(repo_name, {}) - + repo_cfg = self._config.get("repos", {}).get(repo_name, {}) config_path = repo_cfg.get("path") if config_path: - p = Path(config_path).resolve() + path = Path(config_path).resolve() - if p.is_dir(): - return p + if path.is_dir(): + return path # 4. Cached clone cached = self.cache_dir / repo_name @@ -277,17 +271,19 @@ def get_repo_path( if sibling.is_dir(): return sibling - # 6. Legacy embedded core// + # 6. Legacy embedded core// fallback if project_dir: core_path = project_dir / "core" / repo_name if core_path.is_dir(): warnings.warn( f"Using embedded core/{repo_name}/ is deprecated. " - f"Run 'ebuild setup' to clone repos to ~/.ebuild/repos/.", + "Run 'ebuild setup' to clone repos to " + "~/.ebuild/repos/.", DeprecationWarning, stacklevel=2, ) + return core_path return None @@ -298,33 +294,19 @@ def is_available( project_dir: Optional[Path] = None, ) -> bool: """Check if *repo_name* is resolvable anywhere.""" - return ( - self.get_repo_path( - repo_name, - project_dir, - ) - is not None - ) + return self.get_repo_path(repo_name, project_dir) is not None # ------------------------------------------------------------------ # Update # ------------------------------------------------------------------ - def update( - self, - repo_name: Optional[str] = None, - ) -> Dict[str, str]: + def update(self, repo_name: Optional[str] = None) -> Dict[str, str]: """Git pull latest for one or all repos. Returns: - Dict mapping repo name to result string (e.g. ``"updated"``). + Dict mapping repo name to result string. """ - names = ( - [repo_name] - if repo_name - else list(KNOWN_REPOS) - ) - + names = [repo_name] if repo_name else list(KNOWN_REPOS) results: Dict[str, str] = {} for name in names: @@ -334,10 +316,7 @@ def update( results[name] = "not cloned" continue - repo_cfg = self._config.get( - "repos", - {}, - ).get(name, {}) + repo_cfg = self._config.get("repos", {}).get(name, {}) if repo_cfg.get("path"): results[name] = "linked (skipped)" @@ -359,10 +338,8 @@ def update( results[name] = "updated" - except subprocess.CalledProcessError as e: - results[name] = ( - f"failed: {e.stderr.strip()}" - ) + except subprocess.CalledProcessError as error: + results[name] = f"failed: {error.stderr.strip()}" return results @@ -371,15 +348,11 @@ def update( # ------------------------------------------------------------------ def status(self) -> List[Dict[str, Any]]: - """Return status info for all known repos.""" + """Return status information for all known repositories.""" entries: List[Dict[str, Any]] = [] for name in KNOWN_REPOS: - repo_cfg = self._config.get( - "repos", - {}, - ).get(name, {}) - + repo_cfg = self._config.get("repos", {}).get(name, {}) cached = self.cache_dir / name info: Dict[str, Any] = { @@ -398,14 +371,9 @@ def status(self) -> List[Dict[str, Any]]: ), } - # Get current git branch/commit if cloned if cached.is_dir(): - info["git_branch"] = ( - self._git_current_branch(cached) - ) - info["git_commit"] = ( - self._git_head_commit(cached) - ) + info["git_branch"] = self._git_current_branch(cached) + info["git_commit"] = self._git_head_commit(cached) entries.append(info) @@ -420,20 +388,23 @@ def link( repo_name: str, local_path: str, ) -> None: - """Register a local path override (no symlink — just config entry).""" - p = Path(local_path).resolve() + """Register a local path override.""" + path = Path(local_path).resolve() - if not p.is_dir(): + if not path.is_dir(): raise FileNotFoundError( - f"Path does not exist: {p}" + f"Path does not exist: {path}" ) repo_cfg = self._config.setdefault( "repos", {}, - ).setdefault(repo_name, {}) + ).setdefault( + repo_name, + {}, + ) - repo_cfg["path"] = str(p) + repo_cfg["path"] = str(path) self.save_config() def unlink(self, repo_name: str) -> None: @@ -441,7 +412,10 @@ def unlink(self, repo_name: str) -> None: repo_cfg = self._config.get( "repos", {}, - ).get(repo_name, {}) + ).get( + repo_name, + {}, + ) repo_cfg.pop("path", None) self.save_config() @@ -455,11 +429,14 @@ def set_url( repo_name: str, url: str, ) -> None: - """Change the git URL for a repo.""" + """Change the git URL for a repository.""" repo_cfg = self._config.setdefault( "repos", {}, - ).setdefault(repo_name, {}) + ).setdefault( + repo_name, + {}, + ) repo_cfg["url"] = url self.save_config() @@ -469,11 +446,14 @@ def set_branch( repo_name: str, branch: str, ) -> None: - """Change the branch/tag for a repo.""" + """Change the branch/tag for a repository.""" repo_cfg = self._config.setdefault( "repos", {}, - ).setdefault(repo_name, {}) + ).setdefault( + repo_name, + {}, + ) repo_cfg["branch"] = branch self.save_config() @@ -484,6 +464,7 @@ def set_branch( @staticmethod def _default_url(repo_name: str) -> str: + """Return the default repository URL.""" if repo_name == "eos": return DEFAULT_EOS_REPO_URL @@ -499,30 +480,20 @@ def _git_clone( branch: Optional[str], shallow: bool, ) -> None: + """Clone a repository. + + When ``branch`` is ``None``, no ``--branch`` option is supplied. + Git then checks out the remote repository's default branch. + """ cmd = ["git", "clone"] if shallow: - cmd.extend( - [ - "--depth", - "1", - ] - ) + cmd.extend(["--depth", "1"]) if branch: - cmd.extend( - [ - "--branch", - branch, - ] - ) + cmd.extend(["--branch", branch]) - cmd.extend( - [ - url, - str(dest), - ] - ) + cmd.extend([url, str(dest)]) result = subprocess.run( cmd, @@ -532,8 +503,7 @@ def _git_clone( if result.returncode != 0: raise RuntimeError( - f"Failed to clone {url}: " - f"{result.stderr.strip()}" + f"Failed to clone {url}: {result.stderr.strip()}" ) @staticmethod @@ -541,54 +511,57 @@ def _checkout_branch( repo_dir: Path, branch: Optional[str], ) -> None: + """Checkout a configured branch in an existing repository. + + If no branch is configured, this method intentionally does nothing. + """ if not branch: return - current = DepsManager._git_current_branch( - repo_dir + current = DepsManager._git_current_branch(repo_dir) + + if current == branch: + return + + fetch = subprocess.run( + [ + "git", + "-C", + str(repo_dir), + "fetch", + "--all", + ], + capture_output=True, + text=True, ) - if current != branch: - fetch = subprocess.run( - [ - "git", - "-C", - str(repo_dir), - "fetch", - "--all", - ], - capture_output=True, - text=True, + if fetch.returncode != 0: + raise RuntimeError( + f"Failed to fetch {repo_dir}: " + f"{fetch.stderr.strip()}" ) - if fetch.returncode != 0: - raise RuntimeError( - f"Failed to fetch {repo_dir}: " - f"{fetch.stderr.strip()}" - ) + checkout = subprocess.run( + [ + "git", + "-C", + str(repo_dir), + "checkout", + branch, + ], + capture_output=True, + text=True, + ) - checkout = subprocess.run( - [ - "git", - "-C", - str(repo_dir), - "checkout", - branch, - ], - capture_output=True, - text=True, + if checkout.returncode != 0: + raise RuntimeError( + f"Failed to checkout {branch}: " + f"{checkout.stderr.strip()}" ) - if checkout.returncode != 0: - raise RuntimeError( - f"Failed to checkout {branch}: " - f"{checkout.stderr.strip()}" - ) - @staticmethod - def _git_current_branch( - repo_dir: Path, - ) -> str: + def _git_current_branch(repo_dir: Path) -> str: + """Return the current git branch name.""" try: result = subprocess.run( [ @@ -613,9 +586,8 @@ def _git_current_branch( return "(unknown)" @staticmethod - def _git_head_commit( - repo_dir: Path, - ) -> str: + def _git_head_commit(repo_dir: Path) -> str: + """Return the abbreviated current git commit.""" try: result = subprocess.run( [ From d8259bb372926769ff003fd2ac5053cc24b824cb Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 07:33:48 +0100 Subject: [PATCH 14/26] Update manager.py From b96024a8899e07d0821232cde97696946e4d7658 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 08:40:24 +0100 Subject: [PATCH 15/26] Update test_deps_manager.py --- tests/ebuild/test_deps_manager.py | 163 ++++++++++++++++++++++++------ 1 file changed, 131 insertions(+), 32 deletions(-) diff --git a/tests/ebuild/test_deps_manager.py b/tests/ebuild/test_deps_manager.py index 58949d8..f78d911 100644 --- a/tests/ebuild/test_deps_manager.py +++ b/tests/ebuild/test_deps_manager.py @@ -1,9 +1,10 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2026 EoS Project -"""Host tests for DepsManager sibling path resolution.""" +"""Host tests for DepsManager.""" from pathlib import Path +from unittest.mock import Mock import pytest @@ -13,17 +14,39 @@ @pytest.fixture def isolated_deps(tmp_path, monkeypatch): - """Keep get_repo_path off the real ~/.ebuild cache and config.""" + """Keep DepsManager tests away from the real ~/.ebuild directory.""" home = tmp_path / "ebuild-home" repos = home / "repos" config = home / "config.yaml" - monkeypatch.setattr("ebuild.deps.manager.ensure_ebuild_home", lambda: home) - monkeypatch.setattr("ebuild.deps.manager.EBUILD_CONFIG_PATH", config) - monkeypatch.setenv("EBUILD_REPOS_DIR", str(repos)) - monkeypatch.delenv("EBUILD_EOS_PATH", raising=False) - monkeypatch.delenv("EBUILD_EBOOT_PATH", raising=False) + + monkeypatch.setattr( + "ebuild.deps.manager.ensure_ebuild_home", + lambda: home, + ) + + monkeypatch.setattr( + "ebuild.deps.manager.EBUILD_CONFIG_PATH", + config, + ) + + monkeypatch.setenv( + "EBUILD_REPOS_DIR", + str(repos), + ) + + monkeypatch.delenv( + "EBUILD_EOS_PATH", + raising=False, + ) + + monkeypatch.delenv( + "EBUILD_EBOOT_PATH", + raising=False, + ) + home.mkdir() repos.mkdir() + return tmp_path @@ -33,69 +56,145 @@ def _case_sensitive_is_dir(self: Path) -> bool: names = {child.name for child in self.parent.iterdir()} except OSError: return False + return self.name in names def test_eboot_aliases_include_github_casing(): + """eBoot should be recognized as an eboot sibling directory.""" assert SIBLING_DIR_NAMES["eboot"] == ("eboot", "eBoot") -def test_sibling_eboot_resolves_camel_case(isolated_deps, monkeypatch): +def test_sibling_eboot_resolves_camel_case( + isolated_deps, + monkeypatch, +): + """Resolve an eBoot sibling directory.""" workspace = isolated_deps / "ws" project = workspace / "ebuild" camel = workspace / "eBoot" + project.mkdir(parents=True) camel.mkdir() - monkeypatch.setattr(Path, "is_dir", _case_sensitive_is_dir) - resolved = DepsManager().get_repo_path("eboot", project_dir=project) + monkeypatch.setattr( + Path, + "is_dir", + _case_sensitive_is_dir, + ) + + resolved = DepsManager().get_repo_path( + "eboot", + project_dir=project, + ) assert resolved is not None assert resolved.name == "eBoot" -def test_sibling_eboot_still_resolves_lowercase(isolated_deps, monkeypatch): +def test_sibling_eboot_still_resolves_lowercase( + isolated_deps, + monkeypatch, +): + """Resolve the normal lowercase eboot sibling directory.""" workspace = isolated_deps / "ws" project = workspace / "ebuild" lower = workspace / "eboot" + project.mkdir(parents=True) lower.mkdir() - monkeypatch.setattr(Path, "is_dir", _case_sensitive_is_dir) - resolved = DepsManager().get_repo_path("eboot", project_dir=project) + monkeypatch.setattr( + Path, + "is_dir", + _case_sensitive_is_dir, + ) + + resolved = DepsManager().get_repo_path( + "eboot", + project_dir=project, + ) assert resolved is not None assert resolved.name == "eboot" -def test_sibling_eboot_missing_returns_none(isolated_deps, monkeypatch): +def test_sibling_eboot_missing_returns_none( + isolated_deps, + monkeypatch, +): + """Return None when no eboot sibling exists.""" workspace = isolated_deps / "ws" project = workspace / "ebuild" + project.mkdir(parents=True) - monkeypatch.setattr(Path, "is_dir", _case_sensitive_is_dir) - resolved = DepsManager().get_repo_path("eboot", project_dir=project) + monkeypatch.setattr( + Path, + "is_dir", + _case_sensitive_is_dir, + ) + + resolved = DepsManager().get_repo_path( + "eboot", + project_dir=project, + ) assert resolved is None -def test_setters_do_not_mutate_module_defaults(isolated_deps): - """In-place config edits must not leak into the shared DEFAULT_CONFIG. +def test_setters_do_not_mutate_module_defaults( + isolated_deps, +): + """Config changes must not mutate DEFAULT_CONFIG.""" + manager = DepsManager() - load_config() used to alias the nested default repo dicts instead of - copying them, so set_branch() on one DepsManager rewrote the module-level - template — and every later instance in the process inherited the edit as - its 'default'. - """ - mgr = DepsManager() - mgr.set_branch("eos", "dev") - mgr.set_url("eboot", "https://example.invalid/eboot.git") + manager.set_branch("eos", "dev") + manager.set_url( + "eboot", + "https://example.invalid/eboot.git", + ) assert DEFAULT_CONFIG["repos"]["eos"]["branch"] == "master" - assert DEFAULT_CONFIG["repos"]["eboot"]["url"].endswith("/eBoot.git") + assert DEFAULT_CONFIG["repos"]["eboot"]["url"].endswith( + "/eBoot.git" + ) + + +def test_setup_uses_remote_default_branch_when_unconfigured( + isolated_deps, + monkeypatch, +): + """Do not pass --branch when no branch is configured.""" + manager = DepsManager() + + manager._config["repos"]["eos"] = { + "url": "https://example.invalid/eos.git", + } + + clone_result = Mock() + clone_result.returncode = 0 + clone_result.stderr = "" + + mock_run = Mock(return_value=clone_result) + + monkeypatch.setattr( + "ebuild.deps.manager.subprocess.run", + mock_run, + ) + + destination = manager.setup( + "eos", + shallow=False, + ) + + assert destination == manager.cache_dir / "eos" + + mock_run.assert_called_once() + + clone_command = mock_run.call_args.args[0] - # A fresh instance starting from a clean config must not see them either. - fresh = DepsManager() - fresh._config = fresh.load_config() - assert fresh.config["repos"]["eos"]["branch"] == "dev" # persisted for us - assert DEFAULT_CONFIG["repos"]["eos"]["branch"] == "master" # not for all + assert clone_command[:2] == ["git", "clone"] + assert "https://example.invalid/eos.git" in clone_command + assert "--branch" not in clone_command + assert "master" not in clone_command From 859e52c316ae8fa67ada0f1a2c15689ceefdea9e Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 08:42:51 +0100 Subject: [PATCH 16/26] Update test_deps_manager.py --- tests/ebuild/test_deps_manager.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/tests/ebuild/test_deps_manager.py b/tests/ebuild/test_deps_manager.py index f78d911..0de2492 100644 --- a/tests/ebuild/test_deps_manager.py +++ b/tests/ebuild/test_deps_manager.py @@ -1,7 +1,7 @@ # SPDX-License-Identifier: MIT # Copyright (c) 2026 EoS Project -"""Host tests for DepsManager.""" +"""Tests for DepsManager.""" from pathlib import Path from unittest.mock import Mock @@ -14,7 +14,7 @@ @pytest.fixture def isolated_deps(tmp_path, monkeypatch): - """Keep DepsManager tests away from the real ~/.ebuild directory.""" + """Keep tests away from the real ~/.ebuild directory.""" home = tmp_path / "ebuild-home" repos = home / "repos" config = home / "config.yaml" @@ -23,26 +23,13 @@ def isolated_deps(tmp_path, monkeypatch): "ebuild.deps.manager.ensure_ebuild_home", lambda: home, ) - monkeypatch.setattr( "ebuild.deps.manager.EBUILD_CONFIG_PATH", config, ) - - monkeypatch.setenv( - "EBUILD_REPOS_DIR", - str(repos), - ) - - monkeypatch.delenv( - "EBUILD_EOS_PATH", - raising=False, - ) - - monkeypatch.delenv( - "EBUILD_EBOOT_PATH", - raising=False, - ) + monkeypatch.setenv("EBUILD_REPOS_DIR", str(repos)) + monkeypatch.delenv("EBUILD_EOS_PATH", raising=False) + monkeypatch.delenv("EBUILD_EBOOT_PATH", raising=False) home.mkdir() repos.mkdir() @@ -61,7 +48,7 @@ def _case_sensitive_is_dir(self: Path) -> bool: def test_eboot_aliases_include_github_casing(): - """eBoot should be recognized as an eboot sibling directory.""" + """eBoot should be recognized as an eboot sibling.""" assert SIBLING_DIR_NAMES["eboot"] == ("eboot", "eBoot") @@ -96,7 +83,7 @@ def test_sibling_eboot_still_resolves_lowercase( isolated_deps, monkeypatch, ): - """Resolve the normal lowercase eboot sibling directory.""" + """Resolve the lowercase eboot sibling directory.""" workspace = isolated_deps / "ws" project = workspace / "ebuild" lower = workspace / "eboot" @@ -189,7 +176,6 @@ def test_setup_uses_remote_default_branch_when_unconfigured( ) assert destination == manager.cache_dir / "eos" - mock_run.assert_called_once() clone_command = mock_run.call_args.args[0] From 9471325572a06b72c9c77d0d30bd111e3aa15b56 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 08:47:05 +0100 Subject: [PATCH 17/26] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba0175..33572d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ `cjson` (v1.7.18), `nanopb` (v0.4.9.1), `lvgl` (v9.2.2), `tinyusb` (v0.18.0), and `unity` (v2.6.1). ### Fixed +- **Dependency repositories now use their remote default branch when no branch is configured.** ebuild no longer assumes `master` when cloning a dependency without an explicit branch. This allows custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). - **`ebuild test` now finds Windows test binaries.** The Ninja edge for a native `type: test` target already carried the platform suffix (`_exe_suffix()` names it `.exe` on Windows), but `ebuild test` From d1ed101a617b68d833d3a1ce9cb0f6964f4e72e3 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 08:49:40 +0100 Subject: [PATCH 18/26] Update CHANGELOG.md docs(ebuild): document default branch resolution fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33572d2..6aaf78a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ ### Fixed - **Dependency repositories now use their remote default branch when no branch is configured.** ebuild no longer assumes `master` when cloning a dependency without an explicit branch. This allows custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). +- **Dependency repositories now use their remote default branch when no branch is configured.** ebuild no longer assumes `master` when cloning a dependency without an explicit branch. This allows custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). - **`ebuild test` now finds Windows test binaries.** The Ninja edge for a native `type: test` target already carried the platform suffix (`_exe_suffix()` names it `.exe` on Windows), but `ebuild test` From a75f9961369dd081ac0b1da3fba043c2d47cbad1 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 08:50:59 +0100 Subject: [PATCH 19/26] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aaf78a..10b697d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ `cjson` (v1.7.18), `nanopb` (v0.4.9.1), `lvgl` (v9.2.2), `tinyusb` (v0.18.0), and `unity` (v2.6.1). ### Fixed + * Dependency repositories now use their remote default branch when no branch is configured. ebuild no longer assumes `master` when cloning a dependency without an explicit branch, allowing custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). - **Dependency repositories now use their remote default branch when no branch is configured.** ebuild no longer assumes `master` when cloning a dependency without an explicit branch. This allows custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). - **Dependency repositories now use their remote default branch when no branch is configured.** ebuild no longer assumes `master` when cloning a dependency without an explicit branch. This allows custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). - **`ebuild test` now finds Windows test binaries.** The Ninja edge for a From aa163db95db55f73b9641d627437050e56120ef1 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 08:52:23 +0100 Subject: [PATCH 20/26] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10b697d..55c655c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ `cjson` (v1.7.18), `nanopb` (v0.4.9.1), `lvgl` (v9.2.2), `tinyusb` (v0.18.0), and `unity` (v2.6.1). ### Fixed +- **Dependency repositories now use their remote default branch when no branch is configured.** ebuild no longer assumes `master` when cloning a dependency without an explicit branch, allowing custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). * Dependency repositories now use their remote default branch when no branch is configured. ebuild no longer assumes `master` when cloning a dependency without an explicit branch, allowing custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). - **Dependency repositories now use their remote default branch when no branch is configured.** ebuild no longer assumes `master` when cloning a dependency without an explicit branch. This allows custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). - **Dependency repositories now use their remote default branch when no branch is configured.** ebuild no longer assumes `master` when cloning a dependency without an explicit branch. This allows custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). From 5ad733cd47bfe5ca39fe79bed9f10278ddc880d6 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 09:00:23 +0100 Subject: [PATCH 21/26] Update CHANGELOG.md --- CHANGELOG.md | 179 +++++++-------------------------------------------- 1 file changed, 24 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55c655c..cff446c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,176 +1,45 @@ # Changelog -## [Unreleased] - -### Added -- **Remote Package Index & Synchronization (`ebuild/packages/index_sync.py`).** - Downloads and validates central/mirror package repository indices into a local cache - (`~/.ebuild/index/`), caching full recipe definitions. Enforces HTTPS transport, - path-traversal sanitization (`^[a-zA-Z0-9_-]+$`), 10s socket timeouts, and 10MB response - size limits. Index synchronization supports air-gapped operation via `--offline` and `EBUILD_OFFLINE=1`; package archive fetching is not yet offline-gated. -- **Package Discovery & Multi-Source Search (`ebuild search`, `ebuild/packages/repository.py`).** - Search across local project recipes, system-shipped recipes, and cached remote indices. - Supports `--all`, `--json`, `--build-system`, and `--license` filters. -- **Index Synchronization Command (`ebuild update-index`).** - CLI command to refresh local package and recipe index caches from remote repositories. -- **Source-Ranked Recipe Precedence (`ebuild/packages/registry.py`).** - Enforces strict 3-tier precedence hierarchy during package resolution: project-local recipes (`./recipes/`) > system-shipped recipes (`recipes/`) > cached remote index recipes (`~/.ebuild/index/recipes/`), guaranteeing reproducible builds (§9.2) and ensuring project-level pins override remote definitions. -- **Stale Cached Recipe Pruning (`ebuild/packages/index_sync.py`, `ebuild update-index`).** - `ebuild update-index` automatically prunes stale cached `.yaml` and `.yml` recipes from `~/.ebuild/index/recipes/` that are absent from the newly synchronized remote package index. An index that yields no usable entries is treated as a delivery fault: nothing is pruned, the cached index is left in place, and the condition is reported. Surfaced the count of pruned recipes in CLI output and returned `SyncResult`. -- **Expanded Shipped Recipes Catalog (`recipes/`).** - Added 5 verified recipes with HTTPS release pins and SHA-256 integrity digests: - `cjson` (v1.7.18), `nanopb` (v0.4.9.1), `lvgl` (v9.2.2), `tinyusb` (v0.18.0), and `unity` (v2.6.1). - -### Fixed -- **Dependency repositories now use their remote default branch when no branch is configured.** ebuild no longer assumes `master` when cloning a dependency without an explicit branch, allowing custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). - * Dependency repositories now use their remote default branch when no branch is configured. ebuild no longer assumes `master` when cloning a dependency without an explicit branch, allowing custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). -- **Dependency repositories now use their remote default branch when no branch is configured.** ebuild no longer assumes `master` when cloning a dependency without an explicit branch. This allows custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). -- **Dependency repositories now use their remote default branch when no branch is configured.** ebuild no longer assumes `master` when cloning a dependency without an explicit branch. This allows custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly (`ebuild/deps/manager.py`). -- **`ebuild test` now finds Windows test binaries.** The Ninja edge for a - native `type: test` target already carried the platform suffix - (`_exe_suffix()` names it `.exe` on Windows), but `ebuild test` - rebuilt the path itself without that suffix, asked ninja to build - ``, and then looked for that unsuffixed path. Ninja reported an - unknown target and the runner reported "built, but no binary". Both - steps now go through the same `executable_output_path()` the edge itself - uses (`ebuild/build/ninja_backend.py`, `ebuild/cli/commands.py`). -- **`ebuild package` now finds the Windows binary too.** It looked up - `/` directly instead of through `executable_output_path()`, - so on Windows it reported "No built artifact" after a build that had - succeeded. Now uses the same helper as `ebuild test` - (`ebuild/cli/commands.py`). -- **`ebuild build`'s flash/RAM report went silent on Windows.** - `_report_footprint` also looked up `/` directly, so on - Windows the artifact was never found and the function returned with no - diagnostic — the report just never appeared, with no indication it was - skipped rather than not applicable. Now uses `executable_output_path()` - and logs at debug level when it has nothing to measure - (`ebuild/cli/commands.py`). -- **`pytest` collection no longer aborts on Windows without gcc.** - `tests/ebuild/test_build_dir_resolution.py` evaluated - `subprocess.run(["gcc", "--version"])` in a `skipif`. When gcc is not - installed, Windows raises `FileNotFoundError` instead of a non-zero - returncode, which pytest treats as a collection ERROR and stops the - suite. The probe now uses `shutil.which` and catches `OSError`. -- **A path containing a space produced a silently wrong `build.ninja`.** Paths - were written into build statements unescaped, but Ninja ends the output list - at the first unescaped `:` and splits on unescaped spaces. A build directory - under `C:\Users\Jane Doe\` parsed into four targets instead of one, so - `ebuild build` failed with `expected build command name` or built the wrong - thing. `$`, spaces and `:` are now escaped in generated build statements - (`ebuild/build/ninja_backend.py`). -- **`NinjaBackend` could not generate anything.** `_object_path()` is called by - both `_write_ninja()` and `_write_compile_commands()`, but the method itself - was dropped in a merge, so every `generate()` raised - `AttributeError: 'NinjaBackend' object has no attribute '_object_path'`. - The method and its regression tests are restored: objects are named - `obj//.o`, so a source listed by two targets compiles once - per target instead of both targets claiming one output — which ninja rejects - with `multiple rules generate ...` (`ebuild/build/ninja_backend.py`). -- **`ebuild.build.dispatch` was unimportable.** Two branches independently added - an unhandled-backend `else` clause to `BackendDispatcher.configure()`; the - merge kept both, leaving a second `else` after the first and a `SyntaxError` - that broke every command importing the module. The duplicate is removed and - the two clauses are consolidated into one - (`ebuild/build/dispatch.py`). -- **`configure(backend="ninja")` no longer silently succeeds.** `ebuild build` - routes `backend: ninja` with no `targets` into the dispatcher, which has no - ninja configure step; the no-op let the CLI report success having built - nothing. It now raises with a message naming the missing `targets` - (`ebuild/build/dispatch.py`). -- **Unhandled-backend errors no longer contradict themselves.** The message - listed `ALL_BACKENDS` as supported, which includes `ninja` — the very backend - being rejected. Each step now reports only the backends it handles - (`ebuild/build/dispatch.py`). -- **Ninja backend: header changes now trigger a rebuild.** The generated `cc` - rule declared no depfile, so Ninja only knew about the sources listed in - `build.yaml`. Editing a header left stale object files in place and the build - reported success. The rule now compiles with `-MMD -MF $out.d` and declares - `depfile`/`deps`, so Ninja tracks the real include graph - (`ebuild/build/ninja_backend.py`). -- **Package registry: versions that are not purely numeric no longer raise.** - Version ordering parsed every dot-separated component with `int()`, so a - recipe declaring `v2.9.3` -- the upstream tag form `recipes/littlefs.yaml` - already downloads -- made `ebuild list-packages` fail with `ValueError`. - Prereleases (`3.6.0-rc1`), distribution revisions (`1.2.13-1`) and build - metadata (`1.0.0+build2`) failed the same way. It also replaced the - resolver's actionable "package not found" error, which enumerates the - registry, with a traceback. Dot-separated integers keep their numeric - ordering; anything else is ranked below every numeric version and ordered - lexicographically rather than guessed at (`ebuild/packages/registry.py`). -- **Declared targets are no longer overridden by backend auto-detection.** - Backend detection inspects only the filesystem, so a `Makefile` kept for - `make flash`, or a `CMakeLists.txt` belonging to one subcomponent, won over - a `build.yaml` that declared its own `targets:`. The external tool ran, none - of the declared targets were built, no build directory was produced, and - `ebuild build` still reported "Build completed successfully" with exit code - 0. When the backend was auto-detected and targets are declared, the ninja - backend is now used and the choice is logged. An explicit `backend:` in - `build.yaml` or `--backend` still takes precedence (`ebuild/cli/commands.py`). -- **A relative `--build-dir` is now anchored to the project.** ebuild created - and reported the build directory relative to the process working directory, - while the ninja it launched read the same relative path from - `cfg.source_dir`. The two agree only when the working directory is the - project directory, so `ebuild build --config sub/build.yaml` failed with - "ninja: error: loading '_build/build.ninja': No such file or directory" one - line after reporting that it generated that file, and `ebuild configure` - reported success having written it where a later build would not look. A - relative `--build-dir` now resolves against the directory containing - `build.yaml`, as an absolute path, so both sides agree regardless of the - working directory (`ebuild/cli/commands.py`). - -### Added -- `ebuild.build.dispatch.UnknownBackendError`, raised for a backend a dispatch - step does not handle. It derives from both `ValueError` and `RuntimeError` - because the clauses it replaces raised one each and callers depend on both — - notably the CLI's `except RuntimeError`, which turns this into a clean - `exit 1` rather than a traceback. New code should catch - `UnknownBackendError`. - -## [3.0.1] - 2026-05-16 - -### Production Release — Unified EmbeddedOS-org v3.0.1 +### Production Release — Unified EmbeddedOS-org v3.0.0 -This is the synchronized production release across all 18 EmbeddedOS-org repos. +This is the synchronized production release across all 18 EmbeddedOS-org repositories. -- Refreshed governance: LICENSE, NOTICE, CITATION.cff, SECURITY.md -- CI/CD pipelines hardened: release.yml, book-build.yml, video-build.yml, deploy-pages.yml -- Release artifacts produced for: Linux x64/arm64, macOS x64/arm64, Windows x64, Docker, plus per-repo embedded/mobile/extension targets +- Refreshed governance files: `LICENSE`, `NOTICE`, `CITATION.cff`, and `SECURITY.md` +- Hardened CI/CD pipelines: `release.yml`, `book-build.yml`, `video-build.yml`, and `deploy-pages.yml` +- Release artifacts produced for Linux x64/arm64, macOS x64/arm64, Windows x64, Docker, and per-repository embedded, mobile, and extension targets - mdBook documentation built and deployed to GitHub Pages -- Promo video rendered and attached as a release asset +- Promotional video rendered and attached as a release asset -## [3.0.0] - 2026-05-13 - -### Production Release — Unified EmbeddedOS-org v3.0.0 +All notable changes to this project are documented in this file. -This is the synchronized production release across all 18 EmbeddedOS-org repos. +The format is based on +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). -- Refreshed governance: LICENSE, NOTICE, CITATION.cff, SECURITY.md -- CI/CD pipelines hardened: release.yml, book-build.yml, video-build.yml, deploy-pages.yml -- Release artifacts produced for: Linux x64/arm64, macOS x64/arm64, Windows x64, Docker, plus per-repo embedded/mobile/extension targets -- mdBook documentation built and deployed to GitHub Pages -- Promo video rendered and attached as a release asset +## [Unreleased] -All notable changes to this project will be documented in this file. +### Fixed -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +- Dependency repositories now respect their remote default branch when no branch is configured. ebuild no longer assumes `master` for unconfigured dependency repositories. When no branch is specified, Git uses the dependency repository's own default branch, allowing custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly. ## [0.1.0] - 2026-03-31 ### Added + - Initial release of ebuild -- Unified monorepo build system for EoS ecosystem -- 18 CLI commands (build, clean, flash, test, analyze, sdk, release, etc.) +- Unified monorepo build system for the EmbeddedOS ecosystem +- 18 CLI commands, including build, clean, flash, test, analyze, sdk, and release - Yocto-style SDK generation for 14 targets -- Deliverable packager (ZIP per target + manifest.json) -- Hardware analyzer (KiCad/YAML schematic parsing) -- Gated release pipeline (all repos must pass) -- Optional layer integration (eai, eni, eipc) -- Cross-compilation for aarch64, arm, riscv64 +- Deliverable packager with ZIP packages per target and `manifest.json` +- Hardware analyzer with KiCad/YAML schematic parsing +- Gated release pipeline requiring all repositories to pass +- Optional layer integration for `eai`, `eni`, and `eipc` +- Cross-compilation support for `aarch64`, `arm`, and `riscv64` - Complete CI/CD pipeline with nightly, weekly, EoSim sanity, and simulation test runs -- Full cross-platform support (Linux, Windows, macOS) +- Full cross-platform support for Linux, Windows, and macOS - ISO/IEC standards compliance documentation - MIT license +[Unreleased]: https://github.com/embeddedos-org/ebuild/compare/v0.1.0...HEAD [0.1.0]: https://github.com/embeddedos-org/ebuild/releases/tag/v0.1.0 From 9e3d504fb9e4b29541380ea541df6714399a7b0c Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 09:02:01 +0100 Subject: [PATCH 22/26] Update CHANGELOG.md --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cff446c..6b3276c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,14 +21,14 @@ and this project adheres to ### Fixed -- Dependency repositories now respect their remote default branch when no branch is configured. ebuild no longer assumes `master` for unconfigured dependency repositories. When no branch is specified, Git uses the dependency repository's own default branch, allowing custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly. +- **Dependency repositories now respect their remote default branch when no branch is configured.** ebuild no longer assumes `master` for unconfigured dependency repositories. When no branch is specified, Git uses the dependency repository's own default branch, allowing custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly. ## [0.1.0] - 2026-03-31 ### Added - Initial release of ebuild -- Unified monorepo build system for the EmbeddedOS ecosystem +- Unified monorepo build system for the EoS ecosystem - 18 CLI commands, including build, clean, flash, test, analyze, sdk, and release - Yocto-style SDK generation for 14 targets - Deliverable packager with ZIP packages per target and `manifest.json` From e070f14aa1695627b6c64971625170bec9e357be Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 09:04:10 +0100 Subject: [PATCH 23/26] Update CHANGELOG.md --- CHANGELOG.md | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b3276c..dd1eaf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,16 @@ This is the synchronized production release across all 18 EmbeddedOS-org repositories. -- Refreshed governance files: `LICENSE`, `NOTICE`, `CITATION.cff`, and `SECURITY.md` -- Hardened CI/CD pipelines: `release.yml`, `book-build.yml`, `video-build.yml`, and `deploy-pages.yml` -- Release artifacts produced for Linux x64/arm64, macOS x64/arm64, Windows x64, Docker, and per-repository embedded, mobile, and extension targets +- Refreshed governance: `LICENSE`, `NOTICE`, `CITATION.cff`, `SECURITY.md` +- CI/CD pipelines hardened: `release.yml`, `book-build.yml`, `video-build.yml`, `deploy-pages.yml` +- Release artifacts produced for Linux x64/arm64, macOS x64/arm64, Windows x64, Docker, plus per-repo embedded/mobile/extension targets - mdBook documentation built and deployed to GitHub Pages -- Promotional video rendered and attached as a release asset +- Promo video rendered and attached as a release asset -All notable changes to this project are documented in this file. +All notable changes to this project will be documented in this file. -The format is based on -[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to -[Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] @@ -23,21 +21,27 @@ and this project adheres to - **Dependency repositories now respect their remote default branch when no branch is configured.** ebuild no longer assumes `master` for unconfigured dependency repositories. When no branch is specified, Git uses the dependency repository's own default branch, allowing custom and third-party repositories whose default branch is `main` or another branch to be cloned correctly. +- Fixed handling of relative `--build-dir` paths. + +### Added + +- Improved support for dependency repository configuration and Git-based dependency management. + ## [0.1.0] - 2026-03-31 ### Added - Initial release of ebuild -- Unified monorepo build system for the EoS ecosystem -- 18 CLI commands, including build, clean, flash, test, analyze, sdk, and release +- Unified monorepo build system for EoS ecosystem +- 18 CLI commands (build, clean, flash, test, analyze, sdk, release, etc.) - Yocto-style SDK generation for 14 targets -- Deliverable packager with ZIP packages per target and `manifest.json` -- Hardware analyzer with KiCad/YAML schematic parsing -- Gated release pipeline requiring all repositories to pass -- Optional layer integration for `eai`, `eni`, and `eipc` -- Cross-compilation support for `aarch64`, `arm`, and `riscv64` +- Deliverable packager (ZIP per target + `manifest.json`) +- Hardware analyzer (KiCad/YAML schematic parsing) +- Gated release pipeline (all repos must pass) +- Optional layer integration (`eai`, `eni`, `eipc`) +- Cross-compilation for `aarch64`, `arm`, `riscv64` - Complete CI/CD pipeline with nightly, weekly, EoSim sanity, and simulation test runs -- Full cross-platform support for Linux, Windows, and macOS +- Full cross-platform support (Linux, Windows, macOS) - ISO/IEC standards compliance documentation - MIT license From 5b3f20242fef644612fea5c56ad74e5876cd27a8 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 09:20:27 +0100 Subject: [PATCH 24/26] Update test_ci_gate.py --- tests/unit/test_ci_gate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_ci_gate.py b/tests/unit/test_ci_gate.py index 964ca54..fda8563 100644 --- a/tests/unit/test_ci_gate.py +++ b/tests/unit/test_ci_gate.py @@ -11,6 +11,8 @@ required check does not cover. """ +import itertools +import re import yaml import pytest from pathlib import Path @@ -211,8 +213,6 @@ def test_gate_fails_on_any_non_success_result(jobs): # check cannot say which of the three it means, and a Windows-only failure is # indistinguishable from the other two legs without opening the run. -import itertools -import re # `include` and `exclude` shape a matrix but are not dimensions of it, so they # are not part of the cartesian product. From 58c64f11f516d3fe35fae8fccfd14e3b1b8b779f Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 09:22:19 +0100 Subject: [PATCH 25/26] Update test_package_recipe.py --- tests/ebuild/test_package_recipe.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/ebuild/test_package_recipe.py b/tests/ebuild/test_package_recipe.py index 38c38de..927515b 100644 --- a/tests/ebuild/test_package_recipe.py +++ b/tests/ebuild/test_package_recipe.py @@ -114,4 +114,5 @@ def test_depends_alias_must_be_a_list(): """ with pytest.raises(RecipeError, match="dependencies"): - load_recipe_from_string(content) \ No newline at end of file + load_recipe_from_string(content) + From c1baf62de2ab8655f23c10c11c9840fabbd5f231 Mon Sep 17 00:00:00 2001 From: Iregbu MichaelVasco Date: Tue, 15 Sep 2026 09:22:37 +0100 Subject: [PATCH 26/26] Update test_build_dir_resolution.py --- tests/ebuild/test_build_dir_resolution.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/ebuild/test_build_dir_resolution.py b/tests/ebuild/test_build_dir_resolution.py index a19be13..e08444f 100644 --- a/tests/ebuild/test_build_dir_resolution.py +++ b/tests/ebuild/test_build_dir_resolution.py @@ -28,7 +28,6 @@ import os import shutil import subprocess -import shutil import textwrap from pathlib import Path from types import SimpleNamespace