SwiftPM and conditional compilation

I have a Swift package where I want to enable certain features using conditional complilation flags, e.g.

struct MyStruct {
	var count: Int
	
	#if FEATURE_A
	var featureA: String
	#endif

	#if FEATURE_B
	var featureB: [String]
	#endif
}

The user of the package should be able to enable the features. I could set the flags based on the target OS in Package.swift, but that's not what I want.

I have tried creating multiple targets that reference the same source code files and enable the flags.

targets: [
	.target(
		name: "MyPackageFeatureA",
		path: "Sources/MyPackage",
		swiftSettings: [
			.define("FEATURE_A")
		]
	),
	.target(
		name: "MyPackageFeatureB",
		path: "Sources/MyPackage",
		swiftSettings: [
			.define("FEATURE_B")
		]
	)
]

In that case SwiftPM complains that multiple targets contain the same source files.

Is there any way to achieve what I am trying to do with SwiftPM? Do you have any suggestions for a workaround?

3 Likes

In that case SwiftPM complains that multiple targets contain the same source files.

I think this error is deliberately the case because for C targets, it would result in name clashes that would be confusing to debug, since the duplicate names would both be pointing at the same thing.

Since Swift symbols are namespaced by module, it might be possible to lift this restriction for Swift targets. But there could be some other reason I’m unaware of.

It’s a shot in the dark, but can you fool SwiftPM by “duplicating” the files with symlinks?

Remember though, that even if it works, MyPackageFeatureA.MyStruct will not be the same type as MyPackageFeatureB.MyStruct, even though they shadow each other. For many use cases, that would cause more nuisance than it solves.

I have Xcode targets setup just like this. There are several targets in the project. The source code files are the same for all the targets. I have #defines in the target settings that are different between the various targets. This is also how DEBUG and RELEASE builds work. The different targets generate different binaries.

The following somewhat-ridiculous (!) workaround works for me.

  1. First, symlink your targets into distinct source directories, e.g. ln -s MyPackageB Sources/MyPackage
  2. Point each target at a distinct symlink, e.g.
targets: [
	.target(
		name: "MyPackageFeatureA",
		path: "Sources/MyPackage",
		swiftSettings: [
			.define("FEATURE_A")
		]
	),
	.target(
		name: "MyPackageFeatureB",
		path: "Sources/MyPackageB",
		swiftSettings: [
			.define("FEATURE_B")
		]
	)
]
  1. In the case of C targets, you will get errors that the autogenerated header already covers this directory. Instead, write a module.modulemap file inside include.
2 Likes