Compiler flags in Package.swift

Hi

It may be very useful to specify swift/cc/cxx compiler flags and linker flags in Package.swift
Maybe even per target/configuration like Xcode does.

For example flags like -DDEBUG or -DUNIT_TEST
Or something like this swift-llbuild/Package.swift at 1d39af91f8d6fc7d29e0e530f57a3aa3812cea5e · apple/swift-llbuild · GitHub
Or enable some experimental feature for a specific package in dependency graph
Or disable broken optimization pass

10 Likes

This would be very helpful. I was wanting this feature the other day and was going to ask if this had been discussed yet.

The use case I had was when working on a project that is both a Swift Package and Xcode Framework that depend on C code in the same git repository. I have it setup such that the C code is one target of the Package and the Swift target depends on the C target.

But this resulted in me needing to use import to get my C module when compiling with swift build and the import being invalid when compiling with Xcode and the need for different #import statements in the C code. I could have made the Xcode project compile in such a way that it uses import as well, but I've had undefined symbol issues when sharing the framework binary with other people when taking that approach, so I avoid it.

My solution was to make swift build just work by doing the extra flags in Xcode by passing in -DXCODE_FRAMEWORK=1 to both C and Swift. Then using #if to determine if I import the C module and how to #import my headers.

In addition to per target basis it would be nice if you could specify flags for different compilation modes such as debug and release. I have seen quite a few people suggest adding -DDebug as a debug swift build flag for Xcode projects so you can use #if Debug to make decisions.

It’s not exactly what you're asking for, but it is possible to specify a .json destination file via the --destination file.json argument to swift build. In that, you can configure flags for the linker, swiftc, and clang.

I can't find the documentation right now, but the source file within swiftpm is here, and all of the members of the Destination struct can be set through the json file.

It's also possible to set Xcode-specific configuration flags through the --xcconfig-overrides argument to swift build, using an xcconfig file.

An example json destination file looks something like this:

{
    "version": 1,
    "sdk": "/swift/usr/bin/swift",
    "toolchain-bin-dir": "/swift/usr/bin",
    "target": "x86_64-unknown-windows-msvc",
    "dynamic-library-extension": "lib",
    "extra-cc-flags": [
    ],
    "extra-swiftc-flags": [
        "-static-stdlib",
        "-use-ld=lld",
        "-ILibraries/CVulkan/"
    ],
    "extra-cpp-flags": []
}
3 Likes

destination.json is for quite different purpose
As you see toolchain-bin-dir, target, sdk all is required all configure "custom target"
For example we use global destination.json for build for android

Dont see xcconfig overrides
error: unknown option --xcconfig-overrides

This is definitely something we need. Some of the devs have discussed this in the past and started working on a proposal for a real build settings model, which I think is the right way to support this sort of thing. We put that on hold for a while due to other priorities, but I hope we'll be able to get back to it and kick off a discussion here with an initial proposal before too long.

3 Likes

I've been thinking about this a bit as part of some experiments.

My current thinking is that for it to generalize properly, you need two things:

  • a high level key/value way of specifying settings
  • a series of mappings which convert/transform them so that they can be supplied in whatever form any given tool requires

For example you might specify something like the minimum platform setting as

 .setting("minimum-target", "macosx10.12"),

or even

 .minimumTarget(.macosx10.12),

but you want to be able to then transform it depending on what you're doing.

When invoking the Swift compiler you want it to be transformed to "-Xswiftc", "-target", "-Xswiftc", "x86_64-apple-macosx10.12".

But you might also want to generate an xcconfig (as part of swift package generate-xcodeproj), in which case you'd want it to be transformed into MACOSX_DEPLOYMENT_TARGET = "10.12".

You might also want to pass it to a bundler tool to inject the same value into the Info.plist file, in which case you'd probably need another transformation again.

And so on...

For any non-trivial project you probably need to be able to sub-divide settings into groups that are applied in certain contexts: "here are the common settings", "here are the ones that only apply in debug", "here are the ones that only apply on the Mac", etc.

This could be done with straight inheritance (xcconfig-style), but in Xcode it tends to lead to a combinatorial explosion where you have to define a top-level xcconfig file for every combination you care about, and then #include the relevant bits into it. You end up with files like MyProjReleaseWithAssertionStatic.xcconfig, which includes MyProjReleaseWithAssertion.xcconfig and MyProjCommon.xcconfig, which in turn import other things, etc, etc. This can get messy if you're also importing settings from sub-projects or libraries, and they also have the same explosion.

