From 04373788150627e52a078ada830f22e371a76e9d Mon Sep 17 00:00:00 2001 From: Sergey Fedorov Date: Thu, 23 Jul 2026 01:48:29 +0000 Subject: [PATCH 14/25] minicargo: support virtual workspace roots (kalker, jaq) A Cargo.toml with only a [workspace] section previously failed with "Manifest file ... doesn't specify a package name". Now the workspace members list is recorded, and when the requested directory is such a virtual root, a member is picked and built from its directory instead: - `--package `/`-p` selects a member by package name; - otherwise a sole member, then a member whose package name matches the workspace directory name (with any `-` suffix stripped, as in release tarballs: kalker-2.2.2 -> the `cli` member named `kalker`), then a sole binary member (src/main.rs or [[bin]]); - anything still ambiguous errors with the member list and a hint. Glob members are skipped for auto-selection. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_013wCVW89GjmNyYviEcANPHM --- tools/minicargo/main.cpp | 112 +++++++++++++++++++++++++++++++++++ tools/minicargo/manifest.cpp | 19 +++++- tools/minicargo/manifest.h | 3 + 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/tools/minicargo/main.cpp b/tools/minicargo/main.cpp index e81edcea..36584407 100644 --- a/tools/minicargo/main.cpp +++ b/tools/minicargo/main.cpp @@ -63,6 +63,10 @@ struct ProgramOptions bool no_default_features = false; ::std::vector<::std::string> features; + /// Workspace member (package name) to build when the manifest is a virtual + /// workspace root ([workspace] with no [package]) + const char* package_name = nullptr; + int parse(int argc, const char* argv[]); void usage(::std::ostream& os) const; void help() const; @@ -172,6 +176,106 @@ int main(int argc, const char* argv[]) repo.set_workspace(workspace_manifest); } + // Handle virtual workspace roots: a Cargo.toml with a [workspace] section but no + // [package] (e.g. kalker, jaq). Pick a member and build from its directory instead. + if( workspace_manifest_path.is_valid() && workspace_manifest_path == dir / "Cargo.toml" ) + { + bool has_package = false; + { + TomlFile toml_file(workspace_manifest_path); + for(auto key_val : toml_file) + { + if( key_val.path[0] == "package" ) { + has_package = true; + break; + } + } + } + if( !has_package ) + { + struct Candidate { + ::std::string rel_path; + ::std::string name; + bool has_bin; + }; + ::std::vector candidates; + for(const auto& member : workspace_manifest.members()) + { + if( member.find('*') != ::std::string::npos ) { + // Glob members: not supported for auto-selection + continue; + } + auto member_dir = dir / member.c_str(); + auto member_toml = member_dir / "Cargo.toml"; + if( !::std::ifstream(member_toml.str()).is_open() ) + continue; + Candidate c; + c.rel_path = member; + c.has_bin = ::std::ifstream((member_dir / "src" / "main.rs").str()).is_open(); + TomlFile toml_file(member_toml); + for(auto key_val : toml_file) + { + if( key_val.path.size() == 2 && key_val.path[0] == "package" && key_val.path[1] == "name" ) + c.name = key_val.value.as_string(); + else if( key_val.path[0] == "bin" ) + c.has_bin = true; + } + candidates.push_back(::std::move(c)); + } + + const Candidate* sel = nullptr; + if( opts.package_name ) + { + for(const auto& c : candidates) + if( c.name == opts.package_name ) + { sel = &c; break; } + if( !sel ) + throw ::std::runtime_error(format("Workspace has no member named '", opts.package_name, "' (from --package)")); + } + else if( candidates.size() == 1 ) + { + sel = &candidates.front(); + } + if( !sel ) + { + // Prefer the member whose package name matches the workspace directory + // name (with any `-` suffix removed, as in release tarballs: + // `kalker-2.2.2` -> `kalker`) + auto dir_name = dir.basename(); + for(size_t pos = dir_name.find('-'); pos != ::std::string::npos; pos = dir_name.find('-', pos+1)) + { + if( pos + 1 < dir_name.size() && '0' <= dir_name[pos+1] && dir_name[pos+1] <= '9' ) { + dir_name.resize(pos); + break; + } + } + for(const auto& c : candidates) + if( c.name == dir_name ) + { sel = &c; break; } + } + if( !sel ) + { + // Last resort: a single binary member + for(const auto& c : candidates) + { + if( c.has_bin ) { + if( sel ) { sel = nullptr; break; } + sel = &c; + } + } + } + if( !sel ) + { + ::std::cerr << "Virtual workspace root - unable to pick a member to build. Members:" << ::std::endl; + for(const auto& c : candidates) + ::std::cerr << " - " << c.name << " (" << c.rel_path << (c.has_bin ? ", bin" : "") << ")" << ::std::endl; + throw ::std::runtime_error("Virtual workspace root: pass `--package ` to select a member"); + } + ::std::cout << "Virtual workspace root: building member '" << sel->name << "' (" << sel->rel_path << ")" << ::std::endl; + dir = dir / sel->rel_path.c_str(); + } + } + // 1. Load the Cargo.toml file from the passed directory Debug_SetPhase("Load Root"); auto m = PackageManifest::load_from_toml( dir / "Cargo.toml", &workspace_manifest ); @@ -453,6 +557,13 @@ int ProgramOptions::parse(int argc, const char* argv[]) else if( ::std::strcmp(arg, "--no-default-features") == 0 ) { this->no_default_features = true; } + else if( ::std::strcmp(arg, "--package") == 0 || ::std::strcmp(arg, "-p") == 0 ) { + if(i+1 == argc) { + ::std::cerr << "Flag " << arg << " takes an argument" << ::std::endl; + return 1; + } + this->package_name = argv[++i]; + } else if( ::std::strcmp(arg, "--pause") == 0 ) { this->pause_before_quit = true; } @@ -497,6 +608,7 @@ void ProgramOptions::help() const << "-j : Run at most build tasks at once (default is to run only one)\n" << "-n : Don't build any packages, just list the packages that would be built\n" << "-g : Pass `-g` to compiler\n" + << "--package,-p : Workspace member to build (for virtual workspace roots)\n" << "--no-default-features : \n" << "--features : \n" ; diff --git a/tools/minicargo/manifest.cpp b/tools/minicargo/manifest.cpp index 54453e0c..e7f83e69 100644 --- a/tools/minicargo/manifest.cpp +++ b/tools/minicargo/manifest.cpp @@ -186,9 +186,24 @@ WorkspaceManifest WorkspaceManifest::load_from_toml(const ::helpers::path& works { // Resolver version, doesn't really matter for minicargo? } - else if( key == "members" || key == "exclude" ) + else if( key == "members" ) { - // Ignore members list, for now. + // Recorded to support virtual workspace roots (manifests with only + // a [workspace] section) + if( key_val.path.size() == 2 && key_val.value.m_type == TomlValue::Type::List ) + { + for(const auto& sv : key_val.value.m_sub_values) + { + if( sv.m_type == TomlValue::Type::String ) + { + rv.m_members.push_back(sv.as_string()); + } + } + } + } + else if( key == "exclude" ) + { + // Ignore the exclusion list (only members are candidates here) } else if( key == "package" ) { diff --git a/tools/minicargo/manifest.h b/tools/minicargo/manifest.h index 0a18cce7..f88a173d 100644 --- a/tools/minicargo/manifest.h +++ b/tools/minicargo/manifest.h @@ -321,6 +321,8 @@ class WorkspaceManifest { ::std::map< ::std::string, ::helpers::path> m_patches; + ::std::vector<::std::string> m_members; + Edition m_edition = Edition::Unspec; struct Dependencies { ::std::vector main; @@ -334,6 +336,7 @@ public: static WorkspaceManifest load_from_toml(const ::helpers::path& workspace_manifest_path); Edition edition() const { return m_edition; } + const ::std::vector<::std::string>& members() const { return m_members; } const ::std::map< ::std::string, ::helpers::path>& patches() const { return m_patches; } const ::std::vector& dependencies() const { return m_dependencies.main; } const ::std::vector& build_dependencies() const { return m_dependencies.build; } -- 2.43.0