Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/vcpkg-configuration.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
22 changes: 22 additions & 0 deletions docs/vcpkg-schema-definitions.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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>/port/ with sources in editable-ports/<port>/sources/.",
"items": {
"$ref": "#/definitions/package-pattern"
}
}
},
"patternProperties": {
"^\\$": {}
},
"additionalProperties": false
}
}
}
7 changes: 7 additions & 0 deletions include/vcpkg/base/contractual-constants.h
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down
6 changes: 6 additions & 0 deletions include/vcpkg/base/files.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
13 changes: 13 additions & 0 deletions include/vcpkg/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ namespace vcpkg
StringView pretty_location() const;
};

/// <summary>
/// Configuration for editable ports workflow.
/// </summary>
struct EditablePortsConfig
{
std::string path; // Directory containing editable ports (default: "editable-ports")
std::vector<std::string> 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<RegistryConfig> default_reg;
Expand All @@ -53,6 +65,7 @@ namespace vcpkg
Json::Object extra_info;
std::vector<std::string> overlay_ports;
std::vector<std::string> overlay_triplets;
Optional<EditablePortsConfig> editable_ports;

Json::Object serialize() const;
void validate_as_active() const;
Expand Down
5 changes: 5 additions & 0 deletions include/vcpkg/dependencies.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string, std::vector<FeatureSpec>> feature_dependencies;
std::vector<DiagnosticLine> dependency_diagnostics;

Optional<AbiInfo> abi_info;
Path package_dir;

// For editable ports: paths to editable directories
Optional<Path> editable_sources_path; // e.g. editable-ports/zlib/sources (base for src1, src2, etc.)
Optional<Path> editable_build_dir; // e.g. editable-ports/zlib/build
};

struct NotInstalledAction : BasicAction
Expand Down
6 changes: 6 additions & 0 deletions include/vcpkg/fwd/build.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ namespace vcpkg
Yes
};

enum class EditableSubtree
{
No = 0,
Yes
};

enum class BackcompatFeatures
{
Allow = 0,
Expand Down
1 change: 1 addition & 0 deletions include/vcpkg/fwd/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace vcpkg
struct ConfigurationAndSource;
struct RegistryConfig;
struct ManifestConfiguration;
struct EditablePortsConfig;

enum class ConfigurationSource
{
Expand Down
34 changes: 34 additions & 0 deletions src/vcpkg/base/files.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string>& lines, LineInfo li) const
{
std::error_code ec;
Expand Down
10 changes: 10 additions & 0 deletions src/vcpkg/binarycaching.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down Expand Up @@ -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;
Expand Down
76 changes: 74 additions & 2 deletions src/vcpkg/commands.build.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<CMakeVariable> 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()},
Expand All @@ -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))
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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];
Expand Down
Loading