diff --git a/CHANGELOG.md b/CHANGELOG.md index 468e850..7736bfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,14 @@ > 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。 > 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)。 +## [0.0.38] — 2026-05-31 + +### 新增 + +- 支持包描述拥有自己的 `ldflags`,依赖包声明的链接参数会随包源码编译 + 一起进入最终链接命令,消费方项目不再需要手动补齐第三方 C/C++ + 库的私有链接参数。 + ## [0.0.37] — 2026-05-31 ### 修复 diff --git a/README.md b/README.md index 20f9ce1..1bd251c 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ import mcpplibs.cmdline; - Ninja 后端:自动生成 build.ninja,并行编译 - compile_commands.json 自动生成(clangd / ccls 即用) - C 语言一等支持:`.c` 文件自动检测,混合 C/C++ 项目 -- 用户自定义 cflags / cxxflags / c_standard +- 用户自定义 cflags / cxxflags / ldflags / c_standard diff --git a/docs/05-mcpp-toml.md b/docs/05-mcpp-toml.md index ae53aab..0d1c844 100644 --- a/docs/05-mcpp-toml.md +++ b/docs/05-mcpp-toml.md @@ -73,6 +73,7 @@ include_dirs = ["include", "third_party/include"] # 头文件搜索路径 c_standard = "c11" # C 源文件的标准(默认 c11) cflags = ["-DFOO=1"] # 额外 C 编译参数 cxxflags = ["-DBAR=2"] # 额外 C++ 编译参数 +ldflags = ["-lfoo"] # 额外链接参数 static_stdlib = true # 静态链接 libstdc++(默认 true) ``` diff --git a/mcpp.toml b/mcpp.toml index 98b0924..fb7e5bd 100644 --- a/mcpp.toml +++ b/mcpp.toml @@ -1,6 +1,6 @@ [package] name = "mcpp" -version = "0.0.37" +version = "0.0.38" description = "Modern C++ build & package management tool" license = "Apache-2.0" authors = ["mcpp-community"] diff --git a/src/build/compile_commands.cppm b/src/build/compile_commands.cppm index 5525d43..9d6d881 100644 --- a/src/build/compile_commands.cppm +++ b/src/build/compile_commands.cppm @@ -34,7 +34,8 @@ namespace mcpp::build { namespace { bool is_c_source(const std::filesystem::path& src) { - return src.extension() == ".c"; + auto ext = src.extension(); + return ext == ".c" || ext == ".m"; } // Split a flag string into individual tokens AND un-escape ninja-style diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 18bab52..112c380 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -56,6 +56,26 @@ std::string escape_path(const std::filesystem::path& p) { return out; } +std::string normalize_ldflag(const std::filesystem::path& root, const std::string& flag) { + auto absolute_path = [&](std::string_view raw) { + std::filesystem::path p{std::string(raw)}; + if (p.is_absolute() || raw.starts_with("$")) return p; + return root / p; + }; + + if (flag.starts_with("-L") && flag.size() > 2) { + return "-L" + escape_path(absolute_path(std::string_view(flag).substr(2))); + } + + constexpr std::string_view rpathPrefix = "-Wl,-rpath,"; + if (flag.starts_with(rpathPrefix) && flag.size() > rpathPrefix.size()) { + return std::string(rpathPrefix) + + escape_path(absolute_path(std::string_view(flag).substr(rpathPrefix.size()))); + } + + return flag; +} + } // namespace CompileFlags compute_flags(const BuildPlan& plan) { @@ -192,6 +212,11 @@ CompileFlags compute_flags(const BuildPlan& plan) { }; std::string user_cxxflags = join(plan.manifest.buildConfig.cxxflags); std::string user_cflags = join(plan.manifest.buildConfig.cflags); + std::string user_ldflags; + for (auto const& flag : plan.manifest.buildConfig.ldflags) { + user_ldflags += ' '; + user_ldflags += normalize_ldflag(plan.projectRoot, flag); + } // C standard std::string c_std = @@ -257,12 +282,12 @@ CompileFlags compute_flags(const BuildPlan& plan) { } if constexpr (mcpp::platform::is_windows) { - f.ld = ""; + f.ld = user_ldflags; } else if constexpr (mcpp::platform::needs_explicit_libcxx) { - f.ld = std::format("{}{}{} -lc++", full_static, static_stdlib, b_flag); + f.ld = std::format("{}{}{} -lc++{}", full_static, static_stdlib, b_flag, user_ldflags); } else { - f.ld = std::format("{}{}{}{}{}{}", full_static, static_stdlib, link_toolchain_flags, b_flag, - runtime_dirs, payload_ld); + f.ld = std::format("{}{}{}{}{}{}{}", full_static, static_stdlib, link_toolchain_flags, b_flag, + runtime_dirs, payload_ld, user_ldflags); } return f; diff --git a/src/build/ninja_backend.cppm b/src/build/ninja_backend.cppm index 95fe320..07dee58 100644 --- a/src/build/ninja_backend.cppm +++ b/src/build/ninja_backend.cppm @@ -133,7 +133,8 @@ std::filesystem::path mcpp_exe_path() { } bool is_c_source(const std::filesystem::path& src) { - return src.extension() == ".c"; + auto ext = src.extension(); + return ext == ".c" || ext == ".m"; } std::string ltrim_copy(std::string_view s) { @@ -360,7 +361,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { } append("rule cxx_link\n"); - append(" command = $cxx $in -o $out $ldflags\n"); + append(" command = $cxx $in -o $out $ldflags $unit_ldflags\n"); append(" description = LINK $out\n\n"); append("rule cxx_archive\n"); @@ -368,7 +369,7 @@ std::string emit_ninja_string(const BuildPlan& plan) { append(" description = AR $out\n\n"); append("rule cxx_shared\n"); - append(" command = $cxx -shared $in -o $out $ldflags\n"); + append(" command = $cxx -shared $in -o $out $ldflags $unit_ldflags\n"); append(" description = SHARED $out\n\n"); if (dyndep) { @@ -609,7 +610,17 @@ std::string emit_ninja_string(const BuildPlan& plan) { rule = "cxx_shared"; break; } - append(std::format("build {} : {}{}\n", escape_ninja_path(lu.output), rule, ins)); + std::string implicit; + for (auto& input : lu.implicitInputs) { + implicit += " " + escape_ninja_path(input); + } + + std::string out_line = std::format("build {} : {}{}{}\n", + escape_ninja_path(lu.output), rule, ins, + implicit.empty() ? std::string{} : " |" + implicit); + if (auto flags = join_flags(lu.linkFlags); !flags.empty()) + out_line += " unit_ldflags =" + flags + "\n"; + append(std::move(out_line)); } append("\n"); diff --git a/src/build/plan.cppm b/src/build/plan.cppm index ff7f3ee..77335e9 100644 --- a/src/build/plan.cppm +++ b/src/build/plan.cppm @@ -8,6 +8,7 @@ export module mcpp.build.plan; import std; import mcpp.manifest; import mcpp.modgraph.graph; +import mcpp.modgraph.scanner; import mcpp.toolchain.detect; import mcpp.toolchain.fingerprint; import mcpp.platform; @@ -17,6 +18,7 @@ export namespace mcpp::build { struct CompileUnit { std::filesystem::path source; std::filesystem::path object; // relative to plan.outputDir + std::string packageName; std::vector localIncludeDirs; std::vector packageCflags; std::vector packageCxxflags; @@ -28,6 +30,8 @@ struct LinkUnit { std::string targetName; enum Kind { Binary, StaticLibrary, SharedLibrary, TestBinary } kind = Binary; std::vector objects; // relative to plan.outputDir + std::vector implicitInputs; // relative to plan.outputDir + std::vector linkFlags; // per-link edge flags std::filesystem::path output; // relative to plan.outputDir std::optional entryMain; // src path of main.cpp for bin }; @@ -55,6 +59,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, const mcpp::toolchain::Fingerprint& fp, const mcpp::modgraph::Graph& graph, const std::vector& topoOrder, + const std::vector& packages, const std::filesystem::path& projectRoot, const std::filesystem::path& outputDir, const std::filesystem::path& stdBmiPath, @@ -82,6 +87,72 @@ std::string object_filename_for(const std::filesystem::path& src) { return stem + (src.extension() == ".cppm" ? ".m.o" : ".o"); } +std::string qualified_package_name(const mcpp::manifest::Manifest& manifest) { + if (!manifest.package.namespace_.empty() + && manifest.package.name.starts_with(manifest.package.namespace_ + ".")) { + return manifest.package.name; + } + if (manifest.package.namespace_.empty()) return manifest.package.name; + return manifest.package.namespace_ + "." + manifest.package.name; +} + +std::vector dependency_name_candidates( + const std::string& depName, + const mcpp::manifest::DependencySpec& spec) +{ + std::vector out; + auto push = [&](std::string value) { + if (value.empty()) return; + if (std::find(out.begin(), out.end(), value) == out.end()) + out.push_back(std::move(value)); + }; + + push(depName); + if (!spec.shortName.empty()) push(spec.shortName); + if (!spec.namespace_.empty() && !spec.shortName.empty()) { + push(spec.namespace_ + "." + spec.shortName); + } + return out; +} + +std::filesystem::path target_output(const mcpp::manifest::Target& t) { + if (t.kind == mcpp::manifest::Target::Library) { + return std::filesystem::path("bin") / + std::format("{}{}{}", mcpp::platform::lib_prefix, t.name, + mcpp::platform::static_lib_ext); + } + if (t.kind == mcpp::manifest::Target::SharedLibrary) { + return std::filesystem::path("bin") / + std::format("{}{}{}", mcpp::platform::lib_prefix, t.name, + mcpp::platform::shared_lib_ext); + } + return std::filesystem::path("bin") / + std::format("{}{}", t.name, mcpp::platform::exe_suffix); +} + +bool is_implementation_source(const std::filesystem::path& source) { + auto ext = source.extension(); + return ext == ".cpp" || ext == ".cc" || ext == ".cxx" || ext == ".c" || ext == ".m"; +} + +std::vector shared_library_link_flags(const mcpp::manifest::Target& t) { + std::vector flags; + if constexpr (mcpp::platform::is_windows) { + flags.push_back(target_output(t).generic_string()); + } else { + flags.push_back("-L" + target_output(t).parent_path().generic_string()); + if constexpr (mcpp::platform::supports_rpath) { + if constexpr (mcpp::platform::is_macos) { + flags.push_back("-Wl,-rpath,@loader_path"); + } else { + flags.push_back("-Wl,-rpath,'$$ORIGIN'"); + } + } + flags.push_back("-l" + t.name); + } + return flags; +} + } // namespace BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, @@ -89,6 +160,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, const mcpp::toolchain::Fingerprint& fp, const mcpp::modgraph::Graph& graph, const std::vector& topoOrder, + const std::vector& packages, const std::filesystem::path& projectRoot, const std::filesystem::path& outputDir, const std::filesystem::path& stdBmiPath, @@ -122,6 +194,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, auto& u = graph.units[idx]; CompileUnit cu; cu.source = u.path; + cu.packageName = u.packageName; cu.localIncludeDirs = u.localIncludeDirs; cu.packageCflags = u.packageCflags; cu.packageCxxflags = u.packageCxxflags; @@ -163,6 +236,116 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, entryFilesAcrossTargets.insert(projectRoot / t.main); } } + for (auto const& p : packages) { + for (auto const& t : p.manifest.targets) { + if (!t.main.empty()) { + entryFilesAcrossTargets.insert(p.root / t.main); + } + } + } + + struct SharedDepTarget { + std::size_t packageIndex = 0; + std::string packageName; + mcpp::manifest::Target target; + std::filesystem::path output; + }; + std::vector sharedDepTargets; + std::set sharedDepPackages; + std::map> sharedTargetsByPackage; + std::map> packageIndexByName; + for (std::size_t i = 0; i < packages.size(); ++i) { + auto const& p = packages[i]; + packageIndexByName[qualified_package_name(p.manifest)] = i; + packageIndexByName[p.manifest.package.name] = i; + } + + for (std::size_t i = 1; i < packages.size(); ++i) { + auto const& p = packages[i]; + auto qname = qualified_package_name(p.manifest); + for (auto const& t : p.manifest.targets) { + if (t.kind != mcpp::manifest::Target::SharedLibrary) continue; + sharedDepPackages.insert(qname); + const auto targetIndex = sharedDepTargets.size(); + sharedDepTargets.push_back(SharedDepTarget{ + .packageIndex = i, + .packageName = qname, + .target = t, + .output = target_output(t), + }); + sharedTargetsByPackage[i].push_back(targetIndex); + } + } + + std::map> directPackageDeps; + for (std::size_t i = 0; i < packages.size(); ++i) { + for (auto const& [depName, spec] : packages[i].manifest.dependencies) { + for (auto const& candidate : dependency_name_candidates(depName, spec)) { + auto it = packageIndexByName.find(candidate); + if (it == packageIndexByName.end() || it->second == i) continue; + auto& deps = directPackageDeps[i]; + if (std::find(deps.begin(), deps.end(), it->second) == deps.end()) + deps.push_back(it->second); + break; + } + } + } + + auto append_direct_shared_deps = [&](LinkUnit& lu, std::size_t packageIndex) { + auto depsIt = directPackageDeps.find(packageIndex); + if (depsIt == directPackageDeps.end()) return; + for (auto depIndex : depsIt->second) { + auto targetsIt = sharedTargetsByPackage.find(depIndex); + if (targetsIt == sharedTargetsByPackage.end()) continue; + for (auto targetIndex : targetsIt->second) { + auto const& dep = sharedDepTargets[targetIndex]; + lu.implicitInputs.push_back(dep.output); + auto flags = shared_library_link_flags(dep.target); + lu.linkFlags.insert(lu.linkFlags.end(), flags.begin(), flags.end()); + } + } + }; + + auto append_shared_deps_for_linked_objects = [&](LinkUnit& lu) { + std::set linkedPackages; + linkedPackages.insert(0); + for (auto& cu : plan.compileUnits) { + if (sharedDepPackages.contains(cu.packageName)) continue; + auto it = packageIndexByName.find(cu.packageName); + if (it == packageIndexByName.end()) continue; + linkedPackages.insert(it->second); + } + + for (auto packageIndex : linkedPackages) { + append_direct_shared_deps(lu, packageIndex); + } + }; + + auto append_package_objects = [&](LinkUnit& lu, const std::string& packageName) { + for (auto& cu : plan.compileUnits) { + if (cu.packageName != packageName) continue; + if (cu.source.extension() == ".cppm") { + lu.objects.push_back(cu.object); + } + } + for (auto& cu : plan.compileUnits) { + if (cu.packageName != packageName) continue; + if (!is_implementation_source(cu.source)) continue; + if (lu.entryMain && cu.source == *lu.entryMain) continue; + if (entryFilesAcrossTargets.contains(cu.source)) continue; + lu.objects.push_back(cu.object); + } + }; + + for (auto const& dep : sharedDepTargets) { + LinkUnit lu; + lu.targetName = dep.target.name; + lu.kind = LinkUnit::SharedLibrary; + lu.output = dep.output; + append_package_objects(lu, dep.packageName); + append_direct_shared_deps(lu, dep.packageIndex); + plan.linkUnits.push_back(std::move(lu)); + } // 4. Link units (one per [targets.X]) // When any TestBinary target exists, skip Binary/Library/SharedLibrary @@ -179,29 +362,24 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, lu.targetName = t.name; if (t.kind == mcpp::manifest::Target::Library) { lu.kind = LinkUnit::StaticLibrary; - lu.output = std::filesystem::path("bin") / - std::format("{}{}{}", mcpp::platform::lib_prefix, t.name, - mcpp::platform::static_lib_ext); + lu.output = target_output(t); } else if (t.kind == mcpp::manifest::Target::SharedLibrary) { lu.kind = LinkUnit::SharedLibrary; - lu.output = std::filesystem::path("bin") / - std::format("{}{}{}", mcpp::platform::lib_prefix, t.name, - mcpp::platform::shared_lib_ext); + lu.output = target_output(t); } else if (t.kind == mcpp::manifest::Target::TestBinary) { lu.kind = LinkUnit::TestBinary; - lu.output = std::filesystem::path("bin") / - std::format("{}{}", t.name, mcpp::platform::exe_suffix); + lu.output = target_output(t); if (!t.main.empty()) lu.entryMain = projectRoot / t.main; } else { lu.kind = LinkUnit::Binary; - lu.output = std::filesystem::path("bin") / - std::format("{}{}", t.name, mcpp::platform::exe_suffix); + lu.output = target_output(t); if (!t.main.empty()) lu.entryMain = projectRoot / t.main; } // Include all module units' objects (they may be needed at runtime via global init). // For binary target, also include main.cpp's object if main is present. for (auto& cu : plan.compileUnits) { + if (sharedDepPackages.contains(cu.packageName)) continue; if (cu.source.extension() == ".cppm") { lu.objects.push_back(cu.object); } @@ -212,6 +390,7 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, CompileUnit main_cu; main_cu.source = *lu.entryMain; main_cu.object = std::filesystem::path("obj") / object_filename_for(*lu.entryMain); + main_cu.packageName = qualified_package_name(manifest); // We didn't scan main.cpp earlier (it's not in scanner output unless globbed in). // Best-effort: scan its imports here. @@ -251,13 +430,18 @@ BuildPlan make_plan(const mcpp::manifest::Manifest& manifest, // file registered as another target's entryMain (each binary's main() // is exclusive to that binary). for (auto& cu : plan.compileUnits) { - auto ext = cu.source.extension(); - if (ext != ".cpp" && ext != ".cc" && ext != ".cxx" && ext != ".c") continue; + if (sharedDepPackages.contains(cu.packageName)) continue; + if (!is_implementation_source(cu.source)) continue; if (lu.entryMain && cu.source == *lu.entryMain) continue; // own entry: already added above if (entryFilesAcrossTargets.contains(cu.source)) continue; // foreign entry: skip lu.objects.push_back(cu.object); } + if (lu.kind == LinkUnit::Binary || lu.kind == LinkUnit::TestBinary + || lu.kind == LinkUnit::SharedLibrary) { + append_shared_deps_for_linked_objects(lu); + } + plan.linkUnits.push_back(std::move(lu)); } diff --git a/src/cli.cppm b/src/cli.cppm index f0b022b..3d06926 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -597,6 +597,10 @@ std::string canonical_compile_flags(const mcpp::manifest::Manifest& m) { s += " cxxflag:"; s += flag; } + for (auto const& flag : m.buildConfig.ldflags) { + s += " ldflag:"; + s += flag; + } return s; } @@ -623,6 +627,10 @@ std::string canonical_package_build_metadata( s += " cxxflag:"; s += flag; } + for (auto const& flag : pkg.manifest.buildConfig.ldflags) { + s += " ldflag:"; + s += flag; + } for (auto const& [path, content] : pkg.manifest.buildConfig.generatedFiles) { s += " genfile:"; s += path.generic_string(); @@ -1448,14 +1456,33 @@ prepare_build(bool print_fingerprint, // two different exact versions is an error — mcpp prints both // requesting parents and asks the user to align them. - // Auto-refresh the package index if the project has version-source - // dependencies and the local index is missing or stale. + // Auto-refresh the builtin package index only when a version dependency + // is actually routed there. Local/remote project indices are handled by + // the project-scoped setup below; refreshing the global index for those + // packages is both unnecessary and can make offline/local-index builds + // block on unrelated remote repositories. if (!m->dependencies.empty()) { - bool hasVersionDeps = false; + auto usesBuiltinIndex = [&](const mcpp::manifest::DependencySpec& spec) { + if (spec.isPath() || spec.isGit()) return false; + + auto ns = spec.namespace_.empty() + ? std::string(mcpp::pm::kDefaultNamespace) + : spec.namespace_; + if (ns == mcpp::pm::kDefaultNamespace) return true; + + auto it = m->indices.find(ns); + if (it == m->indices.end()) return true; + return it->second.is_builtin(); + }; + + bool needsBuiltinIndexRefresh = false; for (auto& [_, spec] : m->dependencies) { - if (!spec.isPath() && !spec.isGit()) { hasVersionDeps = true; break; } + if (usesBuiltinIndex(spec)) { + needsBuiltinIndexRefresh = true; + break; + } } - if (hasVersionDeps) { + if (needsBuiltinIndexRefresh) { auto cfg2 = get_cfg(); if (cfg2) { auto xlEnv = mcpp::config::make_xlings_env(**cfg2); @@ -1541,6 +1568,7 @@ prepare_build(bool print_fingerprint, std::string source; // "version" | "path" | "git" — for type-clash check std::size_t depIndex = 0; // index into dep_manifests/packages-1 (for in-place re-fetch) std::vector includeDirsAdded; // entries appended to m->buildConfig.includeDirs by this dep + std::vector linkFlagsAdded; // entries appended to m->buildConfig.ldflags by this dep }; std::map resolved; @@ -1664,10 +1692,11 @@ prepare_build(bool print_fingerprint, return fetcher.install(targets, &progress); }; auto target = std::format("{}@{}", fqname, version); - // For custom indices, use indexName:shortName@version format - // so xlings knows which index to resolve from. + // For custom indices, use indexName:fullPackageName@version so + // xlings resolves the package by the descriptor's name field while + // still selecting the project-added index. if (useProjectEnv) { - target = std::format("{}:{}@{}", ns, shortName, version); + target = std::format("{}:{}@{}", ns, fqname, version); } auto r = install_one(target); if (r && r->exitCode != 0 && @@ -1824,6 +1853,48 @@ prepare_build(bool print_fingerprint, } }; + auto normalizeDepLdflag = [](const std::filesystem::path& depRoot, + const std::string& flag) { + auto absolute_path = [&](std::string_view raw) { + std::filesystem::path p{std::string(raw)}; + if (p.is_absolute() || raw.starts_with("$")) return p; + return depRoot / p; + }; + + if (flag.starts_with("-L") && flag.size() > 2) { + return "-L" + absolute_path(std::string_view(flag).substr(2)).string(); + } + + constexpr std::string_view rpathPrefix = "-Wl,-rpath,"; + if (flag.starts_with(rpathPrefix) && flag.size() > rpathPrefix.size()) { + return std::string(rpathPrefix) + + absolute_path(std::string_view(flag).substr(rpathPrefix.size())).string(); + } + + return flag; + }; + + auto propagateLinkFlags = [&](const std::filesystem::path& depRoot, + const mcpp::manifest::Manifest& depManifest) + -> std::vector + { + std::vector added; + for (auto const& flag : depManifest.buildConfig.ldflags) { + auto normalized = normalizeDepLdflag(depRoot, flag); + m->buildConfig.ldflags.push_back(normalized); + added.push_back(std::move(normalized)); + } + return added; + }; + + auto removeLinkFlags = [&](const std::vector& flags) { + auto& ldflags = m->buildConfig.ldflags; + for (auto const& flag : flags) { + auto pos = std::find(ldflags.begin(), ldflags.end(), flag); + if (pos != ldflags.end()) ldflags.erase(pos); + } + }; + // Stage a dep's source files into a fresh directory, rewriting their // module / import declarations against `rename`. Used by the multi- // version mangling fallback (Level 1) so two cross-major copies of @@ -2065,6 +2136,7 @@ prepare_build(bool print_fingerprint, }); packages.push_back({secStage, *dep_manifests.back()}); auto added = propagateIncludeDirs(secStage, *dep_manifests.back()); + auto linkFlagsAdded = propagateLinkFlags(secStage, *dep_manifests.back()); ResolvedKey mangledKey{key.ns, mangled}; resolved[mangledKey] = ResolvedRecord{ @@ -2074,6 +2146,7 @@ prepare_build(bool print_fingerprint, .source = "version", .depIndex = dep_manifests.size() - 1, .includeDirsAdded = std::move(added), + .linkFlagsAdded = std::move(linkFlagsAdded), }; mcpp::ui::info("Mangled", @@ -2136,7 +2209,9 @@ prepare_build(bool print_fingerprint, } removeIncludeDirs(it->second.includeDirsAdded); + removeLinkFlags(it->second.linkFlagsAdded); auto added = propagateIncludeDirs(newRoot, newManifest); + auto linkFlagsAdded = propagateLinkFlags(newRoot, newManifest); // Replace in dep_manifests + packages. depIndex is the slot // in dep_manifests; packages = [main, dep_0, dep_1, …], so @@ -2147,6 +2222,7 @@ prepare_build(bool print_fingerprint, it->second.version = *merged; it->second.includeDirsAdded = std::move(added); + it->second.linkFlagsAdded = std::move(linkFlagsAdded); if (it->second.depIndex < dep_cache_identities.size()) dep_cache_identities[it->second.depIndex].version = *merged; @@ -2284,6 +2360,7 @@ prepare_build(bool print_fingerprint, // expansion against dep_root) — stash it so a SemVer merge can // evict these entries on a re-fetch. auto includeDirsAdded = propagateIncludeDirs(dep_root, *dep_manifest); + auto linkFlagsAdded = propagateLinkFlags(dep_root, *dep_manifest); // Move the manifest into stable storage so we can later look it up // by depIndex (the SemVer merger needs to overwrite the slot). @@ -2307,6 +2384,7 @@ prepare_build(bool print_fingerprint, .source = sourceKind, .depIndex = dep_manifests.size() - 1, .includeDirsAdded = std::move(includeDirsAdded), + .linkFlagsAdded = std::move(linkFlagsAdded), }; // Recurse: the dep's own [dependencies] become new worklist items. @@ -2408,7 +2486,8 @@ prepare_build(bool print_fingerprint, ctx.stdBmi = stdBmiPath; ctx.stdObject = stdObjectPath; ctx.plan = mcpp::build::make_plan(*m, *tc, fp, scan.graph, report.topoOrder, - *root, ctx.outputDir, stdBmiPath, stdObjectPath); + packages, *root, ctx.outputDir, + stdBmiPath, stdObjectPath); ctx.plan.stdCompatBmiPath = stdCompatBmiPath; ctx.plan.stdCompatObjectPath = stdCompatObjectPath; diff --git a/src/config.cppm b/src/config.cppm index 08440b4..632413e 100644 --- a/src/config.cppm +++ b/src/config.cppm @@ -666,6 +666,8 @@ bool ensure_project_index_dir( if (spec.is_builtin()) continue; if (spec.is_local()) { auto source = resolve_project_index_path(projectDir, spec); + std::error_code ec; + std::filesystem::remove(source / ".xlings-index-cache.json", ec); customRepos.emplace_back(name, source.generic_string()); continue; } @@ -682,6 +684,29 @@ bool ensure_project_index_dir( mcpp::xlings::Env env; env.home = dotMcpp; mcpp::xlings::seed_xlings_json(env, customRepos); + + // Project-scoped xlings installs custom-index packages in an additive + // project data dir. Expose the global official xim index there too, so + // package deps like `xim:python@latest` can resolve without falling back + // to unrelated remote index updates or system tools. + auto officialIndex = cfg.xlingsHome() / "data" / "xim-pkgindex"; + if (std::filesystem::exists(officialIndex / "pkgs", ec)) { + auto projectData = dotMcpp / ".xlings" / "data"; + auto projectOfficial = projectData / "xim-pkgindex"; + std::filesystem::create_directories(projectData, ec); + if (!std::filesystem::exists(projectOfficial, ec)) { + std::filesystem::create_directory_symlink(officialIndex, projectOfficial, ec); + if (ec) { + ec.clear(); + std::filesystem::copy( + officialIndex, + projectOfficial, + std::filesystem::copy_options::recursive + | std::filesystem::copy_options::skip_existing, + ec); + } + } + } return true; } diff --git a/src/manifest.cppm b/src/manifest.cppm index 7864cd3..67d3fb8 100644 --- a/src/manifest.cppm +++ b/src/manifest.cppm @@ -7,6 +7,7 @@ import mcpp.libs.toml; import mcpp.pm.dep_spec; // M5.x pm/ subsystem refactor: DependencySpec lives here import mcpp.pm.compat; // Legacy dependency-key compatibility helpers import mcpp.pm.index_spec; // IndexSpec for [indices] section +import mcpp.platform; export namespace mcpp::manifest { @@ -91,6 +92,7 @@ struct BuildConfig { // Empty cStandard → backend default ("c11" today). std::vector cflags; std::vector cxxflags; + std::vector ldflags; std::string cStandard; }; @@ -628,6 +630,7 @@ std::expected parse_string(std::string_view content, if (auto v = doc->get_bool("build.static_stdlib")) m.buildConfig.staticStdlib = *v; if (auto v = doc->get_string_array("build.cflags")) m.buildConfig.cflags = *v; if (auto v = doc->get_string_array("build.cxxflags")) m.buildConfig.cxxflags = *v; + if (auto v = doc->get_string_array("build.ldflags")) m.buildConfig.ldflags = *v; if (auto v = doc->get_string("build.c_standard")) m.buildConfig.cStandard = *v; // [lib] — library root convention (cargo-style). @@ -955,8 +958,71 @@ struct LuaCursor { else { ++pos; } } } + + // Read and consume a balanced { ... } block, returning the inner text. + std::string read_table_body() { + if (!consume('{')) return {}; + auto start = pos; + int depth = 1; + while (!eof() && depth > 0) { + char c = peek(); + if (c == '"' || c == '\'') { + read_string(); + continue; + } + if (c == '-' && pos + 1 < text.size() && text[pos + 1] == '-') { + while (!eof() && peek() != '\n') ++pos; + continue; + } + if (c == '{') { + ++depth; + ++pos; + continue; + } + if (c == '}') { + --depth; + if (depth == 0) { + auto end = pos; + ++pos; + return std::string(text.substr(start, end - start)); + } + ++pos; + continue; + } + ++pos; + } + return {}; + } }; +std::string top_level_table_body_for_key(std::string_view body, std::string_view wantedKey) { + LuaCursor cur { body }; + cur.skip_ws_and_comments(); + while (!cur.eof()) { + auto key = cur.read_key(); + if (key.empty()) { + cur.skip_ws_and_comments(); + if (cur.eof()) break; + ++cur.pos; + continue; + } + cur.skip_ws_and_comments(); + if (!cur.consume('=')) { + cur.skip_ws_and_comments(); + continue; + } + cur.skip_ws_and_comments(); + if (key == wantedKey) { + return cur.read_table_body(); + } + if (cur.peek() == '{') cur.skip_table(); + else if (cur.peek() == '"' || cur.peek() == '\'') (void)cur.read_string(); + else (void)cur.read_bareword(); + cur.skip_ws_and_comments(); + } + return {}; +} + // Strip Lua line comments (`-- ...\n`) and string contents from text, // replacing them with spaces of the same length so positions are // preserved. This is a simple-but-correct way to make the scanner @@ -1214,6 +1280,11 @@ synthesize_from_xpkg_lua(std::string_view luaContent, std::format("xpkg-lua of {}@{}", packageName, packageVersion), 0, 0}); } + if (auto platformBody = top_level_table_body_for_key(body, mcpp::platform::xpkg_platform); + !platformBody.empty()) { + body += "\n"; + body += platformBody; + } Manifest m; m.sourcePath = std::format("xpkg-lua://{}@{}", packageName, packageVersion); @@ -1396,10 +1467,10 @@ synthesize_from_xpkg_lua(std::string_view luaContent, } cur.consume('}'); } - else if (key == "cflags" || key == "cxxflags") { + else if (key == "cflags" || key == "cxxflags" || key == "ldflags") { // `{ "-Dfoo", "-Wall", ... }` — appended to the per-rule baseline // by ninja_backend. cflags goes to the C rule (.c files), cxxflags - // to C++ rule (.cpp/.cc/.cxx/.cppm). + // to C++ rule (.cpp/.cc/.cxx/.cppm), ldflags to link commands. if (!cur.consume('{')) { return std::unexpected(ManifestError{ std::format("expected '{{' after `{} =`", key), @@ -1407,7 +1478,8 @@ synthesize_from_xpkg_lua(std::string_view luaContent, } cur.skip_ws_and_comments(); auto& target = (key == "cflags") - ? m.buildConfig.cflags : m.buildConfig.cxxflags; + ? m.buildConfig.cflags + : (key == "cxxflags" ? m.buildConfig.cxxflags : m.buildConfig.ldflags); while (!cur.eof() && cur.peek() != '}') { auto s = cur.read_string(); if (!s.empty()) target.push_back(std::move(s)); diff --git a/src/modgraph/scanner.cppm b/src/modgraph/scanner.cppm index a07dc68..08a2b80 100644 --- a/src/modgraph/scanner.cppm +++ b/src/modgraph/scanner.cppm @@ -190,12 +190,12 @@ std::expected scan_file(const std::filesystem::path& file u.path = file; u.packageName = packageName; - // .c files are pure C: they cannot legally contain `module` / `import` + // C-like files are not C++ modules: they cannot legally contain `module` / `import` // declarations, and we route them to the C-language compile rule (no // P1689 scan, no BMI lookups). Skip the line-by-line module scan to - // avoid any chance of a benign C identifier (`import_foo`, `module_t`, - // ...) being misparsed. - if (file.extension() == ".c") { + // avoid any chance of a benign identifier (`import_foo`, `module_t`, ...) + // being misparsed. Objective-C .m files use the same C-like path. + if (file.extension() == ".c" || file.extension() == ".m") { return u; } diff --git a/src/toolchain/fingerprint.cppm b/src/toolchain/fingerprint.cppm index 8bc8b1f..30f3d01 100644 --- a/src/toolchain/fingerprint.cppm +++ b/src/toolchain/fingerprint.cppm @@ -18,7 +18,7 @@ import mcpp.toolchain.detect; export namespace mcpp::toolchain { -inline constexpr std::string_view MCPP_VERSION = "0.0.37"; +inline constexpr std::string_view MCPP_VERSION = "0.0.38"; struct FingerprintInputs { Toolchain toolchain; diff --git a/tests/e2e/52_local_path_namespaced_index.sh b/tests/e2e/52_local_path_namespaced_index.sh index 7ecb48a..6864e01 100755 --- a/tests/e2e/52_local_path_namespaced_index.sh +++ b/tests/e2e/52_local_path_namespaced_index.sh @@ -81,4 +81,115 @@ EOF exit 1 } +# Also verify the install target used for a clean local path index. The +# package name in the index is compat.cfg, so xlings must receive the fully +# qualified package name after the index selector: compat:compat.cfg@1.0.0. +mkdir -p "$TMP/fake-bin" +FAKE_REGISTRY="$TMP/fake-registry" +FAKE_LOG="$TMP/fake-xlings.log" +mkdir -p "$FAKE_REGISTRY/data" +if [[ -d "$USER_MCPP/registry/data/xpkgs" ]]; then + ln -s "$USER_MCPP/registry/data/xpkgs" "$FAKE_REGISTRY/data/xpkgs" +fi +cat > "$TMP/fake-bin/xlings" <<'EOF' +#!/usr/bin/env bash +set -e + +if [[ "${1:-}" == "self" && "${2:-}" == "init" ]]; then + mkdir -p "${XLINGS_HOME:?}/subos/default" + printf '{}\n' > "$XLINGS_HOME/subos/default/.xlings.json" + exit 0 +fi + +if [[ "${1:-}" == "update" ]]; then + printf 'update\n' > "${FAKE_XLINGS_UPDATE_LOG:?}" + exit 0 +fi + +if [[ "${1:-}" == "interface" && "${2:-}" == "install_packages" ]]; then + while [[ $# -gt 0 ]]; do + if [[ "$1" == "--args" ]]; then + printf '%s\n' "$2" > "${FAKE_XLINGS_LOG:?}" + break + fi + shift + done + printf '{"kind":"result","exitCode":1}\n' + exit 0 +fi + +exit 0 +EOF +chmod +x "$TMP/fake-bin/xlings" + +cat > "$MCPP_HOME/config.toml" < src/main.cpp <<'EOF' +extern "C" int cfg_value(void); +int main() { + return cfg_value() == 42 ? 0 : 1; +} +EOF + +cat > mcpp.toml < fetch.log 2>&1; then + echo "FAIL: clean local path dependency unexpectedly built without package install" + cat fetch.log + exit 1 +fi + +if [[ -f "$UPDATE_LOG" ]]; then + echo "FAIL: local path index dependency should not refresh the builtin package index" + cat fetch.log + exit 1 +fi + +grep -Fq '"compat:compat.cfg@1.0.0"' "$FAKE_LOG" || { + echo "FAIL: clean local path install target should use full package name" + echo "recorded:" + cat "$FAKE_LOG" 2>/dev/null || true + echo "build log:" + cat fetch.log + exit 1 +} + echo "OK" diff --git a/tests/e2e/53_namespaced_cache_label.sh b/tests/e2e/53_namespaced_cache_label.sh index 7b650cf..a8bb4e3 100755 --- a/tests/e2e/53_namespaced_cache_label.sh +++ b/tests/e2e/53_namespaced_cache_label.sh @@ -88,8 +88,8 @@ if grep -Eq '^\[[0-9]+/[0-9]+\] ' build.log; then exit 1 fi -[[ -f "$MCPP_HOME/registry/data/mcpplibs/.mcpp-index-updated" ]] || { - echo "FAIL: automatic index refresh should write mcpp freshness marker" +[[ ! -e "$MCPP_HOME/registry/data/mcpplibs/.mcpp-index-updated" ]] || { + echo "FAIL: local path index dependency should not refresh builtin mcpplibs index" find "$MCPP_HOME/registry/data" -maxdepth 3 -type f | sort cat build.log exit 1 diff --git a/tests/e2e/54_package_owned_ldflags.sh b/tests/e2e/54_package_owned_ldflags.sh new file mode 100644 index 0000000..333b905 --- /dev/null +++ b/tests/e2e/54_package_owned_ldflags.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# requires: gcc +# Package-owned ldflags: a dependency can declare link flags that are applied +# when the consumer binary is linked. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +cd "$TMP" +mkdir -p native depLink app/src + +cat > native/answer.c <<'EOF' +int dep_link_answer(void) { + return 42; +} +EOF + +gcc -c native/answer.c -o native/answer.o +ar rcs native/libanswer.a native/answer.o +NATIVE_DIR="$(pwd)/native" + +cat > depLink/mcpp.toml < app/src/main.cpp <<'EOF' +extern "C" int dep_link_answer(void); + +int main() { + return dep_link_answer() == 42 ? 0 : 1; +} +EOF + +cat > app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[build] +sources = ["src/*.cpp"] + +[targets.app] +kind = "bin" +main = "src/main.cpp" + +[dependencies.depLink] +path = "../depLink" +EOF + +cd app +"$MCPP" build > build.log 2>&1 || { + cat build.log + echo "build failed" + exit 1 +} + +"$MCPP" run > run.log 2>&1 || { + cat run.log + echo "run failed" + exit 1 +} + +ninja_file="$(find target -name build.ninja | head -1)" +[[ -n "$ninja_file" ]] || { + echo "no build.ninja generated" + exit 1 +} + +grep -q -- "-L${NATIVE_DIR}" "$ninja_file" || { + echo "dep -L flag missing from build.ninja" + cat "$ninja_file" + exit 1 +} +grep -q -- "-lanswer" "$ninja_file" || { + echo "dep -l flag missing from build.ninja" + cat "$ninja_file" + exit 1 +} + +echo "OK" diff --git a/tests/e2e/55_dependency_shared_artifact.sh b/tests/e2e/55_dependency_shared_artifact.sh new file mode 100644 index 0000000..b281ce8 --- /dev/null +++ b/tests/e2e/55_dependency_shared_artifact.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# requires: elf +# Dependency shared libraries must be built as package artifacts and linked by +# consumers, not flattened into the consumer binary as object files. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +cd "$TMP" +mkdir -p depShared/src app/src + +cat > depShared/src/dep.c <<'EOF' +int dep_shared_answer(void) { + return 42; +} +EOF + +cat > depShared/mcpp.toml <<'EOF' +[package] +name = "depShared" +version = "0.1.0" + +[build] +sources = ["src/*.c"] + +[targets.depShared] +kind = "shared" +EOF + +cat > app/src/main.cpp <<'EOF' +extern "C" int dep_shared_answer(void); + +int main() { + return dep_shared_answer() == 42 ? 0 : 1; +} +EOF + +cat > app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[build] +sources = ["src/*.cpp"] + +[targets.app] +kind = "bin" +main = "src/main.cpp" + +[dependencies.depShared] +path = "../depShared" +EOF + +cd app +"$MCPP" build > build.log 2>&1 || { + cat build.log + echo "build failed" + exit 1 +} + +so="$(find target -name 'libdepShared.so' | head -1)" +[[ -n "$so" ]] || { + cat build.log + echo "dependency shared library artifact was not produced" + exit 1 +} + +app_bin="$(find target -path '*/bin/app' -type f | head -1)" +[[ -n "$app_bin" ]] || { + cat build.log + echo "app binary was not produced" + exit 1 +} + +readelf -d "$app_bin" | grep -q 'Shared library: \[libdepShared.so\]' || { + readelf -d "$app_bin" || true + echo "app binary does not link against libdepShared.so" + exit 1 +} + +"$MCPP" run > run.log 2>&1 || { + cat run.log + echo "run failed" + exit 1 +} + +echo "OK" diff --git a/tests/e2e/56_transitive_shared_artifact.sh b/tests/e2e/56_transitive_shared_artifact.sh new file mode 100644 index 0000000..896ae9a --- /dev/null +++ b/tests/e2e/56_transitive_shared_artifact.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# requires: elf +# A shared dependency that depends on another shared package must link against +# that package itself, so the intermediate .so records the correct NEEDED edge. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +cd "$TMP" +mkdir -p depBase/src depMid/src app/src + +cat > depBase/src/base.c <<'EOF' +int dep_base_answer(void) { + return 41; +} +EOF + +cat > depBase/mcpp.toml <<'EOF' +[package] +name = "depBase" +version = "0.1.0" + +[build] +sources = ["src/*.c"] + +[targets.depBase] +kind = "shared" +EOF + +cat > depMid/src/mid.c <<'EOF' +extern int dep_base_answer(void); + +int dep_mid_answer(void) { + return dep_base_answer() + 1; +} +EOF + +cat > depMid/mcpp.toml <<'EOF' +[package] +name = "depMid" +version = "0.1.0" + +[build] +sources = ["src/*.c"] + +[targets.depMid] +kind = "shared" + +[dependencies.depBase] +path = "../depBase" +EOF + +cat > app/src/main.cpp <<'EOF' +extern "C" int dep_mid_answer(void); + +int main() { + return dep_mid_answer() == 42 ? 0 : 1; +} +EOF + +cat > app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[build] +sources = ["src/*.cpp"] + +[targets.app] +kind = "bin" +main = "src/main.cpp" + +[dependencies.depMid] +path = "../depMid" +EOF + +cd app +"$MCPP" build > build.log 2>&1 || { + cat build.log + echo "build failed" + exit 1 +} + +base_so="$(find target -name 'libdepBase.so' | head -1)" +mid_so="$(find target -name 'libdepMid.so' | head -1)" +app_bin="$(find target -path '*/bin/app' -type f | head -1)" +[[ -n "$base_so" && -n "$mid_so" && -n "$app_bin" ]] || { + cat build.log + echo "expected shared artifacts were not produced" + exit 1 +} + +readelf -d "$mid_so" | grep -q 'Shared library: \[libdepBase.so\]' || { + readelf -d "$mid_so" || true + echo "libdepMid.so does not link against libdepBase.so" + exit 1 +} + +readelf -d "$app_bin" | grep -q 'Shared library: \[libdepMid.so\]' || { + readelf -d "$app_bin" || true + echo "app binary does not link against libdepMid.so" + exit 1 +} + +"$MCPP" run > run.log 2>&1 || { + cat run.log + echo "run failed" + exit 1 +} + +echo "OK" diff --git a/tests/e2e/57_static_dep_shared_artifact.sh b/tests/e2e/57_static_dep_shared_artifact.sh new file mode 100644 index 0000000..5b580b8 --- /dev/null +++ b/tests/e2e/57_static_dep_shared_artifact.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# requires: elf +# A static dependency package can reference a shared dependency. Since mcpp +# flattens static package objects into the final consumer link, that final link +# must also include the shared libraries required by those static objects. +set -e + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +cd "$TMP" +mkdir -p depShared/src depStatic/src app/src + +cat > depShared/src/dep.c <<'EOF' +int dep_shared_answer(void) { + return 41; +} +EOF + +cat > depShared/mcpp.toml <<'EOF' +[package] +name = "depShared" +version = "0.1.0" + +[build] +sources = ["src/*.c"] + +[targets.depShared] +kind = "shared" +EOF + +cat > depStatic/src/static.c <<'EOF' +extern int dep_shared_answer(void); + +int dep_static_answer(void) { + return dep_shared_answer() + 1; +} +EOF + +cat > depStatic/mcpp.toml <<'EOF' +[package] +name = "depStatic" +version = "0.1.0" + +[build] +sources = ["src/*.c"] + +[targets.depStatic] +kind = "lib" + +[dependencies.depShared] +path = "../depShared" +EOF + +cat > app/src/main.cpp <<'EOF' +extern "C" int dep_static_answer(void); + +int main() { + return dep_static_answer() == 42 ? 0 : 1; +} +EOF + +cat > app/mcpp.toml <<'EOF' +[package] +name = "app" +version = "0.1.0" + +[build] +sources = ["src/*.cpp"] + +[targets.app] +kind = "bin" +main = "src/main.cpp" + +[dependencies.depStatic] +path = "../depStatic" +EOF + +cd app +"$MCPP" build > build.log 2>&1 || { + cat build.log + echo "build failed" + exit 1 +} + +shared_so="$(find target -name 'libdepShared.so' | head -1)" +app_bin="$(find target -path '*/bin/app' -type f | head -1)" +[[ -n "$shared_so" && -n "$app_bin" ]] || { + cat build.log + echo "expected shared artifact or app binary was not produced" + exit 1 +} + +readelf -d "$app_bin" | grep -q 'Shared library: \[libdepShared.so\]' || { + readelf -d "$app_bin" || true + echo "app binary does not link against static dependency's shared dependency" + exit 1 +} + +"$MCPP" run > run.log 2>&1 || { + cat run.log + echo "run failed" + exit 1 +} + +echo "OK" diff --git a/tests/unit/test_config.cpp b/tests/unit/test_config.cpp index a67bd2f..a6a9ca7 100644 --- a/tests/unit/test_config.cpp +++ b/tests/unit/test_config.cpp @@ -84,3 +84,58 @@ TEST(Config, ProjectIndexJsonEscapesLocalIndexPath) { is.close(); std::filesystem::remove_all(project); } + +TEST(Config, ProjectIndexDirExposesOfficialXimIndex) { + auto project = make_tempdir("mcpp-config-project-xim-index"); + auto registry = make_tempdir("mcpp-config-registry"); + auto official = registry / "data" / "xim-pkgindex"; + std::filesystem::create_directories(official / "pkgs" / "p"); + { + std::ofstream os(official / "pkgs" / "p" / "python.lua"); + os << "package = { name = \"python\" }\n"; + } + + auto localIndex = project / "compat"; + std::filesystem::create_directories(localIndex / "pkgs" / "c"); + + mcpp::pm::IndexSpec local; + local.name = "compat"; + local.path = localIndex; + + std::map indices; + indices.emplace(local.name, local); + + mcpp::config::GlobalConfig cfg; + cfg.registryDir = registry; + ASSERT_TRUE(mcpp::config::ensure_project_index_dir(cfg, project, indices)); + + auto projectOfficial = + project / ".mcpp" / ".xlings" / "data" / "xim-pkgindex"; + EXPECT_TRUE(std::filesystem::exists(projectOfficial / "pkgs" / "p" / "python.lua")); + + std::filesystem::remove_all(project); + std::filesystem::remove_all(registry); +} + +TEST(Config, ProjectLocalIndexStaleCacheIsRemoved) { + auto project = make_tempdir("mcpp-config-local-index-cache"); + auto localIndex = project / "compat"; + std::filesystem::create_directories(localIndex / "pkgs" / "c"); + { + std::ofstream os(localIndex / ".xlings-index-cache.json"); + os << R"({"entries":{"compat.lz4":{"path":"/tmp/deleted/pkgs/c/compat.lz4.lua"}}})"; + } + + mcpp::pm::IndexSpec local; + local.name = "compat"; + local.path = localIndex; + + std::map indices; + indices.emplace(local.name, local); + + mcpp::config::GlobalConfig cfg; + ASSERT_TRUE(mcpp::config::ensure_project_index_dir(cfg, project, indices)); + EXPECT_FALSE(std::filesystem::exists(localIndex / ".xlings-index-cache.json")); + + std::filesystem::remove_all(project); +} diff --git a/tests/unit/test_manifest.cpp b/tests/unit/test_manifest.cpp index 97212a2..81365bf 100644 --- a/tests/unit/test_manifest.cpp +++ b/tests/unit/test_manifest.cpp @@ -2,6 +2,7 @@ import std; import mcpp.manifest; +import mcpp.platform; TEST(Manifest, MinimalValid) { constexpr auto src = R"( @@ -153,6 +154,7 @@ version = "0.1.0" sources = ["src/**/*.{cppm,c}"] cflags = ["-Wall", "-DFOO=1"] cxxflags = ["-Wextra"] +ldflags = ["-lfoo", "-Wl,--as-needed"] c_standard = "c11" [targets.x] kind = "lib" @@ -164,10 +166,13 @@ kind = "lib" EXPECT_EQ(m->buildConfig.cflags[1], "-DFOO=1"); ASSERT_EQ(m->buildConfig.cxxflags.size(), 1u); EXPECT_EQ(m->buildConfig.cxxflags[0], "-Wextra"); + ASSERT_EQ(m->buildConfig.ldflags.size(), 2u); + EXPECT_EQ(m->buildConfig.ldflags[0], "-lfoo"); + EXPECT_EQ(m->buildConfig.ldflags[1], "-Wl,--as-needed"); EXPECT_EQ(m->buildConfig.cStandard, "c11"); } -TEST(SynthesizeFromXpkgLua, CflagsCxxflagsAndCStandard) { +TEST(SynthesizeFromXpkgLua, CflagsCxxflagsLdflagsAndCStandard) { constexpr auto src = R"( package = { spec = "1", @@ -177,6 +182,7 @@ package = { sources = { "*/src/*.c" }, cflags = { "-Wall", "-Dunused" }, cxxflags = { "-Wextra" }, + ldflags = { "-ltinyc" }, c_standard = "c11", targets = { ["tinyc"] = { kind = "lib" } }, }, @@ -189,11 +195,78 @@ package = { EXPECT_EQ(m->buildConfig.cflags[1], "-Dunused"); ASSERT_EQ(m->buildConfig.cxxflags.size(), 1u); EXPECT_EQ(m->buildConfig.cxxflags[0], "-Wextra"); + ASSERT_EQ(m->buildConfig.ldflags.size(), 1u); + EXPECT_EQ(m->buildConfig.ldflags[0], "-ltinyc"); EXPECT_EQ(m->buildConfig.cStandard, "c11"); ASSERT_EQ(m->modules.sources.size(), 1u); EXPECT_EQ(m->modules.sources[0], "*/src/*.c"); } +TEST(SynthesizeFromXpkgLua, AppliesCurrentPlatformMcppOverlay) { + constexpr auto src = R"( +package = { + spec = "1", + name = "tinyc", + xpm = { linux = { ["1.0.0"] = { url = "u", sha256 = "h" } } }, + mcpp = { + sources = { "*/src/common.c" }, + include_dirs = { "*/include" }, + cflags = { "-DCOMMON=1" }, + deps = { ["compat.base"] = "1.0.0" }, + targets = { ["tinyc"] = { kind = "lib" } }, + linux = { + sources = { "*/src/linux.c" }, + include_dirs = { "*/src/linux" }, + cflags = { "-DLINUX=1" }, + deps = { ["compat.x11"] = "1.8.13" }, + }, + macosx = { + sources = { "*/src/cocoa.m" }, + include_dirs = { "*/src/macos" }, + cflags = { "-DMACOS=1" }, + deps = { ["compat.cocoa"] = "1.0.0" }, + }, + windows = { + sources = { "*/src/win32.c" }, + include_dirs = { "*/src/win32" }, + cflags = { "-DWINDOWS=1" }, + deps = { ["compat.win32"] = "1.0.0" }, + }, + }, +} +)"; + auto m = mcpp::manifest::synthesize_from_xpkg_lua(src, "tinyc", "1.0.0"); + ASSERT_TRUE(m.has_value()) << m.error().format(); + + std::string expectedSource = "*/src/linux.c"; + std::string expectedInclude = "*/src/linux"; + std::string expectedCflag = "-DLINUX=1"; + std::string expectedDep = "compat.x11"; + if constexpr (mcpp::platform::is_macos) { + expectedSource = "*/src/cocoa.m"; + expectedInclude = "*/src/macos"; + expectedCflag = "-DMACOS=1"; + expectedDep = "compat.cocoa"; + } else if constexpr (mcpp::platform::is_windows) { + expectedSource = "*/src/win32.c"; + expectedInclude = "*/src/win32"; + expectedCflag = "-DWINDOWS=1"; + expectedDep = "compat.win32"; + } + + ASSERT_EQ(m->modules.sources.size(), 2u); + EXPECT_EQ(m->modules.sources[0], "*/src/common.c"); + EXPECT_EQ(m->modules.sources[1], expectedSource); + ASSERT_EQ(m->buildConfig.includeDirs.size(), 2u); + EXPECT_EQ(m->buildConfig.includeDirs[0], "*/include"); + EXPECT_EQ(m->buildConfig.includeDirs[1], expectedInclude); + ASSERT_EQ(m->buildConfig.cflags.size(), 2u); + EXPECT_EQ(m->buildConfig.cflags[0], "-DCOMMON=1"); + EXPECT_EQ(m->buildConfig.cflags[1], expectedCflag); + EXPECT_EQ(m->dependencies.count("compat.base"), 1u); + EXPECT_EQ(m->dependencies.count(expectedDep), 1u); +} + TEST(SynthesizeFromXpkgLua, GeneratedFiles) { constexpr auto src = R"( package = { diff --git a/tests/unit/test_modgraph.cpp b/tests/unit/test_modgraph.cpp index fa248f5..3d01428 100644 --- a/tests/unit/test_modgraph.cpp +++ b/tests/unit/test_modgraph.cpp @@ -143,6 +143,20 @@ TEST(Scanner, RejectsHeaderUnit) { std::filesystem::remove_all(dir); } +TEST(Scanner, ObjectiveCSourceIsCLike) { + auto dir = make_tempdir("mcpp-scanner-objc"); + write(dir / "src" / "window.m", + "import Cocoa;\n" + "int answer(void) { return 42; }\n"); + + auto u = scan_file(dir / "src" / "window.m", "pkg"); + ASSERT_TRUE(u.has_value()) << u.error().format(); + EXPECT_FALSE(u->provides.has_value()); + EXPECT_TRUE(u->requires_.empty()); + + std::filesystem::remove_all(dir); +} + TEST(Validate, ModuleNameNotRequiredToMatchPackageName) { // 0.0.10+: module name does NOT need to be prefixed by package name. // The library author decides the module naming convention.