From 7c76abed3365f4e1ebc74cf51f3344790873fea3 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sun, 6 Sep 2026 17:18:32 +0200 Subject: [PATCH 1/4] App.find_dir(): Allow restricting the project roots find_dir() and its helpers resolve a project name against the workspace root and /opt. The build iteration needs to resolve a module against the workspace only, to tell buildable projects apart from installed ones. Add a projs_roots parameter to find_dir(), __find_dir() and __proj_dir() that restricts the search to the given roots. With the parameter omitted the previous behavior is unchanged. Signed-off-by: Jan Lindemann Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 --- src/python/jw/pkg/App.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index fe93c585..a9ab6eb6 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -21,7 +21,7 @@ if TYPE_CHECKING: import argparse from argparse import ArgumentParser - from typing import TypeAlias + from typing import Iterable, TypeAlias from .lib.PackageFilter import PackageFilter @@ -77,12 +77,17 @@ class App(Base): raise Exception('Tried to access undefined pretty top directory') return self.___pretty_topdir - def __proj_dir(self, name: str, pretty: bool) -> str | None: + def __proj_dir( + self, + name: str, + pretty: bool, + projs_roots: Iterable[str | None] | None = None, + ) -> str | None: if name == self.__top_name: if pretty: return self.__pretty_topdir return self.__topdir - for d in [self.__projs_root, self.___opt_root]: + for d in projs_roots or [self.__projs_root, self.___opt_root]: if d is None: continue ret = d + '/' + name @@ -99,6 +104,7 @@ class App(Base): search_subdirs: list[str] | None = None, search_absdirs: list[str] | None = None, pretty: bool = True, + projs_roots: Iterable[str | None] | None = None, ) -> str | None: if search_subdirs is None: search_subdirs = [] @@ -134,7 +140,7 @@ class App(Base): f'Tried to pretty-format directory {pd}, not implemented' ) - pd = self.__proj_dir(name, False) + pd = self.__proj_dir(name, pretty = False, projs_roots = projs_roots) if pd is None: return None if not search_subdirs and not search_absdirs: @@ -405,8 +411,9 @@ class App(Base): search_absdirs: list[str] | None = None, pretty: bool = True, throw: bool = False, + projs_roots: Iterable[str | None] | None = None, ) -> str | None: - ret = self.__find_dir(name, search_subdirs, search_absdirs, pretty) + ret = self.__find_dir(name, search_subdirs, search_absdirs, pretty, projs_roots) if ret is not None: return ret if not throw: -- 2.55.0 From a269631e920729e3c78e6410150fa6f753cece44 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Sun, 6 Sep 2026 17:19:06 +0200 Subject: [PATCH 2/4] CmdBuild.run_make(): Skip projects without buildable dir The iteration order contains every project the dependency walk resolved, including projects that are only installed as -devel packages below /opt. Running the target in such a project ran make in a read-only, source-less directory and failed the run. Resolve each module against the workspace root only, and skip the target with a notice when there is no buildable project directory. Factor the skip notice into log_skip() so the platform-exclusion path shares it. Signed-off-by: Jan Lindemann Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 --- src/python/jw/pkg/cmds/projects/CmdBuild.py | 25 ++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/python/jw/pkg/cmds/projects/CmdBuild.py b/src/python/jw/pkg/cmds/projects/CmdBuild.py index 3972bda5..6677e71b 100644 --- a/src/python/jw/pkg/cmds/projects/CmdBuild.py +++ b/src/python/jw/pkg/cmds/projects/CmdBuild.py @@ -94,6 +94,14 @@ class CmdBuild(Cmd): # export @override async def _run(self, args: Namespace) -> None: + @lru_cache(maxsize = None) + def proj_dir(module: str) -> str | None: + return self.app.find_dir( + module, + pretty = False, + projs_roots = [self.app.projs_root], + ) + @lru_cache(maxsize = None) def read_deps(cur: str, dep_flavour: str) -> list[str]: # dep cache doesn't make a difference at all @@ -172,20 +180,27 @@ class CmdBuild(Cmd): # export dep_tree[k].remove(d) return 1 + def log_skip(module: str, msg: str) -> None: + title = f'---- {module}' + log(NOTICE, f',{title} >') + log(NOTICE, f'| {msg}<') + log(NOTICE, f'`{title} <') + async def run_make( module: str, target: str, cur_project: int, num_projects: int ) -> None: patt = self.app.is_excluded_from_build(module) if patt is not None: - title = f'---- {module}' - log(NOTICE, f',{title} >') - log(NOTICE, f'| Configured to skip build on platform >{patt}<') - log(NOTICE, f'`{title} <') + log_skip(module, f'Configured to skip build on platform >{patt}<') + return + + wd = proj_dir(module) + if wd is None: + log_skip(module, 'Skipping: No buildable project directory') return make_cmd = ['make', target] - wd = self.app.find_dir(module, pretty = False) title = '---- [%d/%d]: Running "%s" in %s -' % ( cur_project, num_projects, -- 2.55.0 From 433fe695dbbaafa48cbd34ce943ed0029b5baf28 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Wed, 9 Sep 2026 21:14:25 +0200 Subject: [PATCH 3/4] App.is_installed(): Add method The installed state of a project is judged by the proofs of installation, which differ per subpackage: make/project.conf in the dev tree or /opt proves the -devel package or a buildable checkout, a VERSION file in the project directory or /usr/share/doc/packages the -run package. Add is_installed(), which answers the proof question for a given subpackage by searching the per-subpackage locations. Signed-off-by: Jan Lindemann Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.84.2 --- src/python/jw/pkg/App.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index a9ab6eb6..d5480a7a 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -98,6 +98,29 @@ class App(Base): return None raise Exception('No project path found for module "{}"'.format(name)) + @cache + def __is_installed(self, name: str, devel: bool) -> bool: + # devel: the project is in the dev tree or the -devel + # package is installed: make/project.conf is present. + # run: the -run package is installed: a VERSION file is + # present in the project directory or + # /usr/share/doc/packages. + if devel and name == self.__top_name: + # the topdir is the project's own buildable checkout + if os.path.exists(f'{self.__topdir}/make/project.conf'): + return True + search, file = ( + (self.__projs_root, self.___opt_root), + '/make/project.conf' + ) if devel else ( + (self.__projs_root, '/usr/share/doc/packages'), + '/VERSION' + ) + for root in search: + if root is not None and os.path.exists(f'{root}/{name}{file}'): + return True + return False + def __find_dir( self, name: str, @@ -404,6 +427,18 @@ class App(Base): raise Exception('No distro object') return self.__distro + def is_installed( + self, + name: str, + devel: bool = False, + ) -> bool: + """True if the project is installed: for devel, + make/project.conf is present in the dev tree or /opt; for + run, a VERSION file is present in the project directory + or /usr/share/doc/packages. + """ + return self.__is_installed(name, devel) + def find_dir( self, name: str, -- 2.55.0 From 2ae101d93b338f653685e2ab3bef0020a06de0f1 Mon Sep 17 00:00:00 2001 From: Jan Lindemann Date: Wed, 9 Sep 2026 21:15:39 +0200 Subject: [PATCH 4/4] App.__get_project_refs(): Fix recursion stop conditions App.__get_project_refs() recurses into the package dependency graph, but the recursion end conditions only work well if all required packages are present. Whether a required package is installed or not is decided upon a wrong condition, however - existence of the project's project directory, which may or may not be misleading for both the -devel and the -run requirements. This can lead to various unwanted outcomes: Missing packages happily inserted into the recursion buffer, process termination instead of recursion stop, path lookup errors instead of a clearer "unmet dependency" message (No project path found for module xyz, Failed to find directory of project foo). This commit cleanly detects if -run or -devel are installed and makes the walk stop or raise under the appropriate conditions: 1. -run missing but required, raises unmet dependency 2. -devel missing but required, raises unmet dependency 3. -run present, no -devel required, stop recursing Signed-off-by: Jan Lindemann Assisted-by: unsloth/Qwen3.8-27B-GGUF:Q4_K_M with pi.dev v0.85.1 --- src/python/jw/pkg/App.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/python/jw/pkg/App.py b/src/python/jw/pkg/App.py index d5480a7a..af9b9311 100644 --- a/src/python/jw/pkg/App.py +++ b/src/python/jw/pkg/App.py @@ -210,6 +210,7 @@ class App(Base): names_only: bool, ) -> None: name = self.strip_module_from_spec(spec) + mod = re.split('([=><]+)', spec)[0].strip() if names_only: spec = name if spec in buf: @@ -219,6 +220,15 @@ class App(Base): buf.append(spec) return visited.add(spec) + needed_subpackage = 'devel' if mod.endswith(('-dev', '-devel')) else 'run' + if not self.is_installed(name, devel = True): + if not mod.endswith(('-dev', '-devel')): + if self.is_installed(name, devel = False): + return + raise Exception( + f'Unmet dependency on {mod}: the -{needed_subpackage} package ' + f'of project {name} is not installed' + ) vals = self.get_value(name, section, key) log( DEBUG, -- 2.55.0