How to use a folder generated by an Xcode Aggregate Target as a resource in another target?

I have a multi target project where every target relies on a built "web bundle" which is basically a collection of html, optimized images, and optimized javascript.

Right now that web bundle is built in a pre build step by running a script. The script takes awhile to run and outputs to a folder that is referenced into the project via a PBXFileReference which is then referenced in the Copy Bundle Resources step.

96516AC22BF928DD00576562 /* build */ = {isa = PBXFileReference; lastKnownFileType = folder; name = build; path = "../web-ui/build"; sourceTree = "<group>"; }
....
96516AC32BF928DD00576562 /* build in Resources */ = {isa = PBXBuildFile; fileRef = 96516AC22BF928DD00576562 /* build */; };

As a step, I wrote an aggregate target that can also run this script. I specified its input and output files and turned off sandboxing. It does exactly what I need it to. Critically it is ran based on dependency analysis. If I modify any file in web-ui it rebuilds, if I dont I can repeatedly build the aggregate target and it will not re-run the script. This is perfect.

A97590172E419CBA00741928 /* Build Web Bundle */ = {
	isa = PBXShellScriptBuildPhase;
	buildActionMask = 12;
	files = (
	);
	inputFileListPaths = (
	);
	inputPaths = (
		"$(SRCROOT)/xcodescripts/build-web-bundle.bash",
		"$(SRCROOT)/web-ui/index.html",
		"$(SRCROOT)/web-ui/vite-env.d.ts",
		"$(SRCROOT)/web-ui/vite.config.ts",
		"$(SRCROOT)/web-ui/tsconfig.json",
		"$(SRCROOT)/web-ui/stats.html",
		"$(SRCROOT)/web-ui/postcss.config.js",
		"$(SRCROOT)/web-ui/package.json",
		"$(SRCROOT)/web-ui/justfile",
		"$(SRCROOT)/web-ui/src/",
	);
	name = "Build Web Bundle";
	outputFileListPaths = (
	);
	outputPaths = (
		"$(SRCROOT)/web-ui/build/",
	);
	runOnlyForDeploymentPostprocessing = 0;
	shellPath = /bin/sh;
	shellScript = "exec \"${SCRIPT_INPUT_FILE_0}\"\n";
};

You may notice I reference a src file. This is made possible via a flag USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES which allows Xcode to check folder dependencies recursively.

The problem is that my other targets do not automatically recognize that they need to run the "WebBundle" aggregate target in order to update a resource they copy in their "Copy bundle resources" phase.

So I tried adding it as a Target Dependency.

A9DE685B2E41C9A8005EF4E0 /* PBXTargetDependency */ = {
			isa = PBXTargetDependency;
			target = A97590132E419C1200741928 /* WebBundle */;
			targetProxy = A9DE685A2E41C9A8005EF4E0 /* PBXContainerItemProxy */;
		};

Unfortunately this breaks whatever magic was allowing the script to be run only when there are web bundle changes. Every build it runs the "Build Web Bundle" script.

I think what I am missing is a way to specify in these other targets that a resource they are used to copying from the Xcode PBXFileReference is produced by the aggregate target. This way they can start to reason about the dependencies.

Other possibilities are that I should be building the web bundle to a separate location. Or that these references are somehow broken in another way. To be clear the folder format is as thus

project/
    iOS/
         client.xcodeproj
    web-ui/
         build/ (web bundle build is output here and referenced relatively)
         src/
         index.html (and other things)

I see that the output of the web bundle script is into SRCROOT. Are the results regarding dependency analysis the same if that output was to a location in BUILT_PRODUCTS_DIR instead?

One other thing that you could look at doing is to remove the aggregate build target, and instead use a custom build rule as part of your main app build. This isn't a commonly used feature of Xcode, but it might be well suited to your needs, and we showed an example at WWDC21 that seems similar to your needs.

— Ed Ford,  DTS Engineer

@DTS Engineer Love that video. I am not sure a custom build rule is a fit for my needs. From the documentation

A build rule is the preferred way to process files that are independent from each other.

Which I do not think applies here. There is a large bundle of react JS files that are compiled into a handful of html/css/resources in a folder.

However that comment about BUILT_PRODUCTS_DIR does seem on point and resolves another question I had.

Here's what remains. I seem to need to add the aggregate target that outputs to ${BUILT_PRODUCTS_DIR}/build as a target dependency. Otherwise Xcode does not seem to realize that even though the script in the aggregate output target outputs to ${BUILT_PRODUCTS_DIR}/build and that ${BUILT_PRODUCTS_DIR}/build is a resource copied in "copy bundle resources" that it needs to run the aggregate target. When I add the aggregate target as a target dependency it runs every time and does not seem to benefit from the dependency analysis check even if just building the aggregate target does benefit.

Is this how things are supposed to work or is there a way to get the aggregate target (when added as a target dependency) to have its run script phase ran based on dependency analysis as normal?

A housekeeping note — here is a link to a related thread Caleb started on a different slice of the problem, for anyone who may come to this in the future.

I reread what you originally wrote, and it sounds like you had a reason to pull out this web bundle build into the aggregate target. Is there a reason where that couldn't be a script in the main app target that I'm not seeing? It seems like the file level input and output analysis of this step in the main app build phase would be enough. The one downside I see is that if the script does need to run in full, that could potentially be a place where you don't get a lot of concurrently running build tasks doing other things, and thus takes up more wall clock time than is ideal, but also maybe isn't that different from the pre-build script you started with.

Another idea would be to question why this script needs to run via an Xcode build at all. If the source files don't change often, could other techniques like a git commit hook targeting the source file paths to build and commit a pre-built final bundle be better? If you have a CI system, doing something like that can be a big time saver because you then in effect have a cached asset that doesn't consume build minutes every time, and from Xcode's perspective, the file is simply there when the repo is checked out and only needs to be copied into place.

— Ed Ford,  DTS Engineer

The only reason I am going for an aggregate target is that I need this web bundle to be shared by two separate targets. In my head if I could get it so that it knew when to re-run the script in an aggregate target it would be easy to do so when I added the aggregate targets to the main targets.

I guess a question would be what would I expect the difference to be if I ran the script in the main target as opposed to the aggregate? Is there a reason that would help dependency analysis do the right thing easier. (I need to try it again but last time I did it was no help) breaking this off into its own question.

Your goal for the aggregate target isn't unreasonable, especially since you're trying to hoist something depended on by two different targets into its own target for dependency analysis.

Are you able to break this work down to a small test project that generally reflects how you are attempting to configure your project's build? That would be two targets that depend on this aggregate target, and a script in the aggregate target that consumes input files, does some processing, and has output files all declared as part of the script definition, so that the script always runs regardless of the info in the file lists? Having that structured example attached to a report in Feedback Assistant will be helpful in evaluating if there's something I'm not seeing from our conversation here, or if there is an issue with Xcode. If you open that report, please post the FB number here for my reference.

— Ed Ford,  DTS Engineer

How to use a folder generated by an Xcode Aggregate Target as a resource in another target?
 
 
Q