Line data Source code
1 : /*! 2 : * \file esys/repo/manifest/merge_manifest.cpp 3 : * \brief Flatten-merge manifests (ADR-0038) 4 : * 5 : * \cond 6 : * __legal_b__ 7 : * 8 : * Copyright (c) 2026 Michel Gillet 9 : * Distributed under the MIT License. 10 : * (See accompanying file LICENSE.txt or 11 : * copy at https://opensource.org/licenses/MIT) 12 : * 13 : * __legal_e__ 14 : * \endcond 15 : * 16 : */ 17 : 18 : #include "esys/repo/esysrepo_prec.h" 19 : #include "esys/repo/manifest/merge.h" 20 : #include "esys/repo/manifest/depends.h" 21 : #include "esys/repo/manifest/group.h" 22 : #include "esys/repo/manifest/location.h" 23 : #include "esys/repo/manifest/repository.h" 24 : #include "esys/repo/manifest/revisionset.h" 25 : #include "esys/repo/manifest/xmlfile.h" 26 : #include "esys/repo/grepo/manifest.h" 27 : #include "esys/repo/resultcode.h" 28 : 29 : #include <boost/filesystem.hpp> 30 : 31 : #include <fstream> 32 : #include <functional> 33 : 34 : namespace esys::repo::manifest 35 : { 36 : 37 : namespace 38 : { 39 : 40 2 : bool path_has_dir_sep_suffix(const std::string &path) 41 : { 42 2 : if (path.empty()) return false; 43 2 : const char last = path.back(); 44 2 : return last == '/' || last == '\\'; 45 : } 46 : 47 8 : bool file_looks_native(const std::string &path) 48 : { 49 8 : std::ifstream ifs(path); 50 8 : if (!ifs) return false; 51 8 : std::string chunk(4096, '\0'); 52 8 : ifs.read(chunk.data(), static_cast<std::streamsize>(chunk.size())); 53 8 : chunk.resize(static_cast<std::size_t>(ifs.gcount())); 54 8 : return chunk.find("esysrepo_manifest") != std::string::npos; 55 8 : } 56 : 57 : } // namespace 58 : 59 3 : const std::string &Merge::default_native_filename() 60 : { 61 3 : static const std::string name = ".esysrepo.manifest.xml"; 62 3 : return name; 63 : } 64 : 65 0 : const std::string &Merge::default_google_filename() 66 : { 67 0 : static const std::string name = "default.xml"; 68 0 : return name; 69 : } 70 : 71 3 : Result Merge::resolve_output_path(const std::string &output, OutputFormat format, std::string &resolved) 72 : { 73 3 : resolved.clear(); 74 3 : if (output.empty()) return ESYSREPO_RESULT(ResultCode::OK); 75 : 76 2 : const std::string &def_name = 77 2 : (format == OutputFormat::Google) ? default_google_filename() : default_native_filename(); 78 : 79 2 : boost::system::error_code ec; 80 2 : const bool as_dir = path_has_dir_sep_suffix(output); 81 2 : boost::filesystem::path out(output); 82 : 83 2 : if (as_dir) 84 : { 85 0 : boost::filesystem::create_directories(out, ec); 86 0 : if (ec) return ESYSREPO_RESULT(ResultCode::FOLDER_CREATION_ERROR, output); 87 0 : resolved = (out / def_name).lexically_normal().string(); 88 0 : return ESYSREPO_RESULT(ResultCode::OK); 89 : } 90 : 91 3 : if (boost::filesystem::exists(out) && boost::filesystem::is_directory(out)) 92 : { 93 3 : resolved = (out / def_name).lexically_normal().string(); 94 1 : return ESYSREPO_RESULT(ResultCode::OK); 95 : } 96 : 97 2 : resolved = out.lexically_normal().string(); 98 1 : return ESYSREPO_RESULT(ResultCode::OK); 99 3 : } 100 : 101 4 : Merge::Merge() = default; 102 : 103 8 : Merge::~Merge() = default; 104 : 105 8 : void Merge::add_input(const std::string &path) 106 : { 107 8 : m_inputs.push_back(path); 108 8 : } 109 : 110 0 : void Merge::set_inputs(const std::vector<std::string> &paths) 111 : { 112 0 : m_inputs = paths; 113 0 : } 114 : 115 0 : const std::vector<std::string> &Merge::get_inputs() const 116 : { 117 0 : return m_inputs; 118 : } 119 : 120 2 : void Merge::set_output(const std::string &path) 121 : { 122 2 : m_output = path; 123 2 : } 124 : 125 0 : const std::string &Merge::get_output() const 126 : { 127 0 : return m_output; 128 : } 129 : 130 1 : const std::string &Merge::get_resolved_output() const 131 : { 132 1 : return m_resolved_output; 133 : } 134 : 135 1 : void Merge::set_on_conflict(OnConflict on_conflict) 136 : { 137 1 : m_on_conflict = on_conflict; 138 1 : } 139 : 140 0 : Merge::OnConflict Merge::get_on_conflict() const 141 : { 142 0 : return m_on_conflict; 143 : } 144 : 145 0 : void Merge::set_output_format(OutputFormat format) 146 : { 147 0 : m_output_format = format; 148 0 : } 149 : 150 0 : Merge::OutputFormat Merge::get_output_format() const 151 : { 152 0 : return m_output_format; 153 : } 154 : 155 0 : void Merge::set_parse_mode(ParseMode mode) 156 : { 157 0 : m_parse_mode = mode; 158 0 : } 159 : 160 0 : ParseMode Merge::get_parse_mode() const 161 : { 162 0 : return m_parse_mode; 163 : } 164 : 165 5 : std::shared_ptr<Manifest> Merge::get_result() const 166 : { 167 5 : return m_result; 168 : } 169 : 170 1 : Result Merge::conflict(const std::string &msg) 171 : { 172 1 : return ESYSREPO_RESULT(ResultCode::MANIFEST_MERGE_CONFLICT, msg); 173 : } 174 : 175 4 : bool Merge::same_location_identity(const Location &a, const Location &b) 176 : { 177 8 : return a.get_name() == b.get_name() && a.get_address() == b.get_address() 178 8 : && a.get_alt_address() == b.get_alt_address(); 179 : } 180 : 181 2 : bool Merge::same_repo_identity(const Repository &a, const Repository &b) 182 : { 183 2 : if (generic_repo_path(a.get_path()) != generic_repo_path(b.get_path())) return false; 184 2 : if (a.get_name() != b.get_name()) return false; 185 2 : if (a.get_revision() != b.get_revision()) return false; 186 0 : if (a.get_location_str() != b.get_location_str()) return false; 187 0 : if (a.get_url() != b.get_url()) return false; 188 2 : if (a.get_sync_s() != b.get_sync_s()) return false; 189 0 : if (a.get_sharing() != b.get_sharing()) return false; 190 0 : if (a.get_workspace() != b.get_workspace()) return false; 191 0 : if (a.get_depends().size() != b.get_depends().size()) return false; 192 0 : for (std::size_t idx = 0; idx < a.get_depends().size(); ++idx) 193 : { 194 0 : if (*a.get_depends()[idx] != *b.get_depends()[idx]) return false; 195 : } 196 : return true; 197 : } 198 : 199 6 : std::shared_ptr<Repository> Merge::clone_repo(const Repository &src) 200 : { 201 6 : auto copy = std::make_shared<Repository>(src.get_name(), src.get_path()); 202 6 : copy->set_revision(src.get_revision()); 203 6 : copy->set_location(src.get_location_str()); 204 6 : copy->set_sync_s(src.get_sync_s()); 205 6 : copy->set_sharing(src.get_sharing()); 206 6 : copy->set_workspace(src.get_workspace()); 207 6 : for (const auto &dep : src.get_depends()) 208 : { 209 0 : if (dep == nullptr) continue; 210 0 : auto d = std::make_shared<Depends>(); 211 0 : d->set_selector(dep->get_selector()); 212 0 : d->set_value(dep->get_value()); 213 0 : d->set_line_number(dep->get_line_number()); 214 0 : copy->add_depends(d); 215 0 : } 216 6 : return copy; 217 0 : } 218 : 219 8 : Result Merge::load_file(const std::string &path, std::shared_ptr<Manifest> &out) 220 : { 221 8 : if (!boost::filesystem::exists(path)) return ESYSREPO_RESULT(ResultCode::FILE_NOT_EXISTING, path); 222 : 223 16 : out = std::make_shared<Manifest>(); 224 8 : const bool native = file_looks_native(path); 225 8 : if (native) 226 : { 227 8 : XMLFile xml; 228 16 : xml.set_data(out); 229 8 : xml.set_parse_mode(m_parse_mode); 230 8 : Result result = xml.read(path); 231 8 : if (result.error()) return ESYSREPO_RESULT(result); 232 8 : out->set_type(Type::ESYSREPO_MANIFEST); 233 8 : out->set_format(Format::XML); 234 8 : return ESYSREPO_RESULT(ResultCode::OK); 235 8 : } 236 : 237 0 : grepo::Manifest grepo; 238 0 : grepo.set_data(out); 239 0 : grepo.set_parse_mode(m_parse_mode); 240 0 : Result result = grepo.read(path); 241 0 : if (result.error()) return ESYSREPO_RESULT(result); 242 0 : out->set_type(Type::GOOGLE_MANIFEST); 243 0 : out->set_format(Format::XML); 244 0 : return ESYSREPO_RESULT(ResultCode::OK); 245 0 : } 246 : 247 4 : Result Merge::run() 248 : { 249 4 : m_result = std::make_shared<Manifest>(); 250 4 : m_result->set_format(Format::XML); 251 4 : m_all_inputs_google = true; 252 4 : m_resolved_output.clear(); 253 : 254 4 : if (m_inputs.size() < 2) 255 0 : return ESYSREPO_RESULT(ResultCode::CMD_MISSING_PARAMETERS, "manifest merge needs at least two input files"); 256 : 257 11 : for (const auto &path : m_inputs) 258 : { 259 8 : std::shared_ptr<Manifest> src; 260 8 : Result result = load_file(path, src); 261 8 : if (result.error()) return ESYSREPO_RESULT(result); 262 8 : if (src->get_type() != Type::GOOGLE_MANIFEST) m_all_inputs_google = false; 263 8 : if (!src->get_includes().empty()) 264 0 : return conflict("Google <include> is not flattened: '" + path + "'"); 265 8 : result = absorb(*src, path); 266 9 : if (result.error()) return ESYSREPO_RESULT(result); 267 16 : } 268 : 269 3 : if (m_output_format == OutputFormat::Google && !m_all_inputs_google) 270 0 : return ESYSREPO_RESULT(ResultCode::CMD_INCORRECT_PARAMETERS_IN_CONTEXT, 271 : "--format google requires every input to be Google XML"); 272 : 273 3 : if (m_output_format == OutputFormat::Google) 274 0 : m_result->set_type(Type::GOOGLE_MANIFEST); 275 : else 276 3 : m_result->set_type(Type::ESYSREPO_MANIFEST); 277 : 278 3 : Result result = m_result->validate_depends(); 279 3 : if (result.error()) return ESYSREPO_RESULT(result); 280 : 281 3 : if (m_result->get_locations().empty()) 282 0 : return ESYSREPO_RESULT(ResultCode::MANIFEST_NO_LOCATIONS_DEFINED); 283 : 284 3 : result = write_result(); 285 3 : if (result.error()) return ESYSREPO_RESULT(result); 286 3 : return ESYSREPO_RESULT(ResultCode::OK); 287 3 : } 288 : 289 3 : Result Merge::write_result() 290 : { 291 3 : Result result = resolve_output_path(m_output, m_output_format, m_resolved_output); 292 3 : if (result.error()) return ESYSREPO_RESULT(result); 293 3 : if (m_resolved_output.empty()) return ESYSREPO_RESULT(ResultCode::OK); 294 : 295 2 : std::shared_ptr<FileBase> file; 296 2 : if (m_output_format == OutputFormat::Google) 297 0 : file = std::make_shared<grepo::Manifest>(); 298 : else 299 4 : file = std::make_shared<XMLFile>(); 300 4 : file->set_data(m_result); 301 2 : return file->write(m_resolved_output); 302 3 : } 303 : 304 8 : Result Merge::absorb(const Manifest &src, const std::string &src_path) 305 : { 306 8 : Result result = absorb_defaults(src, src_path); 307 8 : if (result.error()) return ESYSREPO_RESULT(result); 308 : 309 15 : for (const auto &loc : src.get_locations()) 310 : { 311 8 : if (loc == nullptr) continue; 312 16 : result = absorb_location(loc, src_path); 313 9 : if (result.error()) return ESYSREPO_RESULT(result); 314 : } 315 : 316 7 : result = absorb_groups(src); 317 7 : if (result.error()) return ESYSREPO_RESULT(result); 318 7 : return absorb_revision_sets(src, src_path); 319 8 : } 320 : 321 8 : Result Merge::absorb_defaults(const Manifest &src, const std::string &src_path) 322 : { 323 32 : auto take_or_conflict = [this, &src_path](const std::string &field, const std::string &cur, const std::string &inc, 324 0 : const std::function<void(const std::string &)> &set) -> Result { 325 24 : if (inc.empty()) return ESYSREPO_RESULT(ResultCode::OK); 326 0 : if (cur.empty()) 327 : { 328 0 : set(inc); 329 0 : return ESYSREPO_RESULT(ResultCode::OK); 330 : } 331 0 : if (cur == inc) return ESYSREPO_RESULT(ResultCode::OK); 332 0 : if (m_on_conflict == OnConflict::PreferFirst) return ESYSREPO_RESULT(ResultCode::OK); 333 0 : if (m_on_conflict == OnConflict::PreferLast) 334 : { 335 0 : set(inc); 336 0 : return ESYSREPO_RESULT(ResultCode::OK); 337 : } 338 0 : return conflict(field + " differs in '" + src_path + "'"); 339 8 : }; 340 : 341 8 : if (src.get_kind() != Kind::NOT_SET && src.get_kind() != Kind::UNKNOWN) 342 : { 343 8 : if (m_result->get_kind() == Kind::NOT_SET || m_result->get_kind() == Kind::UNKNOWN) 344 4 : m_result->set_kind(src.get_kind()); 345 4 : else if (m_result->get_kind() != src.get_kind()) 346 : { 347 0 : if (m_on_conflict == OnConflict::Fail) return conflict("kind differs in '" + src_path + "'"); 348 0 : if (m_on_conflict == OnConflict::PreferLast) m_result->set_kind(src.get_kind()); 349 : } 350 : } 351 : 352 8 : const std::string dest_rev = (m_result->get_default_revision() == "master") ? "" : m_result->get_default_revision(); 353 8 : const std::string src_rev = (src.get_default_revision() == "master") ? "" : src.get_default_revision(); 354 8 : Result result = take_or_conflict( 355 16 : "default revision", dest_rev, src_rev, [this](const std::string &v) { m_result->set_default_revision(v); }); 356 8 : if (result.error()) return ESYSREPO_RESULT(result); 357 : 358 8 : if (src.get_default_job_count() != 1) 359 : { 360 0 : if (m_result->get_default_job_count() == 1) 361 0 : m_result->set_default_job_count(src.get_default_job_count()); 362 0 : else if (m_result->get_default_job_count() != src.get_default_job_count()) 363 : { 364 0 : if (m_on_conflict == OnConflict::Fail) return conflict("default sync-j differs in '" + src_path + "'"); 365 0 : if (m_on_conflict == OnConflict::PreferLast) m_result->set_default_job_count(src.get_default_job_count()); 366 : } 367 : } 368 : 369 8 : if (src.get_default_sync_s().has_value()) 370 : { 371 0 : if (!m_result->get_default_sync_s().has_value()) 372 0 : m_result->set_default_sync_s(src.get_default_sync_s()); 373 0 : else if (m_result->get_default_sync_s() != src.get_default_sync_s()) 374 : { 375 0 : if (m_on_conflict == OnConflict::Fail) return conflict("default sync-s differs in '" + src_path + "'"); 376 0 : if (m_on_conflict == OnConflict::PreferLast) m_result->set_default_sync_s(src.get_default_sync_s()); 377 : } 378 : } 379 : 380 16 : result = take_or_conflict("default location", m_result->get_default_location_str(), src.get_default_location_str(), 381 8 : [this](const std::string &v) { m_result->set_default_location(v); }); 382 8 : if (result.error()) return ESYSREPO_RESULT(result); 383 : 384 16 : result = take_or_conflict("default revision-set", m_result->get_default_revision_set(), 385 : src.get_default_revision_set(), 386 8 : [this](const std::string &v) { m_result->set_default_revision_set(v); }); 387 8 : if (result.error()) return ESYSREPO_RESULT(result); 388 : 389 8 : return ESYSREPO_RESULT(ResultCode::OK); 390 16 : } 391 : 392 8 : Result Merge::absorb_location(std::shared_ptr<Location> src_loc, const std::string &src_path) 393 : { 394 8 : auto dest_loc = m_result->find_location(src_loc->get_name()); 395 8 : if (dest_loc == nullptr) 396 : { 397 4 : dest_loc = std::make_shared<Location>(src_loc->get_name(), src_loc->get_address()); 398 4 : dest_loc->set_alt_address(src_loc->get_alt_address()); 399 12 : if (m_result->add_location(dest_loc) < 0) 400 0 : return ESYSREPO_RESULT(ResultCode::INTERNAL_ERROR, src_loc->get_name()); 401 : } 402 4 : else if (!same_location_identity(*dest_loc, *src_loc)) 403 : { 404 0 : if (m_on_conflict == OnConflict::Fail) 405 0 : return conflict("location '" + src_loc->get_name() + "' differs in '" + src_path + "'"); 406 0 : if (m_on_conflict == OnConflict::PreferLast) 407 : { 408 0 : dest_loc->set_address(src_loc->get_address()); 409 0 : dest_loc->set_alt_address(src_loc->get_alt_address()); 410 : } 411 : } 412 : 413 15 : for (const auto &repo : src_loc->get_repos()) 414 : { 415 8 : if (repo == nullptr) continue; 416 32 : Result result = absorb_repo(dest_loc, repo, src_path); 417 9 : if (result.error()) return ESYSREPO_RESULT(result); 418 8 : } 419 7 : return ESYSREPO_RESULT(ResultCode::OK); 420 8 : } 421 : 422 8 : Result Merge::absorb_repo(std::shared_ptr<Location> dest_loc, std::shared_ptr<Repository> src_repo, 423 : const std::string &src_path) 424 : { 425 8 : auto existing = m_result->find_repo_by_path(src_repo->get_path()); 426 8 : if (existing == nullptr) 427 : { 428 6 : dest_loc->add_repo(clone_repo(*src_repo)); 429 6 : return ESYSREPO_RESULT(ResultCode::OK); 430 : } 431 : 432 2 : if (existing->get_location() != nullptr && existing->get_location()->get_name() != dest_loc->get_name()) 433 0 : return conflict("path '" + generic_repo_path(src_repo->get_path()) + "' uses different locations in '" 434 0 : + src_path + "'"); 435 : 436 2 : if (same_repo_identity(*existing, *src_repo)) return ESYSREPO_RESULT(ResultCode::OK); 437 : 438 2 : if (m_on_conflict == OnConflict::PreferFirst) return ESYSREPO_RESULT(ResultCode::OK); 439 2 : if (m_on_conflict == OnConflict::PreferLast) 440 : { 441 1 : existing->set_name(src_repo->get_name()); 442 1 : existing->set_revision(src_repo->get_revision()); 443 1 : existing->set_location(src_repo->get_location_str()); 444 1 : existing->set_sync_s(src_repo->get_sync_s()); 445 1 : existing->clear_depends(); 446 1 : for (const auto &dep : src_repo->get_depends()) 447 : { 448 0 : if (dep == nullptr) continue; 449 0 : auto d = std::make_shared<Depends>(); 450 0 : d->set_selector(dep->get_selector()); 451 0 : d->set_value(dep->get_value()); 452 0 : d->set_line_number(dep->get_line_number()); 453 0 : existing->add_depends(d); 454 0 : } 455 1 : return ESYSREPO_RESULT(ResultCode::OK); 456 : } 457 : 458 2 : return conflict("repository path '" + generic_repo_path(src_repo->get_path()) + "' differs in '" + src_path + "'"); 459 8 : } 460 : 461 7 : Result Merge::absorb_groups(const Manifest &src) 462 : { 463 9 : for (const auto &group : src.get_groups().get_groups()) 464 : { 465 2 : if (group == nullptr) continue; 466 2 : auto dest_group = m_result->get_groups().find_or_new_group_by_name(group->get_name()); 467 4 : for (const auto &repo : group->get_repos()) 468 : { 469 2 : if (repo == nullptr) continue; 470 2 : auto dest_repo = m_result->find_repo_by_path(repo->get_path()); 471 2 : if (dest_repo == nullptr) continue; 472 6 : if (dest_group->find_repo_by_path(dest_repo->get_path()) == nullptr) dest_group->add_repo(dest_repo); 473 2 : } 474 2 : } 475 7 : return ESYSREPO_RESULT(ResultCode::OK); 476 : } 477 : 478 7 : Result Merge::absorb_revision_sets(const Manifest &src, const std::string &src_path) 479 : { 480 7 : for (const auto &rs : src.get_revision_sets().get_revision_sets()) 481 : { 482 0 : if (rs == nullptr) continue; 483 0 : auto dest = m_result->get_revision_sets().find_by_name(rs->get_name()); 484 0 : if (dest == nullptr) 485 : { 486 0 : auto copy = std::make_shared<RevisionSet>(rs->get_name()); 487 0 : copy->set_default_revision(rs->get_default_revision()); 488 0 : for (const auto &kv : rs->get_repo_revisions()) copy->set_repo_revision(kv.first, kv.second); 489 0 : m_result->get_revision_sets().add_revision_set(copy); 490 0 : continue; 491 0 : } 492 0 : const bool same = dest->get_default_revision() == rs->get_default_revision() 493 0 : && dest->get_repo_revisions() == rs->get_repo_revisions(); 494 0 : if (same) continue; 495 0 : if (m_on_conflict == OnConflict::PreferFirst) continue; 496 0 : if (m_on_conflict == OnConflict::PreferLast) 497 : { 498 0 : dest->clear(); 499 0 : dest->set_name(rs->get_name()); 500 0 : dest->set_default_revision(rs->get_default_revision()); 501 0 : for (const auto &kv : rs->get_repo_revisions()) dest->set_repo_revision(kv.first, kv.second); 502 0 : continue; 503 0 : } 504 0 : return conflict("revision-set '" + rs->get_name() + "' differs in '" + src_path + "'"); 505 0 : } 506 7 : return ESYSREPO_RESULT(ResultCode::OK); 507 : } 508 : 509 : } // namespace esys::repo::manifest