Using -lto=llvm-full/thin with xcodebuild

Xcode 16.3 ships Apple Swift version 6.1 and I can use it on the command-line to build a hello world example with link-time optimization:

> cat main.swift
print("Hello world")
> swiftc -O main.swift -lto=llvm-full -o lto_full
> ./lto_full
Hello world

Verbose output (simplified):

> swiftc -O main.swift -lto=llvm-full -o lto_full -v -Xclang-linker -v
Apple Swift version 6.1 (swiftlang-6.1.0.110.21 clang-1700.0.13.3)
Target: arm64-apple-macosx15.0
swift-frontend -frontend -emit-bc -primary-file main.swift -O -lto=llvm-full ... -o /var/folders/.../main-1.bc
clang /var/folders/.../main-1.bc -flto=full -O3 ... -o lto_full -v
  clang -cc1 ... -flto=full ... -o /var/folders/.../main-1-e1c2b7.o -x ir /var/folders/.../main-1.bc
  ld -o lto_full ... /var/folders/.../main-1-e1c2b7.o

Shows the steps:

  1. swift-frontend compiles main.swift to LLVM bitcode and writes main-1.bc
  2. clang runs two steps:
    a. clang -cc1 optimizes the LLVM bitcode and writes main-1-e1c2b7.o
    b. ld links main-1-e1c2b7.o as lto_full executable

The same works with -lto=llvm-thin

How can I do this with Xcode or xcodebuild?

The Xcode UI doesn't seem to expose an option for -lto. Passing it as OTHER_SWIFT_FLAGS in my hello world Swift command-line app causes a strange error:

> xcodebuild -scheme Test -configuration Release OTHER_SWIFT_FLAGS="-lto=llvm-full"
...
ld: file cannot be open()ed, errno=2 path=/Users/.../Objects-normal/arm64/main.o

The linker doesn't find the object file main.o because it doesn't exist. We only have the LLVM bitcode file:

> ls /Users/.../Objects-normal/arm64/main.* 
/Users/.../Objects-normal/arm64/main.bc
1 Like

Swift Forums is dedicated to swift.org open source project and closed-source software like Xcode is off-topic here. If you're interested exclusively in Xcode, you should file an issue at https://feedbackassistant.apple.com.

To possibly keep this on-topic, is it reproducible for you when using --build-system swiftbuild with latest snapshots, which hopefully should be closer to xcodebuild in its behavior? Maybe @jakepetroules or @owenv can provide some other steps to reproduce this with swift.org distributions outside of Xcode and xcodebuild.

Thanks for the quick reply. Yes that seems to reproduce with swift-build. Here is what I did.

In the folder with my Test.xcodeproject I initialized a swift-build package with swift package init --type executable. This is what I get:

> tree -L2 .
.
โ”œโ”€โ”€ Package.swift
โ”œโ”€โ”€ Sources
โ”‚   โ””โ”€โ”€ main.swift
โ”œโ”€โ”€ Test
โ”‚   โ””โ”€โ”€ main.swift
โ””โ”€โ”€ Test.xcodeproj
    โ”œโ”€โ”€ project.pbxproj
    โ”œโ”€โ”€ project.xcworkspace
    โ”œโ”€โ”€ xcshareddata
    โ””โ”€โ”€ xcuserdata

7 directories, 4 files

swift build -c release works and swift run prints "Hello world" as expected.

Now, I added the -lto option to the generated Package.swift (and -v for clang):

// swift-tools-version: 6.1
import PackageDescription
let package = Package(
    name: "Test",
    targets: [
        .executableTarget(
            name: "Test",
            swiftSettings: [
                .unsafeFlags(["-lto=llvm-full"], .when(configuration: .release)),
            ],
            linkerSettings: [
                .unsafeFlags(["-Xclang-linker", "-v"], .when(configuration: .release))
            ]
        ),
    ]
)

I cleared the build folder and run the build with LTO and verbose output:

> rm -rf .build
> swift build -c release -v

This fails in a very similar way as xcodebuild:

clang: error: no such file or directory: '/Users/.../Test/.build/arm64-apple-macosx/release/Test.build/main.swift.o'

Seems like it's not allowed to attach logs, so I put the log with the full output here: https://pastebin.com/raw/9U0xs9dg

