Building an static executable for distribution on macOS

Hello,

I want to create a pull request for the swift static site generator Publish that should contain a build script for the publish cli. Currently I am building the executable in every CI pipeline run, which is super inefficient.

For that purpose it would be great to create an executable which contains all required dependencies and distribute the binary.

Why?

There are several ways to address this problem in general (e.g. a docker container with all dependencies) but there is another project that would benefit from a macOS native executable.

Approach

As mentioned in this linux thread I tried the -static-executable compiler flag.
But that results in the errors below.

git clone https://github.com/JohnSundell/Publish
cd Publish
swift build --configuration release -Xswiftc -static-executable

Error logs (excerpt):

[1/7] Compiling Sweep Sweep.swift
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/usr/include/module.modulemap:118:11: error: header 'stdarg.h' not found
   header "stdarg.h" // note: supplied by the compiler
          ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/usr/lib/swift/Darwin.swiftmodule/x86_64-apple-macos.swiftinterface:4:19: error: could not build Objective-C module 'Darwin'
@_exported import Darwin
                  ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/System/Library/Frameworks/Combine.framework/Modules/Combine.swiftmodule/x86_64-apple-macos.swiftinterface:4:8: error: failed to build module 'Darwin' from its module interface; the compiler that produced it, 'Apple Swift version 5.3.1 (swiftlang-1200.2.41 clang-1200.0.32.8)', may have used features that aren't supported by this compiler, 'Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28)'
import Darwin
       ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/usr/lib/swift/Foundation.swiftmodule/x86_64-apple-macos.swiftinterface:4:8: error: failed to build module 'Combine' from its module interface; the compiler that produced it, 'Apple Swift version 5.3.1 (swiftlang-1200.2.41 clang-1200.0.32.8)', may have used features that aren't supported by this compiler, 'Apple Swift version 5.3.2 (swiftlang-1200.0.45 clang-1200.0.32.28)'
import Combine
       ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/usr/include/module.modulemap:118:11: error: header 'stdarg.h' not found
   header "stdarg.h" // note: supplied by the compiler
          ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/System/Library/Frameworks/CoreFoundation.framework/Headers/CoreFoundation.h:16:10: note: submodule of top-level module 'Darwin' implicitly imported here
#include <sys/types.h>
         ^
<module-includes>:1:9: note: in file included from <module-includes>:1:
#import "Headers/CoreFoundation.h"
        ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/System/Library/Frameworks/CoreFoundation.framework/Headers/CoreFoundation.h:17:10: error: 'stdarg.h' file not found
#include <stdarg.h>
         ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/usr/include/module.modulemap:118:11: error: header 'stdarg.h' not found
   header "stdarg.h" // note: supplied by the compiler
          ^
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/usr/include/dispatch/dispatch.h:25:10: 

The output of the other project looks like this:

git clone https://github.com/worldiety/chia
cd Publish
swift build --configuration release -Xswiftc -static-executable
/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX11.1.sdk/usr/include/module.modulemap:118:11: error: header 'stdarg.h' not found
   header "stdarg.h" // note: supplied by the compiler
          ^
/Users/temp/chia/.build/checkouts/swift-tools-support-core/Sources/TSCLibc/libc.swift:17:19: error: could not build Objective-C module 'Darwin'
@_exported import Darwin.C
                  ^

Question

Unfortunately, I lack a deeper understanding of the compiling/linking process to debug the problem further. Can someone help me with this?

Resources

1 Like

There are always problems building static executables on MacOS where you rely on Apple-private frameworks(modules) like Darwin, Combine, Foundation. On MacOS, these are only provided as dynamic libraries since their use is ubiquitous throughout the operating system, applications, service programs, and much code-sharing is used to keep total system code memory down. In the case of Darwin and Combine, they are Swift-only (although the Darwin module is derived from a lot of C/Objective-C system calls and libraries). Foundation(not the Foundation in Linux/Windows distributions) is used in Swift, C, C++, Objective-C/C++, and all the other languages supported by the O/S. This is not a problem with SPM, it's an architectural decision used in NextOS/MacOS since the '80s.

If you are trying to build a statically linked executable ala Linux, you have to stay away from Apple-private frameworks, which, looking at the modules imported in the source code, is going to require a big re-write.

Is your CI-machine running MacOS? If so, shouldn't be a problem to use an executable using the Apple system frameworks since they are installed in known places as part of the system installation.

Given that Publish is a SwiftPM project, as long as none of its dependencies produce a library target of type .dynamic and only depend on things that are part of the macOS SDK then this already works today.

As noted above, macOS does not support fully static executables. At the very least you are required to dynamically link libSystem. The use-case for "distributable executables" is covered by the various Apple platform SDK versioning guarantees. A distributable macOS binary is one that only dynamically links libraries provided by the system.

In this case, Publish does not declare its minimum deployment target, so its practical minimum deployment target is macOS 10.10. As a result, you ought to be able to build this binary and distribute it however you see fit. You do not need to pass any extra flags: swift build -c release is sufficient. To distribute it to other users you will need to code sign and notarise the binary as usual. SwiftPM does not have any hooks for this.