I started thinking it might be more flexible to allow some kind of conditional-filtering which lets you mix-in / import settings under certain conditions - without having to explicitly specify every combination.

Hi I'm struggling with the same requirement.
Im my app a have many sub module.
In the modules minimise the useless requirements with:

#if INPLACE
    import some_library
#end if

But in the app (Xcode project) even I have -DINPLACE flag is defined.
But sub module is not compiling accordingly.

1 Like

Compilation flags defined in the build settings for your app do not apply to the swift packages they depend on.

Workaround for Xcode project with local packages. In your local package you can get the Xcode flags (ie: OTHER_SWIFT_FLAGS) using xcodebuild -showBuildSettings.

Warning this seems to work but might not in the future.

// swift-tools-version: 5.6

import PackageDescription
import Foundation

// Path to .xcodeproj relative to the folder Package.swift is in
let settings = otherSwiftFlags("../MyXcodeProject.xcodeproj")

let package = Package(
    name: "MyPackage",
    platforms: [
        .iOS(.v15),
    ],
    products: [
        .library(name: "MyLibary", targets: ["MyLibary"])
    ],
    dependencies: [ ],
    targets: [
        .target(name: "MyLibary",
                swiftSettings: settings
               ),
        .testTarget(
            name: "MyLibaryTests",
            dependencies: ["MyLibary"],
            swiftSettings: settings),
    ]
)

func otherSwiftFlags(_ path: String) -> [SwiftSetting] {
    var settings = [SwiftSetting]()
    let task = Process()
    let pipe = Pipe()

    let command = "xcodebuild -project \"\(#file.replacingOccurrences(of: "Package.swift", with: ""))\"\(path) -showBuildSettings | grep OTHER_SWIFT_FLAGS"
    task.standardOutput = pipe
    task.standardError = pipe
    task.arguments = ["-c", command]
    task.launchPath = "/bin/zsh"
    task.standardInput = nil
    task.launch()

    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    guard let output = String(data: data, encoding: .utf8),
          let regex = try? NSRegularExpression(pattern: "-D [a-zA-Z0-9_]+", options: []) else { return [] }

    let matches = regex.matches(in: output, options: [], range: NSRange(location: 0, length: output.count))
    matches.forEach {
        let r = $0.range(at: 0)
        guard r.location != NSNotFound,
              let flag = (output as NSString).substring(with: r).components(separatedBy: " ").last
            else { return }
        settings.append(.define(flag))
    }
    return settings
}
3 Likes

Has there been any further development on this topic ? I need to be able to specify a library type in a Package.swift depending on an external condition (namely if the package is built through xcodebuild or Xcode), so :

#if TEST_FLAG
let libType:Product.Library.LibraryType? = nil
#else
let libType:Product.Library.LibraryType? = Product.Library.LibraryType.dynamic
#endif
(...)
let package = Package(
    name: "TestPackage",
    products: [
        .library(
            name: "TestPackage",
            type: libType,
            targets: ["TestPackage"]),
    ]

but I tried passing -DTEST_VALUE through OTHER_SWIFT_FLAGS both through xcodebuild or in Xcode settings, and neither seem to work.

1 Like

For the record, I ended up using an environment variable and a func() returning the library type depending on the var's value :

func runtimeLibType() -> Product.Library.LibraryType?
{
    return ProcessInfo.processInfo.environment["SPM_GENERATE_FRAMEWORK"] != nil ? .dynamic : nil
}

and then

let package = Package(
    name: "FooPackage",
    products: [
        .library(
            name: "FooLibrary",
            type: runtimeLibType(),
            targets: ["FooLibrary"]),
    ]
1 Like

Hi glaurent, I was trying the same approach. But I couldn't find a good place to set the environment variable.
May I ask when and where did you put the export SPM_GENERATE_FRAMEWORK=""?

1 Like

In the shell script I wrote to build an XCPackage out of a Swift Package. If you're using Xcode, I think it can be set in the Xcode build settings.

1 Like

Xcode build step happens after resolving and getting the packages, so it will mean that the code will compile without ENV_VAR exposed.

1 Like