This is very similar to the behavior in xcodebuild: swiftc produces a LLVM bitcode file that clang cannot find. In particular (you can find all of it in the log above), I get:

  1. swift-build writing the linker input paths to /Users/.../Test/.build/arm64-apple-macosx/release/Test.product/Objects.LinkFileList
  2. swift-frontend compiling the LLVM bitcode file in my CWD (this is clearly a bug on its own): swift-frontend -frontend -emit-bc /Users/.../Test/Sources/main.swift -o main.bc
  3. clang looking for the LLVM bitcode in the Objects.LinkFileList path /Users/.../Test/.build/arm64-apple-macosx/release/Test.build/main.swift.o, which doesn't exist

When I change the clang invocation and point it at the actual bitcode file /Users/.../Test/main.bc instead, it works perfectly:

> /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang /Users/.../Test/main.bc -F /Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks -F /Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks --sysroot /Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk --target=arm64-apple-macosx10.13 -force_load /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/libswiftCompatibilityConcurrency.a -force_load /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/libswiftCompatibility56.a /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/libswiftCompatibilityPacks.a -L /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx -L /Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk/usr/lib/swift -rpath /usr/lib/swift -L /Users/.../Test/.build/arm64-apple-macosx/release -L /Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib -Xlinker -no_warn_duplicate_libraries -Xlinker -dead_strip -Xlinker -alias -Xlinker _Test_main -Xlinker _main -Xlinker -rpath -Xlinker @loader_path -Xlinker -rpath -Xlinker /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.5/macosx -v -flto=full -o /Users/.../Test/.build/arm64-apple-macosx/release/Test
Apple clang version 17.0.0 (clang-1700.0.13.3)
Target: arm64-apple-macosx10.13
Thread model: posix
InstalledDir: /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
 "/Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang" -cc1 -triple arm64-apple-macosx10.13.0 -Wundef-prefix=TARGET_OS_ -Wdeprecated-objc-isa-usage -Werror=deprecated-objc-isa-usage -Werror=implicit-function-declaration -emit-llvm-bc -flto=full -flto-unit -dumpdir /Users/.../Test/.build/arm64-apple-macosx/release/Test- -disable-free -clear-ast-before-backend -disable-llvm-verifier -discard-value-names -main-file-name main.bc -mrelocation-model pic -pic-level 2 -mframe-pointer=non-leaf -fno-strict-return -ffp-contract=on -fno-rounding-math -funwind-tables=1 -fobjc-msgsend-selector-stubs -fcompatibility-qualified-id-block-type-checking -fvisibility-inlines-hidden-static-local-var -fbuiltin-headers-in-system-modules -fdefine-target-os-macros -fno-assume-unique-vtables -target-cpu apple-m1 -target-feature +zcm -target-feature +zcz -target-feature +v8.5a -target-feature +aes -target-feature +altnzcv -target-feature +ccdp -target-feature +complxnum -target-feature +crc -target-feature +dotprod -target-feature +fp-armv8 -target-feature +fp16fml -target-feature +fptoint -target-feature +fullfp16 -target-feature +jsconv -target-feature +lse -target-feature +neon -target-feature +pauth -target-feature +perfmon -target-feature +predres -target-feature +ras -target-feature +rcpc -target-feature +rdm -target-feature +sb -target-feature +sha2 -target-feature +sha3 -target-feature +specrestrict -target-feature +ssbs -target-abi darwinpcs -debugger-tuning=lldb -fdebug-compilation-dir=/Users/.../Test -target-linker-version 1167.4.1 -v -fcoverage-compilation-dir=/Users/.../Test -resource-dir /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/17 -Wno-reorder-init-list -Wno-implicit-int-float-conversion -Wno-c99-designator -Wno-final-dtor-non-final-class -Wno-extra-semi-stmt -Wno-misleading-indentation -Wno-quoted-include-in-framework-header -Wno-implicit-fallthrough -Wno-enum-enum-conversion -Wno-enum-float-conversion -Wno-elaborated-enum-base -Wno-reserved-identifier -Wno-gnu-folding-constant -ferror-limit 19 -stack-protector 1 -fstack-check -mdarwin-stkchk-strong-link -fblocks -fencode-extended-block-signature -fregister-global-dtors-with-atexit -fgnuc-version=4.2.1 -fskip-odr-check-in-gmf -fmax-type-align=16 -fcommon -fcolor-diagnostics -clang-vendor-feature=+disableNonDependentMemberExprInCurrentInstantiation -fno-odr-hash-protocols -clang-vendor-feature=+enableAggressiveVLAFolding -clang-vendor-feature=+revert09abecef7bbf -clang-vendor-feature=+thisNoAlignAttr -clang-vendor-feature=+thisNoNullAttr -clang-vendor-feature=+disableAtImportPrivateFrameworkInImplementationError -D__GCC_HAVE_DWARF2_CFI_ASM=1 -o /var/folders/_g/4v0k_c211371j17qv5gd5y_h0000gn/T/main-e5d497.o -x ir /Users/.../Test/main.bc
