529 return absl::FailedPreconditionError(
530 "The project hack manifest does not define messages.source");
539 root, layout.source->canonical_bundle_path,
540 true,
"Canonical message bundle"));
545 ParseBundleDocument(content,
"Canonical message bundle"));
546 ExpandedSourceBank bank;
549 "Canonical message bundle"));
550 size_t encoded_size = 1;
551 for (
const auto& [
id, message] : bank) {
552 encoded_size += message.bytes.size() + 1;
554 if (encoded_size > capacity) {
555 return absl::ResourceExhaustedError(
556 absl::StrFormat(
"Canonical message bundle needs %zu bytes, but "
557 "manifest capacity is %zu",
558 encoded_size, capacity));
565 int address =
static_cast<int>(
SnesToPc(layout.data_start));
566 for (
auto& [
id, source] : bank) {
572 message.
Data = std::move(source.bytes);
574 address +=
static_cast<int>(message.
Data.size()) + 1;
575 snapshot.
messages.push_back(std::move(message));
584 return absl::FailedPreconditionError(
585 "Project must load a hack manifest before message source sync");
587#if defined(__EMSCRIPTEN__)
589 return absl::FailedPreconditionError(
590 "message-source-sync --write is unavailable in browser builds because "
591 "durable atomic filesystem publication cannot be guaranteed");
595 if (!layout.
source.has_value()) {
596 return absl::FailedPreconditionError(
597 "Hack manifest does not define messages.source");
602 fs::path project_root;
605 fs::path canonical_bundle_path;
607 canonical_bundle_path,
608 ResolveProjectTarget(project_root, layout.
source->canonical_bundle_path,
609 true,
"Canonical message bundle"));
610 fs::path asm_include_path;
612 ResolveProjectTarget(
613 project_root, layout.
source->generated_asm_include_path,
614 false,
"Generated message ASM include"));
616 ValidateDistinctTargets(canonical_bundle_path, asm_include_path));
617 const std::vector<fs::path> publication_targets = {canonical_bundle_path,
620 publication_targets, kMessageSourcePublisherLabels));
622 std::error_code incoming_ec;
623 const fs::path resolved_incoming =
624 fs::canonical(incoming_bundle_path, incoming_ec);
625 if (incoming_ec || !fs::is_regular_file(resolved_incoming, incoming_ec) ||
627 return absl::NotFoundError(absl::StrFormat(
628 "Incoming message bundle must be an existing regular file: %s",
629 incoming_bundle_path.string()));
632 std::string expected_sha;
633 std::unique_ptr<core::SourceArtifactPublicationLock> write_locks;
636 return absl::InvalidArgumentError(
637 "--write requires --expected-source-sha256");
643 publication_targets, kMessageSourcePublisherLabels));
646 std::string canonical_before;
648 "canonical message bundle"));
649 const std::string source_sha_before =
651 Json canonical_document;
654 ParseBundleDocument(canonical_before,
"Canonical message bundle"));
655 ExpandedSourceBank merged_bank;
659 true,
"Canonical message bundle"));
660 const std::string expected_current_asm = RenderAsmInclude(
663 std::string incoming_content;
665 ReadTextFile(resolved_incoming,
"incoming message bundle"));
666 Json incoming_document;
669 ParseBundleDocument(incoming_content,
"Incoming message bundle"));
670 ExpandedSourceBank incoming_bank;
674 false,
"Incoming message bundle"));
675 for (
auto& [
id, message] : incoming_bank) {
676 merged_bank[id] = std::move(message);
679 size_t encoded_size = 1;
680 for (
const auto& entry : merged_bank) {
681 encoded_size += entry.second.bytes.size() + 1;
683 if (encoded_size > capacity) {
684 return absl::ResourceExhaustedError(absl::StrFormat(
685 "Expanded message source needs %zu bytes including terminators, but "
686 "manifest capacity is %zu bytes [0x%06X, 0x%06X]",
690 const std::string canonical_after = SerializeCanonicalBundle(merged_bank);
691 const std::string proposed_source_sha =
693 const std::string asm_after = RenderAsmInclude(
696 std::optional<std::string> asm_before;
697 std::error_code include_exists_ec;
698 if (fs::exists(asm_include_path, include_exists_ec)) {
699 std::string existing_include;
702 ReadTextFile(asm_include_path,
"generated message ASM include"));
703 asm_before = std::move(existing_include);
704 }
else if (include_exists_ec) {
705 return absl::FailedPreconditionError(
706 absl::StrFormat(
"Cannot inspect generated message ASM include: %s",
707 include_exists_ec.message()));
709 if (asm_before.has_value() && *asm_before != expected_current_asm) {
710 return absl::FailedPreconditionError(absl::StrFormat(
711 "Generated message ASM include drifted from the current canonical "
713 asm_include_path.string()));
718 .generated_asm_include_path = asm_include_path,
722 .incoming_updates =
static_cast<int>(incoming_bank.size()),
723 .encoded_size = encoded_size,
724 .capacity = capacity,
725 .source_sha256_before = source_sha_before,
726 .proposed_source_sha256 = proposed_source_sha,
727 .changed = canonical_before != canonical_after ||
728 !asm_before.has_value() || *asm_before != asm_after,
730 if (!options.
write) {
733 if (expected_sha != source_sha_before) {
734 return absl::AbortedError(absl::StrFormat(
735 "Canonical message source SHA-256 CAS failed: expected %s, got %s",
736 expected_sha, source_sha_before));
738 if (!result.changed) {
742 std::vector<core::SourceArtifactUpdate> updates;
744 .
target = canonical_bundle_path,
745 .before = canonical_before,
746 .after = canonical_after,
749 .
target = asm_include_path,
750 .before = asm_before,
754 *write_locks, std::move(updates), expected_sha, [&]() -> absl::Status {
757 std::string reopened_canonical;
759 ReadTextFile(canonical_bundle_path,
760 "published canonical message bundle"));
761 Json reopened_document;
764 ParseBundleDocument(reopened_canonical,
765 "Published canonical message bundle"));
766 ExpandedSourceBank reopened_bank;
771 "Published canonical message bundle"));
773 proposed_source_sha) {
774 return absl::DataLossError(
775 "Published canonical message bundle SHA-256 readback failed");
777 return absl::OkStatus();