1 Like

It is available via homebrew, which downloads the prebuilt binary on macOS: publish — Homebrew Formulae

If you want to use a Linux CI runner then you could cache the built package, so you don't always have to rebuild it. I made a GitHub Action for it but it's fairly simple, so it could be easily ported to other CI systems: Cyberbeni/install-swift-tool: GitHub Action to build and cache any Swift based tool in workflows.
(This takes into account swift/linux version changes which make it rebuild but don't happen that often)

For the error in the OP, I'm pretty sure you have to use Linux to build a static executable.

And, the upshot is that you can't build Publish as a statically linked executable because of the dependencies on Apple-private frameworks (Darwin, Combine, Foundation). These are only provided as dynamic frameworks and cannot be linked statically. The OP wants to build a standalone executable with all dependencies statically linked, but that's not possible for a MacOS application. However, if publish is run within a MacOS environment, those dynamic libraries are present as part of the system library/frameworks so there is no need to provide them as part of the executable.

@jonprescott @lukasa @Cyberbeni thank you so much for explaining the situation! The Swift community is really awesome by leaving no one behind <3

@jonprescott the CI machine is runnning macOS and the executable of publish looks good to me. You can have a look at the build script and the assets. Since the binary runs on my laptops I guess it should be fine now.

@lukasa I also added --arch x86_64 --arch arm64 flags to be M1 compatible. Code signing does not seem to be a problem here.

@Cyberbeni your action looks really awesome!

I just looked into the brew file from:

$ brew info publish
publish: stable 0.7.0 (bottled), HEAD
Static site generator for Swift developers
https://github.com/JohnSundell/Publish
Not installed
From: https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/publish.rb

It seems like brew builds the publish cli during brew install

The bottle stores prebuilt files that are inside the install block. If a bottle exists for your OS, that will be downloaded instead, unless you specify the --build-from-source flag.

Ah ok this is good to know!

Now I have several problems with my other tool chia.

Setup

This is the basic setup:

$ git clone https://github.com/JulianKahnert/chia
$ cd chia

Runtime Problem

I have created a build script which uploads the compiled binary (similar to the one from above).
When I download the binary one dynamic lib seems to be missing:

$ ~/Downloads/chia
dyld: Library not loaded: @rpath/lib_InternalSwiftSyntaxParser.dylib
  Referenced from: ~/Downloads/chia
  Reason: image not found
[1]    95500 abort      ~/Downloads/chia

The documentation of swift-syntax gives a hint:

When you do swift build SwiftSyntax links and uses the library included in the Swift toolchain. If you are building an application make sure to embed _InternalSwiftSyntaxParser as part of your application's libraries.

But when I have a look at the Makefile of BartyCrouch a tool that also uses SwiftSyntax I can not find any hint of linking _InternalSwiftSyntaxParser manually or am I missing something?

Multiple Architectures Problem

There is another problem during the build process when I choose multiple architectures. I've found this from @0xTim but the problem looks a little different.
Does anyone has a deeper understanding of this problem?

$ swift build --configuration release --arch x86_64 --arch arm64
0%: Compile Swift source files (arm64)
0%: Compile Swift source files (x86_64)
0%: Compile Swift source files (arm64)
0%: Compile Swift source files (x86_64)
0%: Compile Swift source files (x86_64)
0%: Compile Swift source files (arm64)
0%: Compile Swift source files (arm64)
0%: Compile Swift source files (x86_64)
0%: Compile Files.swift (arm64)
0%: Compile Files.swift (x86_64)
15%: Compile Swift source files (arm64)
15%: Compile Swift source files (x86_64)
15%: Compile Swift source files (x86_64)
15%: Compile Swift source files (arm64)
15%: Compile Swift source files (arm64)
15%: Compile Swift source files (x86_64)
~/chia/.build/apple/Intermediates.noindex/GeneratedModuleMaps/macosx/TSCclibc.modulemap:1:8: error: redefinition of module 'TSCclibc'
module TSCclibc {
       ^
~/chia/.build/checkouts/swift-tools-support-core/Sources/TSCclibc/include/module.modulemap:1:8: note: previously defined here
module TSCclibc {
       ^
<module-includes>:3:9: error: could not build module 'CSQLite3'
#import "~/chia/.build/checkouts/swift-tools-support-core/Sources/TSCclibc/include/csqlite3.h"
        ^
~/chia/.build/checkouts/swift-tools-support-core/Sources/TSCBasic/Process.swift:17:29: error: could not build Objective-C module 'TSCclibc'
@_implementationOnly import TSCclibc
                            ^

Build cancelled

You would only need to distribute _InternalSwiftSyntaxParser if you were distributing a compiled binary. If you compile from source you will necessarily have a Swift toolchain and so you will have a copy of the library you need.

As for TSCclibc this seems to be an issue with SwiftPM: I think you’ll need to file a bug report on bugs.swift.org.

1 Like

IIRC, the TSCclibc problem mentioned here was fixed a while ago, but it's possible that the fix is only in 5.4