clang -cc1 version 17.0.0 (clang-1700.0.13.3) default target arm64-apple-darwin24.4.0
 "/Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld" -demangle -object_path_lto /var/folders/_g/4v0k_c211371j17qv5gd5y_h0000gn/T/cc-5a4cc4.o -lto_library /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libLTO.dylib -no_deduplicate -dynamic -arch arm64 -force_load /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/libswiftCompatibilityConcurrency.a -force_load /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/libswiftCompatibility56.a -platform_version macos 11.0.0 11.0.0 -syslibroot /Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk -mllvm -enable-linkonceodr-outlining -o /Users/.../Test/.build/arm64-apple-macosx/release/Test -L/Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx -L/Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX15.4.sdk/usr/lib/swift -L/Users/.../Test/.build/arm64-apple-macosx/release -L/Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/usr/lib /var/folders/_g/4v0k_c211371j17qv5gd5y_h0000gn/T/main-e5d497.o /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/libswiftCompatibilityPacks.a -rpath /usr/lib/swift -no_warn_duplicate_libraries -dead_strip -alias _Test_main _main -rpath @loader_path -rpath /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.5/macosx -lSystem /Applications/Xcode_16.3.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/17/lib/darwin/libclang_rt.osx.a -F/Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/Frameworks -F/Applications/Xcode_16.3.app/Contents/Developer/Platforms/MacOSX.platform/Developer/Library/PrivateFrameworks

Edit: Also added .unsafeFlags(["-Xclang-linker", "-flto=full"], .when(configuration: .release)) to linkerSettings in Package.swift but that didn't affect the behavior of swift-build

Thanks for the detailed investigation! This seems to me like something that SwiftPM should be able to handle regardless of the underlying build system. Would you mind filing an issue with your findings on the SwiftPM repository?

In general, you're not going to find much success passing unsafe flags, especially if those flags change things like compiler outputs, because the build planning done by Xcode and SwiftPM (and most build systems, typically) won't take those into account beyond passing them opaquely to the compiler/linker. So if a flag changes the compiler's outputs from .o to .bc, SwiftPM won't be aware of that and it'll still pass expect the former.

It looks like SwiftPM added a --experimental-lto-mode={thin,full} flag a while back that addresses the different expectations in file extensions; that might be worth a try. (But I don't know if there's a way to get Xcode to pass that through.)

It's not wired up to swift build --build-system swiftbuild right now (filed --experimental-lto-mode={thin,full} should override SWIFT_LTO setting with --build-system swiftbuild ยท Issue #8682 ยท swiftlang/swift-package-manager ยท GitHub to track this), but SWIFT_LTO=YES or SWIFT_LTO=YES_THIN can be used to enable LTO for Swift sources in the context of an Xcode project, either as a user defined build setting or command line settings override.

2 Likes

As the person who added SwiftPM's --experimental-lto-mode={thin,full} flag, I'd like to note that you will find bugs with this implementation currently (like -lto_object_path not being set). I assume the new/wip swiftbuild backend for SwiftPM will be a lot more robust.

1 Like

Perfect, thanks! I only tested LLVM_LTO before and had no luck. The SWIFT_LTO flag is exactly what I was looking for. In Xcode 16.3, this works with my initial hello world example in xcodebuild:

> xcodebuild -scheme Test -configuration Release SWIFT_LTO=YES
> cat /Users/.../Objects-normal/arm64/Test.LinkFileList
/Users/.../Objects-normal/arm64/main.bc

I didn't get anywhere with swift build quickly. It seems like SWIFT_LTO isn't supported here and --experimental-lto-mode isn't yet available in this release of the toolchain.

FWIW that experimental lto option has been in swift build for a while:

โžœ swiftly use
Swift 6.0.3 (default)
โžœ swift build --version
Swift Package Manager - Swift 6.0.3
โžœ swift build --help-hidden | rg lto
  --experimental-lto-mode <experimental-lto-mode>