[Apple] Allow passing a manifest to the post-processing script
When modifying the list of dependencies, it was possible to break
a build from scratch but not an incremental build. This is because
the bundles generate by create_bundle(...) targets are assembled
in multiple steps (each of them copying a single file).
This incremental construction prevented detecting that a file was
present in the bundle only by virtue of being added in a previous
build.
To detect this, allow to invoke the post-processing script with a
manifest (a list of all the files that should be present in the
bundle). The post-processing is then free to use the manifest as
it wants to either report an error if unexpected files are found
or by deleting them.
Update the sample project to use a post-processing script that
fail if there are any unexpected files (as an exemple of how to
use the manifest).
Bug: 408138713
Change-Id: I4ef09536d775e10de0aaf4bf3fe39bdb4af0ab7f
Reviewed-on: https://gn-review.googlesource.com/c/gn/+/19561
Commit-Queue: Sylvain Defresne <sdefresne@chromium.org>
Reviewed-by: Takuto Ikuta <tikuta@google.com>
diff --git a/docs/reference.md b/docs/reference.md
index b6b4b78..ea95bc8 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -143,6 +143,7 @@
* [partial_info_plist: [filename] Path plist from asset catalog compiler.](#var_partial_info_plist)
* [pool: [string] Label of the pool used by binary targets and actions.](#var_pool)
* [post_processing_args: [string list] Args for the post-processing script.](#var_post_processing_args)
+ * [post_processing_manifest: [file] Name of the generated bundle manifest.](#var_post_processing_manifest)
* [post_processing_outputs: [file list] Outputs of the post-processing step.](#var_post_processing_outputs)
* [post_processing_script: [file name] Script for the post-processing step.](#var_post_processing_script)
* [post_processing_sources: [file list] Sources for the post-processing step.](#var_post_processing_sources)
@@ -1769,6 +1770,12 @@
be defined and non-empty to inform when the script needs to be re-run. The
`post_processing_args` will be passed as is to the script (so path have to be
rebased) and additional inputs may be listed via `post_processing_sources`.
+
+ If `post_processing_manifest` is defined, then gn will write a file listing
+ the expected content of the generated bundle (one file per line). The file
+ can then be passed as a parameter to `post_processing_script` via the
+ `post_processing_args` array. This can only be set if `post_processing_script`
+ is set.
```
#### **Variables**
@@ -1782,10 +1789,10 @@
visibility
Bundle vars: bundle_root_dir, bundle_contents_dir, bundle_resources_dir,
bundle_executable_dir, bundle_deps_filter, product_type,
- post_processing_args, post_processing_script,
- post_processing_sources, post_processing_outputs,
- xcode_extra_attributes, xcode_test_application_name,
- partial_info_plist
+ post_processing_args, post_processing_manifest,
+ post_processing_script, post_processing_sources,
+ post_processing_outputs, xcode_extra_attributes,
+ xcode_test_application_name, partial_info_plist
```
#### **Example**
@@ -1860,6 +1867,7 @@
if (is_ios && code_signing) {
deps += [ ":${app_name}_generate_executable" ]
post_processing_script = "//build/config/ios/codesign.py"
+ post_processing_manifest = "$target_out_dir/$target_name.manifest"
post_processing_sources = [
invoker.entitlements_path,
"$target_gen_dir/$app_name",
@@ -1878,6 +1886,7 @@
invoker.entitlements_path, root_build_dir),
"-e=" + rebase_path(
"$target_gen_dir/$app_name.xcent", root_build_dir),
+ "-m=" + rebase_path(post_processing_manifest, root_build_dir),
rebase_path(bundle_root_dir, root_build_dir),
]
} else {
@@ -6394,6 +6403,15 @@
See also "gn help create_bundle".
```
+### <a name="var_post_processing_manifest"></a>**post_processing_manifest**: [file] Name of the generated bundle manifest. [Back to Top](#gn-reference)
+
+```
+ Path where a manifest listing all the files found in the bundle will be
+ written for the post processing step. The path must be outside of the
+ bundle_root_dir.
+
+ See also "gen help create_bundle".
+```
### <a name="var_post_processing_outputs"></a>**post_processing_outputs**: [file list] Outputs of the post-processing step. [Back to Top](#gn-reference)
```
diff --git a/examples/ios/build/config/ios/scripts/check-manifest.py b/examples/ios/build/config/ios/scripts/check-manifest.py
new file mode 100644
index 0000000..1cb7771
--- /dev/null
+++ b/examples/ios/build/config/ios/scripts/check-manifest.py
@@ -0,0 +1,73 @@
+# Copyright 2025 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+"""Check the content of a bundle according to a manifest."""
+
+import sys
+import os
+
+
+def write_if_needed(path, content):
+ """Write `content` to `path` if needed."""
+ if os.path.isfile(path):
+ # If the file exists and has the same content, skip writing.
+ with open(path, encoding='utf-8') as file:
+ if content == file.read():
+ return
+ with open(path, 'w', encoding='utf-8') as file:
+ file.write(content)
+ file.flush()
+
+
+
+def read_manifest(manifest_path):
+ """Read a manifest file."""
+ manifest_content = set()
+ with open(manifest_path, encoding='utf-8') as manifest_file:
+ for line in manifest_file:
+ assert line.endswith('\n')
+ manifest_content.add(line[:-1])
+ return manifest_content
+
+
+def main(args):
+ if len(args) != 3:
+ print(
+ f'usage: check-manifest.py bundle-dir manifest-path output-path',
+ file=sys.stderr)
+ return 1
+
+ bundle_dir, manifest_path, output_path = args
+ if not os.path.isdir(bundle_dir):
+ print(
+ f'error: {bundle_dir}: not a directory',
+ file=sys.stderr)
+ return 1
+
+ has_unexpected_files = False
+ manifest_content = read_manifest(manifest_path)
+ for dirpath, dirnames, filenames in os.walk(bundle_dir):
+ dirnames_to_skip = []
+ for dirname in dirnames:
+ subdirpath = os.path.relpath(os.path.join(dirpath, dirname), bundle_dir)
+ if subdirpath in manifest_content:
+ dirnames_to_skip.append(dirname)
+ if dirnames_to_skip:
+ dirnames[:] = list(set(dirnames) - set(dirnames_to_skip))
+ for filename in filenames:
+ filepath = os.path.relpath(os.path.join(dirpath, filename), bundle_dir)
+ if filepath not in manifest_content:
+ fullpath = os.path.normpath(os.path.join(bundle_dir, filepath))
+ print(f'error: {fullpath}: unexpected file', file=sys.stderr)
+ has_unexpected_files = True
+
+ write_if_needed(output_path, 'ko\n' if has_unexpected_files else 'ok\n')
+
+ if has_unexpected_files:
+ return 1
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv[1:]))
diff --git a/examples/ios/build/config/ios/templates/ios_binary_bundle.gni b/examples/ios/build/config/ios/templates/ios_binary_bundle.gni
index 1e7b73d..65775a0 100644
--- a/examples/ios/build/config/ios/templates/ios_binary_bundle.gni
+++ b/examples/ios/build/config/ios/templates/ios_binary_bundle.gni
@@ -119,6 +119,15 @@
bundle_executable_dir = bundle_contents_dir
bundle_resources_dir = bundle_contents_dir
+ post_processing_script = "//build/config/ios/scripts/check-manifest.py"
+ post_processing_manifest = "$target_out_dir/$target_name.manifest"
+ post_processing_outputs = [ "$post_processing_manifest.checked" ]
+ post_processing_args =
+ [
+ rebase_path(bundle_root_dir, root_build_dir),
+ rebase_path(post_processing_manifest, root_build_dir),
+ ] + rebase_path(post_processing_outputs, root_build_dir)
+
xcode_extra_attributes = {
CODE_SIGN_IDENTITY = ""
CODE_SIGNING_REQUIRED = "NO"
diff --git a/misc/emacs/gn-mode.el b/misc/emacs/gn-mode.el
index f98093e..f72493a 100644
--- a/misc/emacs/gn-mode.el
+++ b/misc/emacs/gn-mode.el
@@ -85,15 +85,16 @@
"asmflags" "assert_no_deps" "bundle_deps_filter" "bundle_executable_dir"
"bundle_resources_dir" "bundle_root_dir" "cflags" "cflags_c" "cflags_cc"
"cflags_objc" "cflags_objcc" "check_includes" "post_processing_args"
- "post_processing_outputs" "post_processing_script" "post_processing_sources"
- "complete_static_lib" "configs" "data" "data_deps" "defines" "depfile"
- "deps" "framework_dir" "frameworks" "include_dirs" "inputs" "ldflags"
- "lib_dirs" "libs" "output_dir" "output_extension" "output_name"
- "output_prefix_override" "outputs" "pool" "precompiled_header"
- "precompiled_header_type" "precompiled_source" "product_type" "public"
- "public_configs" "public_deps" "response_file_contents" "script" "sources"
- "testonly" "visibility" "write_runtime_deps" "bundle_contents_dir"
- "contents" "output_conversion" "rebase" "data_keys" "walk_keys"))
+ "post_processing_manifest" "post_processing_outputs"
+ "post_processing_script" "post_processing_sources" "complete_static_lib"
+ "configs" "data" "data_deps" "defines" "depfile" "deps" "framework_dir"
+ "frameworks" "include_dirs" "inputs" "ldflags" "lib_dirs" "libs"
+ "output_dir" "output_extension" "output_name" "output_prefix_override"
+ "outputs" "pool" "precompiled_header" "precompiled_header_type"
+ "precompiled_source" "product_type" "public" "public_configs"
+ "public_deps" "response_file_contents" "script" "sources" "testonly"
+ "visibility" "write_runtime_deps" "bundle_contents_dir" "contents"
+ "output_conversion" "rebase" "data_keys" "walk_keys"))
(defconst gn-font-lock-keywords
`((,(regexp-opt gn-font-lock-reserved-keywords 'words) .
diff --git a/src/gn/bundle_data.cc b/src/gn/bundle_data.cc
index 86f7abf..2653d3a 100644
--- a/src/gn/bundle_data.cc
+++ b/src/gn/bundle_data.cc
@@ -149,6 +149,9 @@
post_processing_output_files.end());
}
+ if (!post_processing_manifest_.is_null())
+ outputs_as_source->push_back(post_processing_manifest_);
+
if (!root_dir_.is_null())
outputs_as_source->push_back(GetBundleRootDirOutput(settings));
diff --git a/src/gn/bundle_data.h b/src/gn/bundle_data.h
index 2e03694..59e1f2d 100644
--- a/src/gn/bundle_data.h
+++ b/src/gn/bundle_data.h
@@ -149,6 +149,13 @@
return post_processing_script_;
}
+ void set_post_processing_manifest(const SourceFile& manifest_file) {
+ post_processing_manifest_ = manifest_file;
+ }
+ const SourceFile& post_processing_manifest() const {
+ return post_processing_manifest_;
+ }
+
std::vector<SourceFile>& post_processing_sources() {
return post_processing_sources_;
}
@@ -236,9 +243,10 @@
// (corresponds to {{bundle_partial_info_plist}} expansion).
SourceFile partial_info_plist_;
- // Holds the values (script name, sources, outputs, script arguments) for the
- // post-processing step if defined.
+ // Holds the values (script name, manifest path, sources, outputs, script
+ // arguments) for the post-processing step if defined.
SourceFile post_processing_script_;
+ SourceFile post_processing_manifest_;
std::vector<SourceFile> post_processing_sources_;
SubstitutionList post_processing_outputs_;
SubstitutionList post_processing_args_;
diff --git a/src/gn/create_bundle_target_generator.cc b/src/gn/create_bundle_target_generator.cc
index 516aa0a..c4c2ac4 100644
--- a/src/gn/create_bundle_target_generator.cc
+++ b/src/gn/create_bundle_target_generator.cc
@@ -59,6 +59,9 @@
if (!FillPostProcessingScript())
return;
+ if (!FillPostProcessingManifest())
+ return;
+
if (!FillPostProcessingSources())
return;
@@ -207,6 +210,31 @@
return true;
}
+bool CreateBundleTargetGenerator::FillPostProcessingManifest() {
+ const Value* value =
+ scope_->GetValue(variables::kPostProcessingManifest, true);
+ if (!value)
+ return true;
+
+ if (target_->bundle_data().post_processing_script().is_null()) {
+ *err_ = Err(function_call_, "No post-processing script.",
+ "You must define post_processing_script if you use "
+ "post_processing_manifest.");
+ return false;
+ }
+
+ if (!value->VerifyTypeIs(Value::STRING, err_))
+ return false;
+
+ SourceFile manifest_file = scope_->GetSourceDir().ResolveRelativeFile(
+ *value, err_, scope_->settings()->build_settings()->root_path_utf8());
+ if (err_->has_error())
+ return false;
+
+ target_->bundle_data().set_post_processing_manifest(manifest_file);
+ return true;
+}
+
bool CreateBundleTargetGenerator::FillPostProcessingSources() {
const Value* value =
scope_->GetValue(variables::kPostProcessingSources, true);
diff --git a/src/gn/create_bundle_target_generator.h b/src/gn/create_bundle_target_generator.h
index c713368..81d4947 100644
--- a/src/gn/create_bundle_target_generator.h
+++ b/src/gn/create_bundle_target_generator.h
@@ -35,6 +35,7 @@
bool FillXcodeTestApplicationName();
bool FillPostProcessingScript();
+ bool FillPostProcessingManifest();
bool FillPostProcessingSources();
bool FillPostProcessingOutputs();
bool FillPostProcessingArgs();
diff --git a/src/gn/functions_target.cc b/src/gn/functions_target.cc
index 819dc89..9a19c41 100644
--- a/src/gn/functions_target.cc
+++ b/src/gn/functions_target.cc
@@ -402,16 +402,22 @@
`post_processing_args` will be passed as is to the script (so path have to be
rebased) and additional inputs may be listed via `post_processing_sources`.
+ If `post_processing_manifest` is defined, then gn will write a file listing
+ the expected content of the generated bundle (one file per line). The file
+ can then be passed as a parameter to `post_processing_script` via the
+ `post_processing_args` array. This can only be set if `post_processing_script`
+ is set.
+
Variables
)" DEPENDENT_CONFIG_VARS DEPS_VARS GENERAL_TARGET_VARS
R"( Bundle vars: bundle_root_dir, bundle_contents_dir, bundle_resources_dir,
bundle_executable_dir, bundle_deps_filter, product_type,
- post_processing_args, post_processing_script,
- post_processing_sources, post_processing_outputs,
- xcode_extra_attributes, xcode_test_application_name,
- partial_info_plist
+ post_processing_args, post_processing_manifest,
+ post_processing_script, post_processing_sources,
+ post_processing_outputs, xcode_extra_attributes,
+ xcode_test_application_name, partial_info_plist
Example
@@ -484,6 +490,7 @@
if (is_ios && code_signing) {
deps += [ ":${app_name}_generate_executable" ]
post_processing_script = "//build/config/ios/codesign.py"
+ post_processing_manifest = "$target_out_dir/$target_name.manifest"
post_processing_sources = [
invoker.entitlements_path,
"$target_gen_dir/$app_name",
@@ -502,6 +509,7 @@
invoker.entitlements_path, root_build_dir),
"-e=" + rebase_path(
"$target_gen_dir/$app_name.xcent", root_build_dir),
+ "-m=" + rebase_path(post_processing_manifest, root_build_dir),
rebase_path(bundle_root_dir, root_build_dir),
]
} else {
diff --git a/src/gn/ninja_create_bundle_target_writer.cc b/src/gn/ninja_create_bundle_target_writer.cc
index 09299c7..b3a15ba 100644
--- a/src/gn/ninja_create_bundle_target_writer.cc
+++ b/src/gn/ninja_create_bundle_target_writer.cc
@@ -13,6 +13,7 @@
#include "gn/ninja_utils.h"
#include "gn/output_file.h"
#include "gn/scheduler.h"
+#include "gn/string_output_buffer.h"
#include "gn/substitution_writer.h"
#include "gn/target.h"
#include "gn/toolchain.h"
@@ -144,9 +145,39 @@
out_ << " restat = 1" << std::endl;
out_ << std::endl;
+ WritePostProcessingManifestFile();
return custom_rule_name;
}
+void NinjaCreateBundleTargetWriter::WritePostProcessingManifestFile() {
+ const BundleData& bundle_data = target_->bundle_data();
+ const SourceFile& manifest_path = bundle_data.post_processing_manifest();
+ if (manifest_path.is_null()) {
+ return;
+ }
+
+ const BuildSettings* build_settings = settings_->build_settings();
+ const base::FilePath bundle_root_dir = build_settings->GetFullPath(
+ bundle_data.GetBundleRootDirOutputAsDir(settings_));
+
+ StringOutputBuffer storage;
+ std::ostream manifest(&storage);
+
+ std::vector<SourceFile> outputs;
+ bundle_data.GetOutputsAsSourceFiles(settings_, target_, &outputs, nullptr);
+ for (const SourceFile& output_file : outputs) {
+ const base::FilePath full_path = build_settings->GetFullPath(output_file);
+
+ base::FilePath relative_path;
+ if (bundle_root_dir.AppendRelativePath(full_path, &relative_path)) {
+ manifest << relative_path.As8Bit() << "\n";
+ }
+ }
+
+ storage.WriteToFileIfChanged(build_settings->GetFullPath(manifest_path),
+ nullptr);
+}
+
void NinjaCreateBundleTargetWriter::WriteCopyBundleDataSteps(
const std::vector<OutputFile>& order_only_deps,
std::vector<OutputFile>* output_files) {
diff --git a/src/gn/ninja_create_bundle_target_writer.h b/src/gn/ninja_create_bundle_target_writer.h
index ee27557..6b5d6cc 100644
--- a/src/gn/ninja_create_bundle_target_writer.h
+++ b/src/gn/ninja_create_bundle_target_writer.h
@@ -24,6 +24,9 @@
// if defined, otherwise returns an empty string.
std::string WritePostProcessingRuleDefinition();
+ // Writes the post processing manifest file if needed.
+ void WritePostProcessingManifestFile();
+
// Writes the steps to copy files into the bundle.
//
// The list of newly created files will be added to |output_files|.
diff --git a/src/gn/variables.cc b/src/gn/variables.cc
index 31b3b0d..1754172 100644
--- a/src/gn/variables.cc
+++ b/src/gn/variables.cc
@@ -1673,6 +1673,19 @@
See also "gn help create_bundle".
)";
+const char kPostProcessingManifest[] = "post_processing_manifest";
+const char kPostProcessingManifest_HelpShort[] =
+ "post_processing_manifest: [file] Name of the generated bundle manifest.";
+const char kPostProcessingManifest_Help[] =
+ R"(post_processing_manifest: [file] Name of the generated bundle manifest.
+
+ Path where a manifest listing all the files found in the bundle will be
+ written for the post processing step. The path must be outside of the
+ bundle_root_dir.
+
+ See also "gen help create_bundle".
+)";
+
const char kPostProcessingOutputs[] = "post_processing_outputs";
const char kPostProcessingOutputs_HelpShort[] =
"post_processing_outputs: [file list] Outputs of the post-processing step.";
@@ -2424,6 +2437,7 @@
INSERT_VARIABLE(PartialInfoPlist)
INSERT_VARIABLE(Pool)
INSERT_VARIABLE(PostProcessingArgs)
+ INSERT_VARIABLE(PostProcessingManifest)
INSERT_VARIABLE(PostProcessingOutputs)
INSERT_VARIABLE(PostProcessingScript)
INSERT_VARIABLE(PostProcessingSources)
diff --git a/src/gn/variables.h b/src/gn/variables.h
index a9ed6c4..b4d2862 100644
--- a/src/gn/variables.h
+++ b/src/gn/variables.h
@@ -262,6 +262,10 @@
extern const char kPostProcessingArgs_HelpShort[];
extern const char kPostProcessingArgs_Help[];
+extern const char kPostProcessingManifest[];
+extern const char kPostProcessingManifest_HelpShort[];
+extern const char kPostProcessingManifest_Help[];
+
extern const char kPostProcessingOutputs[];
extern const char kPostProcessingOutputs_HelpShort[];
extern const char kPostProcessingOutputs_Help[];