diff --git a/docs/vcpkg-configuration.schema.json b/docs/vcpkg-configuration.schema.json index eb0aa2461d..bfc054c80e 100644 --- a/docs/vcpkg-configuration.schema.json +++ b/docs/vcpkg-configuration.schema.json @@ -35,6 +35,9 @@ "type": "string" } }, + "editable-ports": { + "$ref": "vcpkg-schema-definitions.schema.json#/definitions/editable-ports-config" + }, "requires": { "description": "Artifacts that are required for this package to function.", "$ref": "vcpkg-schema-definitions.schema.json#/definitions/artifact-references" diff --git a/docs/vcpkg-schema-definitions.schema.json b/docs/vcpkg-schema-definitions.schema.json index e18d70d631..b681ce8935 100644 --- a/docs/vcpkg-schema-definitions.schema.json +++ b/docs/vcpkg-schema-definitions.schema.json @@ -988,6 +988,28 @@ "format": "uri" } } + }, + "editable-ports-config": { + "type": "object", + "description": "Configuration for editable ports workflow. Allows building ports from local source with development-friendly paths.", + "properties": { + "path": { + "type": "string", + "description": "Directory containing editable ports (default: 'editable-ports'). Each port has subdirectories: port/ (portfiles), sources/ (src1/, src2/, etc.), build/ (artifacts), packages/ (output).", + "default": "editable-ports" + }, + "ports": { + "type": "array", + "description": "List of port names or patterns to treat as editable. Supports wildcards like 'boost*', 'zlib-*'. Matched ports are built from editable-ports//port/ with sources in editable-ports//sources/.", + "items": { + "$ref": "#/definitions/package-pattern" + } + } + }, + "patternProperties": { + "^\\$": {} + }, + "additionalProperties": false } } } diff --git a/include/vcpkg/base/contractual-constants.h b/include/vcpkg/base/contractual-constants.h index bf5cbbe106..4790347652 100644 --- a/include/vcpkg/base/contractual-constants.h +++ b/include/vcpkg/base/contractual-constants.h @@ -76,6 +76,8 @@ namespace vcpkg inline constexpr StringLiteral JsonIdOverlayPorts = "overlay-ports"; inline constexpr StringLiteral JsonIdOverlayTriplets = "overlay-triplets"; inline constexpr StringLiteral JsonIdOverrides = "overrides"; + inline constexpr StringLiteral JsonIdEditablePorts = "editable-ports"; + inline constexpr StringLiteral JsonIdPorts = "ports"; inline constexpr StringLiteral JsonIdPackages = "packages"; inline constexpr StringLiteral JsonIdPackageUnderscoreName = "package_name"; inline constexpr StringLiteral JsonIdPackageUnderscoreUrl = "package_url"; @@ -383,6 +385,8 @@ namespace vcpkg inline constexpr StringLiteral CMakeVariableDownloadMode = "VCPKG_DOWNLOAD_MODE"; inline constexpr StringLiteral CMakeVariableDownloads = "DOWNLOADS"; inline constexpr StringLiteral CMakeVariableEditable = "_VCPKG_EDITABLE"; + inline constexpr StringLiteral CMakeVariableEditableSubtree = "_VCPKG_EDITABLE_SUBTREE"; + inline constexpr StringLiteral CMakeVariableEditableSourcesPath = "_VCPKG_EDITABLE_SOURCES_PATH"; inline constexpr StringLiteral CMakeVariableEnvPassthrough = "VCPKG_ENV_PASSTHROUGH"; inline constexpr StringLiteral CMakeVariableEnvPassthroughUntracked = "VCPKG_ENV_PASSTHROUGH_UNTRACKED"; inline constexpr StringLiteral CMakeVariableFeatures = "FEATURES"; @@ -535,6 +539,9 @@ namespace vcpkg inline constexpr StringLiteral EnvironmentVariableVcpkgDisableMetrics = "VCPKG_DISABLE_METRICS"; inline constexpr StringLiteral EnvironmentVariableVcpkgDownloads = "VCPKG_DOWNLOADS"; inline constexpr StringLiteral EnvironmentVariableVcpkgFeatureFlags = "VCPKG_FEATURE_FLAGS"; + inline constexpr StringLiteral EnvironmentVariableVcpkgEditableMode = "VCPKG_EDITABLE_MODE"; + inline constexpr StringLiteral EditableModeIgnore = "ignore"; + inline constexpr StringLiteral EditableModeForbid = "forbid"; inline constexpr StringLiteral EnvironmentVariableVcpkgForceDownloadedBinaries = "VCPKG_FORCE_DOWNLOADED_BINARIES"; inline constexpr StringLiteral EnvironmentVariableVcpkgForceSystemBinaries = "VCPKG_FORCE_SYSTEM_BINARIES"; inline constexpr StringLiteral EnvironmentVariableVcpkgKeepEnvVars = "VCPKG_KEEP_ENV_VARS"; diff --git a/include/vcpkg/base/files.h b/include/vcpkg/base/files.h index 9e7a0bf704..fb2c946763 100644 --- a/include/vcpkg/base/files.h +++ b/include/vcpkg/base/files.h @@ -349,6 +349,12 @@ namespace vcpkg virtual bool set_executable(DiagnosticContext& context, const Path& target) const = 0; + // Copy a file while preserving its timestamp. Returns true on success. + bool copy_file_preserving_timestamp(const Path& source, const Path& dest) const; + + // Compare two files byte-by-byte. Returns true if files are identical. + bool files_are_identical(const Path& file1, const Path& file2) const; + using ReadOnlyFilesystem::current_path; virtual void current_path(const Path& new_current_path, std::error_code&) const = 0; void current_path(const Path& new_current_path, LineInfo li) const; diff --git a/include/vcpkg/configuration.h b/include/vcpkg/configuration.h index 60587ea965..211e8591a8 100644 --- a/include/vcpkg/configuration.h +++ b/include/vcpkg/configuration.h @@ -45,6 +45,18 @@ namespace vcpkg StringView pretty_location() const; }; + /// + /// Configuration for editable ports workflow. + /// + struct EditablePortsConfig + { + std::string path; // Directory containing editable ports (default: "editable-ports") + std::vector ports; // List of port names to treat as editable + + Path get_editable_ports_path(const Path& config_dir) const; + bool is_port_editable(StringView port_name) const; + }; + struct Configuration { Optional default_reg; @@ -53,6 +65,7 @@ namespace vcpkg Json::Object extra_info; std::vector overlay_ports; std::vector overlay_triplets; + Optional editable_ports; Json::Object serialize() const; void validate_as_active() const; diff --git a/include/vcpkg/dependencies.h b/include/vcpkg/dependencies.h index 2ef66b000f..fce11cfdab 100644 --- a/include/vcpkg/dependencies.h +++ b/include/vcpkg/dependencies.h @@ -89,12 +89,17 @@ namespace vcpkg UseHeadVersion use_head_version; Editable editable; + EditableSubtree editable_subtree; // True if this port or any dependency is editable std::map> feature_dependencies; std::vector dependency_diagnostics; Optional abi_info; Path package_dir; + + // For editable ports: paths to editable directories + Optional editable_sources_path; // e.g. editable-ports/zlib/sources (base for src1, src2, etc.) + Optional editable_build_dir; // e.g. editable-ports/zlib/build }; struct NotInstalledAction : BasicAction diff --git a/include/vcpkg/fwd/build.h b/include/vcpkg/fwd/build.h index 8c8ebd6637..05149b77b3 100644 --- a/include/vcpkg/fwd/build.h +++ b/include/vcpkg/fwd/build.h @@ -67,6 +67,12 @@ namespace vcpkg Yes }; + enum class EditableSubtree + { + No = 0, + Yes + }; + enum class BackcompatFeatures { Allow = 0, diff --git a/include/vcpkg/fwd/configuration.h b/include/vcpkg/fwd/configuration.h index ad8bd1d488..1abdec1869 100644 --- a/include/vcpkg/fwd/configuration.h +++ b/include/vcpkg/fwd/configuration.h @@ -6,6 +6,7 @@ namespace vcpkg struct ConfigurationAndSource; struct RegistryConfig; struct ManifestConfiguration; + struct EditablePortsConfig; enum class ConfigurationSource { diff --git a/src/vcpkg/base/files.cpp b/src/vcpkg/base/files.cpp index 1f39d77eb0..d854d983d2 100644 --- a/src/vcpkg/base/files.cpp +++ b/src/vcpkg/base/files.cpp @@ -2446,6 +2446,40 @@ namespace vcpkg return result; } + bool Filesystem::copy_file_preserving_timestamp(const Path& source, const Path& dest) const + { + std::error_code ec; + const auto timestamp = this->last_write_time(source, ec); + if (ec) return false; + + this->create_directories(dest.parent_path(), ec); + if (ec) return false; + + this->copy_file(source, dest, CopyOptions::overwrite_existing, ec); + if (ec) return false; + + this->last_write_time(null_diagnostic_context, dest, timestamp); + return true; + } + + bool Filesystem::files_are_identical(const Path& file1, const Path& file2) const + { + std::error_code ec; + const auto size1 = this->file_size(file1, ec); + if (ec) return false; + const auto size2 = this->file_size(file2, ec); + if (ec) return false; + + if (size1 != size2) return false; + + auto contents1 = this->read_contents(file1, ec); + if (ec) return false; + auto contents2 = this->read_contents(file2, ec); + if (ec) return false; + + return contents1 == contents2; + } + void Filesystem::write_lines(const Path& file_path, const std::vector& lines, LineInfo li) const { std::error_code ec; diff --git a/src/vcpkg/binarycaching.cpp b/src/vcpkg/binarycaching.cpp index 3689dcbf58..789829be3b 100644 --- a/src/vcpkg/binarycaching.cpp +++ b/src/vcpkg/binarycaching.cpp @@ -2454,6 +2454,11 @@ namespace vcpkg statuses.clear(); for (size_t i = 0; i < actions.size(); ++i) { + // Skip editable ports and their dependents - they should always be built from source + if (actions[i].editable == Editable::Yes || actions[i].editable_subtree == EditableSubtree::Yes) + { + continue; + } if (auto abi = actions[i].package_abi()) { CacheStatus& status = m_status[*abi]; @@ -2900,6 +2905,11 @@ namespace vcpkg void BinaryCache::push_success(CleanPackages clean_packages, const InstallPlanAction& action) { + // Don't push editable ports or their dependents to binary cache + if (action.editable == Editable::Yes || action.editable_subtree == EditableSubtree::Yes) + { + return; + } if (auto abi = action.package_abi()) { bool restored; diff --git a/src/vcpkg/commands.build.cpp b/src/vcpkg/commands.build.cpp index 249efddaa7..feda773c5f 100644 --- a/src/vcpkg/commands.build.cpp +++ b/src/vcpkg/commands.build.cpp @@ -893,9 +893,11 @@ namespace vcpkg std::string all_post_portfile_includes = Strings::join(";", Util::fmap(post_portfile_includes, [](const Path& p) { return p.generic_u8string(); })); + // For editable ports, use editable_build_dir instead of buildtrees + const Path build_dir = action.editable_build_dir.value_or(paths.build_dir(port_name)); std::vector variables{ {CMakeVariableAllFeatures, all_features}, - {CMakeVariableCurrentBuildtreesDir, paths.build_dir(port_name)}, + {CMakeVariableCurrentBuildtreesDir, build_dir}, {CMakeVariableCurrentPackagesDir, action.package_dir}, {CMakeVariableCurrentPortDir, scfl.port_directory()}, {CMakeVariableHostTriplet, host_triplet.canonical_name()}, @@ -904,11 +906,18 @@ namespace vcpkg {CMakeVariableVersion, scf.to_version().text}, {CMakeVariableUseHeadVersion, Util::Enum::to_bool(action.use_head_version) ? "1" : "0"}, {CMakeVariableEditable, Util::Enum::to_bool(action.editable) ? "1" : "0"}, + {CMakeVariableEditableSubtree, Util::Enum::to_bool(action.editable_subtree) ? "1" : "0"}, {CMakeVariableNoDownloads, !Util::Enum::to_bool(build_options.allow_downloads) ? "1" : "0"}, {CMakeVariableZChainloadToolchainFile, action.pre_build_info(VCPKG_LINE_INFO).toolchain_file()}, {CMakeVariableZPostPortfileIncludes, all_post_portfile_includes}, }; + // Pass editable sources path to CMake macros (base directory for src1, src2, etc.) + if (auto* editable_sources = action.editable_sources_path.get()) + { + variables.emplace_back(CMakeVariableEditableSourcesPath, *editable_sources); + } + if (auto cmake_debug = args.cmake_debug.get()) { if (cmake_debug->is_port_affected(port_name)) @@ -1361,10 +1370,11 @@ namespace vcpkg Debug::print("Binary caching for package ", action.spec, " is disabled due to --head\n"); return; } + // Note: For editable ports, we still compute ABI (including source hashes) so that + // dependencies can compute their ABI. Binary cache push/pull is disabled elsewhere. if (action.editable == Editable::Yes) { Debug::print("Binary caching for package ", action.spec, " is disabled due to --editable\n"); - return; } abi_info.compiler_info = &paths.get_compiler_info(*abi_info.pre_build_info, toolset); @@ -1462,6 +1472,68 @@ namespace vcpkg Util::Vectors::append(abi_tag_entries, port_dir_cache_entry.abi_entries); + // For editable ports, compute a single hash for all sources (sources/src1, sources/src2, etc.) + auto* editable_sources_for_abi = action.editable_sources_path.get(); + if (action.editable == Editable::Yes && editable_sources_for_abi) + { + const auto& sources_path = *editable_sources_for_abi; + if (fs.exists(sources_path, IgnoreErrors{})) + { + const ElapsedTimer hash_timer; + auto source_files = fs.get_regular_files_recursive_lexically_proximate(sources_path, VCPKG_LINE_INFO); + // Exclude .git directories (match path component, not substring) + Util::erase_remove_if(source_files, [](const Path& p) { + const auto& native = p.native(); + // Match ".git" as a full path component: starts with ".git/" or contains "/.git/" or "\.git/" + if (native.size() >= 4 && native[0] == '.' && native[1] == 'g' && native[2] == 'i' && + native[3] == 't' && (native.size() == 4 || native[4] == '/' || native[4] == '\\')) + { + return true; + } + return native.find("/.git/") != std::string::npos || native.find("\\.git\\") != std::string::npos || + native.find("/.git\\") != std::string::npos || native.find("\\.git/") != std::string::npos; + }); + + // Sort for deterministic hash + std::sort(source_files.begin(), source_files.end()); + + // Compute combined hash: concatenate "path|hash\n" for each file, then hash the result + std::string combined_hashes; + size_t files_hashed = 0; + for (const Path& rel_source_file : source_files) + { + const Path abs_source_file = sources_path / rel_source_file; + auto hash = Hash::get_file_hash(fs, abs_source_file, Hash::Algorithm::Sha256); + if (hash) + { + combined_hashes += rel_source_file.native(); + combined_hashes += "|"; + combined_hashes += hash.value_or_exit(VCPKG_LINE_INFO); + combined_hashes += "\n"; + ++files_hashed; + } + } + + // Single ABI entry for entire sources directory + auto sources_hash = Hash::get_string_sha256(combined_hashes); + abi_tag_entries.emplace_back("editable_sources", sources_hash); + Debug::print("Editable sources hash: ", + sources_hash, + " (", + std::to_string(files_hashed), + " files) in ", + std::to_string(hash_timer.us_64() / 1000), + "ms [", + sources_path, + "]\n"); + } + else + { + abi_tag_entries.emplace_back("editable_sources", "0"); + Debug::print("Editable sources hash: initializing.\n"); + } + } + for (size_t i = 0; i < abi_info.pre_build_info->post_portfile_includes.size(); ++i) { auto& file = abi_info.pre_build_info->post_portfile_includes[i]; diff --git a/src/vcpkg/commands.install.cpp b/src/vcpkg/commands.install.cpp index 8d5f35c082..5b7b682ea4 100644 --- a/src/vcpkg/commands.install.cpp +++ b/src/vcpkg/commands.install.cpp @@ -32,11 +32,40 @@ #include #include +#include namespace { using namespace vcpkg; + // Editable port source info parsed from portfile.cmake + // Initialize an editable port: copy port files to editable-ports//port/ folder + // Structure: + // editable-ports//port/ <- port files (portfile.cmake, vcpkg.json, etc.) + // editable-ports//sources/ <- source code (src1/, src2/, etc. for multi-source ports) + // editable-ports//build/ <- build artifacts + // editable-ports//packages/ <- package output + // Note: Source handling is done by CMake macros (vcpkg_from_github, etc.) + // which check _VCPKG_EDITABLE flag and use local source if available + void initialize_editable_port(const Filesystem& fs, + const SourceControlFileAndLocation& scfl, + const Path& editable_port_dir) + { + const auto port_dir = scfl.port_directory(); + const auto port_name = port_dir.filename().to_string(); + + msg::println(Color::success, LocalizedString::from_raw("Initializing editable port: " + port_name)); + + // Copy all port files to /port/ + const auto port_files_path = editable_port_dir / "port"; + fs.create_directories(port_files_path, VCPKG_LINE_INFO); + fs.copy_regular_recursive(port_dir, port_files_path, VCPKG_LINE_INFO); + + msg::println(LocalizedString::from_raw(" Port files copied to: " + port_files_path.native())); + msg::println(LocalizedString::from_raw(" Sources will be cloned automatically on first build to: " + + (editable_port_dir / "sources").native())); + } + struct InstalledFile { std::string file_path; @@ -69,6 +98,24 @@ namespace } }; + // Restore timestamp from old file to new file if they are identical + void restore_timestamp_if_unchanged(const Filesystem& fs, const Path& new_file, const Path& old_file_in_temp) + { + if (!fs.exists(old_file_in_temp, IgnoreErrors{})) return; + if (!fs.exists(new_file, IgnoreErrors{})) return; + + if (fs.files_are_identical(new_file, old_file_in_temp)) + { + std::error_code ec; + const auto old_timestamp = fs.last_write_time(old_file_in_temp, ec); + if (!ec) + { + fs.last_write_time(null_diagnostic_context, new_file, old_timestamp); + Debug::println("Restored timestamp for unchanged file: ", new_file); + } + } + } + static constexpr StringLiteral SYMLINK_STATUS = "symlink_status"; static constexpr StringLiteral STATUS = "status"; } @@ -622,6 +669,51 @@ namespace vcpkg size_t action_index = 1; auto& fs = paths.get_filesystem(); + const auto& installed = paths.installed(); + + // Create temporary directory for storing old files + const auto temp_base = fs.create_or_get_temp_directory(VCPKG_LINE_INFO) / "vcpkg-incremental-install"; + // Clear any existing temp directory from previous runs + fs.remove_all(temp_base, IgnoreErrors{}); + fs.create_directories(temp_base, VCPKG_LINE_INFO); + + // For each package to remove, copy its files to temp directory preserving timestamps + // (files stay in installed/ so remove_package can clean them up normally) + std::unordered_map temp_package_dirs; + for (const auto& action : action_plan.remove_actions) + { + auto maybe_ipv = status_db.get_installed_package_view(action.spec); + if (auto ipv = maybe_ipv.get()) + { + auto maybe_lines = fs.read_lines(installed.listfile_path(ipv->core->package)); + if (auto lines = maybe_lines.get()) + { + // Create subfolder named like the listfile: __ + const auto& spec = action.spec; + const auto temp_pkg_dir = temp_base / ipv->core->package.fullstem(); + fs.create_directories(temp_pkg_dir, VCPKG_LINE_INFO); + temp_package_dirs[spec] = temp_pkg_dir; + + Debug::println("Copying old files for ", spec, " to temp: ", temp_pkg_dir); + + // Copy each file to temp, preserving timestamps + for (const auto& suffix : *lines) + { + if (suffix.empty() || suffix.back() == '/') continue; // Skip directories + + const auto source = installed.root() / suffix; + const auto dest = temp_pkg_dir / suffix; + + if (fs.copy_file_preserving_timestamp(source, dest)) + { + Debug::println(" Copied: ", suffix); + } + } + } + } + } + + // Process removals for (auto&& action : action_plan.remove_actions) { msg::println(msgRemovingPackage, @@ -630,7 +722,7 @@ namespace vcpkg msg::spec = action.spec); ++action_index; const auto& remove_summary = - summary.removed_results.emplace_back(remove_package(fs, paths.installed(), action.spec, status_db)); + summary.removed_results.emplace_back(remove_package(fs, installed, action.spec, status_db)); msg::println(msgElapsedForPackage, msg::spec = remove_summary.build_result.spec, msg::elapsed = remove_summary.timing); @@ -648,6 +740,7 @@ namespace vcpkg nullptr); } + // Install packages and restore timestamps for unchanged files for (auto&& action : action_plan.install_actions) { binary_cache.print_updates(); @@ -666,6 +759,30 @@ namespace vcpkg args, paths, host_triplet, build_options, action, status_db, binary_cache, build_logs_recorder)); if (result.build_result.code == BuildResult::Succeeded) { + // For reinstalled packages, restore timestamps for unchanged files + auto temp_it = temp_package_dirs.find(action.spec); + if (temp_it != temp_package_dirs.end()) + { + const auto& temp_pkg_dir = temp_it->second; + if (auto bcf = result.build_result.binary_control_file.get()) + { + auto maybe_lines = fs.read_lines(installed.listfile_path(bcf->core_paragraph)); + if (auto lines = maybe_lines.get()) + { + Debug::println("Checking for unchanged files in ", action.spec); + for (const auto& suffix : *lines) + { + if (suffix.empty() || suffix.back() == '/') continue; // Skip directories + + const auto new_file = installed.root() / suffix; + const auto old_file = temp_pkg_dir / suffix; + + restore_timestamp_if_unchanged(fs, new_file, old_file); + } + } + } + } + const auto& scfl = action.source_control_file_and_location(); const auto& scf = *scfl.source_control_file; auto& license = scf.core_paragraph->license; @@ -738,6 +855,10 @@ namespace vcpkg msg::println(msgElapsedForPackage, msg::spec = action.spec, msg::elapsed = result.timing); } + // Clean up temporary directory + Debug::println("Cleaning up temporary directory: ", temp_base); + fs.remove_all(temp_base, VCPKG_LINE_INFO); + database_load_collapse(fs, paths.installed()); summary.elapsed = timer.elapsed(); return summary; @@ -1413,6 +1534,108 @@ namespace vcpkg Util::erase_remove_if(install_plan.install_actions, [&toplevel](auto&& action) { return action.spec == toplevel; }); + // Check configuration for editable ports + const auto& config = paths.get_configuration(); + const auto& editable_config = config.config.editable_ports; + const auto config_dir = config.directory; + + const bool has_editable_config = + editable_config.has_value() && !editable_config.get()->ports.empty(); + const auto editable_mode_env = + get_environment_variable(EnvironmentVariableVcpkgEditableMode).value_or(""); + + if (has_editable_config && editable_mode_env == EditableModeForbid) + { + Checks::msg_exit_with_error( + VCPKG_LINE_INFO, + LocalizedString::from_raw( + "Editable mode is configured but VCPKG_EDITABLE_MODE=forbid.\n" + "Remove the editable-ports configuration from vcpkg-configuration.json " + "or unset the environment variable.")); + } + + const bool editable_active = has_editable_config && editable_mode_env != EditableModeIgnore; + + if (editable_mode_env == EditableModeIgnore && has_editable_config) + { + msg::println(Color::warning, + LocalizedString::from_raw("Note: Editable mode configuration found but ignored " + "(VCPKG_EDITABLE_MODE=ignore).")); + } + + // Print warning if editable mode is active + if (editable_active) + { + msg::println(Color::warning, + LocalizedString::from_raw("\n" + "=============== EDITABLE MODE ENABLED ===============\n" + "Editable ports are experimental and may cause:\n" + " - Inconsistent builds between machines\n" + " - Binary caching disabled for editable ports\n" + " - Sources cloned to editable-ports//sources/\n" + "Use for development only, not production builds.\n" + "======================================================\n")); + } + + for (InstallPlanAction& action : install_plan.install_actions) + { + const auto& port_name = action.spec.name(); + const bool port_is_editable = + editable_active && editable_config.get()->is_port_editable(port_name); + + if (port_is_editable) + { + action.editable = Editable::Yes; + + msg::println(Color::success, LocalizedString::from_raw("Editable port: " + port_name)); + + const auto editable_ports_path = editable_config.get()->get_editable_ports_path(config_dir); + const auto editable_port_path = editable_ports_path / port_name; + action.editable_sources_path = editable_port_path / "sources"; + action.editable_build_dir = editable_port_path / "build"; + // Override package_dir to use editable location + action.package_dir = editable_port_path / "packages"; + + // Initialize if port directory doesn't exist yet + if (!fs.exists(editable_port_path, IgnoreErrors{})) + { + initialize_editable_port(fs, action.source_control_file_and_location(), editable_port_path); + } + else + { + msg::println(LocalizedString::from_raw(" Using existing editable port at: " + + editable_port_path.native())); + } + } + } + + // Compute editable subtree: mark ports that are editable or have editable dependencies + // The install plan is topologically sorted (dependencies first), so we can propagate forward + std::set editable_subtree_ports; + for (InstallPlanAction& action : install_plan.install_actions) + { + bool in_subtree = (action.editable == Editable::Yes); + + // Check if any dependency is in the editable subtree + if (!in_subtree) + { + for (const auto& dep_spec : action.package_dependencies) + { + if (editable_subtree_ports.count(dep_spec.name())) + { + in_subtree = true; + break; + } + } + } + + if (in_subtree) + { + editable_subtree_ports.insert(action.spec.name()); + action.editable_subtree = EditableSubtree::Yes; + } + } + command_set_installed_and_exit_ex(args, paths, host_triplet, diff --git a/src/vcpkg/commands.upgrade.cpp b/src/vcpkg/commands.upgrade.cpp index 48fac0c7be..38965f63b2 100644 --- a/src/vcpkg/commands.upgrade.cpp +++ b/src/vcpkg/commands.upgrade.cpp @@ -60,7 +60,7 @@ namespace vcpkg ? UnsupportedPortAction::Warn : UnsupportedPortAction::Error; - static const BuildPackageOptions build_options{ + const BuildPackageOptions build_options{ BuildMissing::Yes, AllowDownloads::Yes, OnlyDownloads::No, diff --git a/src/vcpkg/configuration.cpp b/src/vcpkg/configuration.cpp index 6d75943108..0c6446acce 100644 --- a/src/vcpkg/configuration.cpp +++ b/src/vcpkg/configuration.cpp @@ -7,6 +7,7 @@ #include #include +#include #include namespace @@ -335,6 +336,34 @@ namespace }; DemandsDeserializer DemandsDeserializer::instance; + struct EditablePortsConfigDeserializer final : Json::IDeserializer + { + LocalizedString type_name() const override { return msg::format(msgAConfigurationObject); } + + View valid_fields() const noexcept override + { + static constexpr StringLiteral fields[] = { + JsonIdPath, + JsonIdPorts, + }; + return fields; + } + + Optional visit_object(Json::Reader& r, const Json::Object& obj) const override + { + EditablePortsConfig ret; + r.optional_object_field(obj, JsonIdPath, ret.path, Json::UntypedStringDeserializer::instance); + // Use PackagePatternArrayDeserializer to support wildcards like "boost*", "stlab-*" + std::vector declarations; + r.optional_object_field(obj, JsonIdPorts, declarations, PackagePatternArrayDeserializer::instance); + ret.ports = Util::fmap(declarations, [](auto&& decl) { return decl.pattern; }); + return ret; + } + + static const EditablePortsConfigDeserializer instance; + }; + const EditablePortsConfigDeserializer EditablePortsConfigDeserializer::instance; + struct ConfigurationDeserializer final : Json::IDeserializer { virtual LocalizedString type_name() const override { return msg::format(msgAConfigurationObject); } @@ -535,6 +564,8 @@ namespace r.optional_object_field(obj, JsonIdOverlayPorts, ret.overlay_ports, OverlayPathArrayDeserializer::instance); r.optional_object_field( obj, JsonIdOverlayTriplets, ret.overlay_triplets, OverlayTripletsPathArrayDeserializer::instance); + r.optional_object_field_emplace( + obj, JsonIdEditablePorts, ret.editable_ports, EditablePortsConfigDeserializer::instance); RegistryConfig* default_registry = r.optional_object_field_emplace( obj, JsonIdDefaultRegistry, ret.default_reg, RegistryConfigDeserializer::instance); @@ -753,6 +784,7 @@ namespace vcpkg JsonIdRegistries, JsonIdOverlayPorts, JsonIdOverlayTriplets, + JsonIdEditablePorts, JsonIdMessage, JsonIdWarning, JsonIdError, @@ -1014,6 +1046,19 @@ namespace vcpkg return out; } + // EditablePortsConfig implementation + Path EditablePortsConfig::get_editable_ports_path(const Path& config_dir) const + { + return config_dir / (path.empty() ? "editable-ports" : path); + } + + bool EditablePortsConfig::is_port_editable(StringView port_name) const + { + return std::any_of(ports.begin(), ports.end(), [&](const std::string& pattern) { + return package_pattern_match(port_name, pattern) != 0; + }); + } + bool is_package_pattern(StringView sv) { if (Json::IdentifierDeserializer::is_ident(sv)) diff --git a/src/vcpkg/dependencies.cpp b/src/vcpkg/dependencies.cpp index 9e05472bde..d00b099a46 100644 --- a/src/vcpkg/dependencies.cpp +++ b/src/vcpkg/dependencies.cpp @@ -528,6 +528,7 @@ namespace vcpkg , default_features(default_features) , use_head_version(use_head_version) , editable(editable) + , editable_subtree(EditableSubtree::No) , feature_dependencies(std::move(dependencies)) , dependency_diagnostics(std::move(dependency_diagnostics)) , package_dir(packages_dir_assigner.generate(spec)) diff --git a/src/vcpkg/vcpkgpaths.cpp b/src/vcpkg/vcpkgpaths.cpp index bb07a8fed3..621a414489 100644 --- a/src/vcpkg/vcpkgpaths.cpp +++ b/src/vcpkg/vcpkgpaths.cpp @@ -124,19 +124,59 @@ namespace } } - // Merges overlay settings from the 3 major sources in the usual priority order, where command line wins first, then - // manifest, then environment. The parameter order is specifically chosen to group information that comes from the - // manifest together and make parameter order confusion less likely to compile. + // Scans the editable-ports directory for subdirectories matching any configured pattern, + // returning their port/ subpaths as overlay paths (sorted for deterministic order). + static std::vector compute_editable_overlays(const ReadOnlyFilesystem& fs, + const Optional& editable_ports, + const Path& config_directory) + { + std::vector result; + if (get_environment_variable(EnvironmentVariableVcpkgEditableMode).value_or("") == EditableModeIgnore) + { + return result; + } + if (auto editable_config = editable_ports.get()) + { + if (!editable_config->ports.empty()) + { + auto editable_base_path = editable_config->get_editable_ports_path(config_directory); + for (auto& port_dir : fs.get_directories_non_recursive(editable_base_path, IgnoreErrors{})) + { + auto port_name = port_dir.filename().to_string(); + for (const auto& pattern : editable_config->ports) + { + if (package_pattern_match(port_name, pattern) != 0) + { + auto port_overlay_path = port_dir / "port"; + if (fs.exists(port_overlay_path, IgnoreErrors{})) + { + result.push_back(port_overlay_path); + Debug::print("Added editable-ports overlay: ", port_overlay_path, '\n'); + } + break; // No need to check other patterns for this port + } + } + } + std::sort(result.begin(), result.end()); + } + } + return result; + } + + // Merges overlay settings from all sources in priority order: editable (highest), then command line, then + // config, then environment (lowest). The parameter order is specifically chosen to group information that comes + // from the manifest together and make parameter order confusion less likely to compile. static std::vector merge_overlays(const ReadOnlyFilesystem& fs, const std::vector& cli_overlays, const std::vector& env_overlays, const Path& original_cwd, bool forbid_config_dot, const std::vector& config_overlays, - const Path& config_directory) + const Path& config_directory, + std::vector&& editable_overlays = {}) { - std::vector result; - result.reserve(cli_overlays.size() + config_overlays.size() + env_overlays.size()); + std::vector result = std::move(editable_overlays); + result.reserve(result.size() + cli_overlays.size() + config_overlays.size() + env_overlays.size()); append_overlays(result, fs, cli_overlays, original_cwd, config_directory, false); append_overlays(result, fs, config_overlays, config_directory, config_directory, forbid_config_dot); append_overlays(result, fs, env_overlays, original_cwd, config_directory, false); @@ -685,13 +725,17 @@ namespace vcpkg m_pimpl->m_config = merge_validate_configs( std::move(maybe_manifest_config), m_pimpl->m_manifest_dir, std::move(maybe_json_config), config_dir, *this); - overlay_ports.overlay_ports = merge_overlays(m_pimpl->m_fs, - args.cli_overlay_ports, - args.env_overlay_ports, - original_cwd, - true, - m_pimpl->m_config.config.overlay_ports, - m_pimpl->m_config.directory); + overlay_ports.overlay_ports = + merge_overlays(m_pimpl->m_fs, + args.cli_overlay_ports, + args.env_overlay_ports, + original_cwd, + true, + m_pimpl->m_config.config.overlay_ports, + m_pimpl->m_config.directory, + compute_editable_overlays( + m_pimpl->m_fs, m_pimpl->m_config.config.editable_ports, m_pimpl->m_config.directory)); + overlay_triplets = merge_overlays(m_pimpl->m_fs, args.cli_overlay_triplets, args.env_overlay_triplets,