liamnichols/xcstrings-tool

Multiple commands produce generated source when working with embedded watchOS app

liamnichols opened this issue · 8 comments

There seems to be an issue when a Package integrates XCStrings Tool and that package target is linked into both an iOS app and a watchOS app.

I was able to reproduce a project with the issue here: https://github.com/liamnichols/xcstrings-tool-demo/compare/ln/embedded-watch-app

The issue seems to happen when the Embed Watch Content phase exists in a target:

Screenshot 2024-04-24 at 14 50 56

It seems like the build system might be setting up to compile DogKit to be built twice, once for PuppyTracker (the watchOS app) and another time for DogTracker (the iOS app) and this is what is causing a problem.

Could it be because one complication is for watchOS and the other is for iOS?

Could it be because one complication is for watchOS and the other is for iOS?

I have a feeling that this might be the case...

Screenshot 2024-04-24 at 15 03 15

The plugin work directory is ./SourcePackages/plugins/DogTracker.output/DogTracker/XCStringsToolPlugin/XCStringsTool/..., which doesn't account for the possibility of one build needing to produce for different platforms.

You can see that the separate builds for the watchOS and iOS apps output into ./Build/Products/Debug-iphonesimulator/ and ./Build/Products/Debug-watchsimulator/ respectively.

I need to see if this information is passed to the build tool plugin so that we can output to unique directories.

I had a brief look at the PackagePlugin module interface and I can't seem to find a way to get a pluginWorkDirectory for outputting sources unique to the build platform.

It seems like this might be a limitation of SPM currently 😕 I'll try and wrap my head around this a bit more and raise a bug/ask over in the Swift forums.

Reported as FB13750966

[SPM] pluginWorkDirectory is not unique to build configuration when building package target with build tool plugin

FB13750966

Recent Similar Reports: None
Resolution: Open

Basic Information

Please provide a descriptive title for your feedback:

[SPM] pluginWorkDirectory is not unique to build configuration when building package target with build tool plugin

Which area are you seeing an issue with?

Xcode

What type of feedback are you reporting?

Incorrect/Unexpected Behavior

Details

What version of Xcode are you using?

Xcode Version 15.3 (15E204a)

Please provide the App name/App ID if possible:

Did you see an error message?

Yes

What was the error?

Multiple commands produce ...

What time did the issue occur?

24 Apr 2024 at 16:37

What devices were you using when the issue occurred?

Not answered

Description

Please describe the issue and what steps we can take to reproduce it

When a build graph needs to compile a SPM target multiple times across different build configurations (iOS and watchOS), the pluginWorkDirectory used by SPM for running build tool plugins is not unique to the build configuration (i.e Debug-iphonesimulator or Debug-watchossimulator) which can result in “Multiple commands produce...” Errors.

Related issue: #57
Demo: https://github.com/liamnichols/xcstrings-tool-demo/tree/ln/embedded-watch-app (also attached)

When an iOS app target embeds a watchOS app target via the “Embed Watch Content” build phase, if both iOS and watchOS targets depend on an SPM Package Target, which happens to use Build Tool plugins, the SPM target will need to be compiled twice in the same build graph, but the pluginWorkDirectory passed to the Build Tool plugin is the same each time which prevents the build tool plugin from being able to write sources out to a unique location causing the build to fail.

This is a unique circumstance limited only to Xcode projects and not a scenario that can be replicated in pure Swift Package dependency graphs (currently).

Steps to Reproduce:

  1. Download xcstrings-tool-demo-ln-embedded-watch-app.zip and unzip the archive
  2. Open DogTracker.xcodeproj
  3. Select the PuppyTracker (watchOS app) scheme
  4. Build the schema and verify that the watchOS target compiles successfully in isolation
  5. Select the DogTracker (iOS app with embedded watchOS app) scheme
  6. Build the scheme
  7. Observe the build error

As a plugin author, there is no way to use the PluginContext or Target information to generate a unique file path. Or there is, but it is not clearly documented.

As a developer of an embedded watchOS app, there is no way to use package target dependencies that use SPM build tool plugins in this way.

Please consider updating the SPM integration within Xcode so that the pluginWorkDirectory is unique in the same way that the build outputs are. It seems that this is generally achieved using subdirectories such as Debug-iphonesimulator or Debug-watchossimulator.

Files

xcstrings-tool-demo-ln-embedded-watch-app.zip

Are there any updates and/or workarounds for this? I'd love to move back to using the plugin rather than the CLI!

I'm afraid not 😢 I did create a post over on the Swift Forums alongside the Feedback, but neither have had any response

https://forums.swift.org/t/buildtoolplugin-multiple-commands-produce-error-when-embedding-wachos-app-in-ios-app-target/71437

I think that due to the closed nature of the system, there aren't really many options for workarounds 😕

I plan to work on a command plugin (i.e swift package generate-strings ...) that can be a slight improvement from invoking the CLI manually (#32) but thats the best that I can come up with

Gotcha, appreciate the update, thank you! I've put together a little zsh script in the meantime that works pretty well for me in the meantime, posting it here in case anyone else might find it useful:

#!/bin/zsh

STRINGS_PATH="Packages/Sources/Core/Base/Resources/Localization"
OUTPUT_PATH="Packages/Sources/Core/Base/Strings/"

rm -r $OUTPUT_PATH/*

generate_strings() {
  	local file_path="$1"
  	local file_name="${file_path:t:r}"
  	local destination_path="$OUTPUT_PATH/$file_name.swift"
	mint run xcstrings-tool@0.5.2 xcstrings-tool generate \
		"$file_path" \
		--access-level public \
		--output "$destination_path"
}

find "$STRINGS_PATH" -type f -name "*.xcstrings" | while read -r file; do
	generate_strings "$file"
done