Picking up the epic tale of printing a Hello on the terminal, this post details how to encapsulate the static library within a framework.

This is the script. Paste it to the terminal and you shall see a Hello World! message.

Static Framework demo

# Ignore lines that start with a hash (#)
setopt INTERACTIVE_COMMENTS

# Skip commands where the glob pattern does not match any files
setopt null_glob

# Prepare an empty folder
mkdir -p ~/Desktop/Hello
cd ~/Desktop/Hello
rm -rf *


# BUILD DYNAMIC LIBRARY

# Compile the library source
cat << EOF > Greeter.swift
public class Greeter {
    public init() {}
    public func hello() {
        print("Hello World!")
    }
}
EOF

# Create directories for each architecture
mkdir -p arm64 x86_64

# Compile Hello.swift to object files and generate module files for each architecture
swiftc -parse-as-library \
    -emit-object -o arm64/Hello.o \
    -emit-module -module-name Hello -emit-module-path arm64/Hello.swiftmodule \
    -enable-library-evolution -emit-module-interface-path arm64/Hello.swiftinterface \
    -target arm64-apple-macosx10.9.0 \
    Greeter.swift

swiftc -parse-as-library \
    -emit-object -o x86_64/Hello.o \
    -emit-module -module-name Hello -emit-module-path x86_64/Hello.swiftmodule \
    -enable-library-evolution -emit-module-interface-path x86_64/Hello.swiftinterface \
    -target x86_64-apple-macosx10.9.0 \
    Greeter.swift

# Create a universal (fat) static library from the object files
lipo -create arm64/Hello.o x86_64/Hello.o -output Hello.o
libtool -static -o libHello.a Hello.o
rm Hello.o


# BUILD STATIC FRAMEWORK

# Create framework structure
mkdir -p Hello.framework/Versions/A/
mkdir -p Hello.framework/Versions/A/Modules
mkdir -p Hello.framework/Versions/A/Modules/Hello.swiftmodule
mkdir -p Hello.framework/Versions/A/Headers
mkdir -p Hello.framework/Versions/A/Resources

# Move static library
mv libHello.a Hello.framework/Versions/A/Hello

# Move module files
mv arm64/Hello.swiftdoc Hello.framework/Versions/A/Modules/
mv arm64/Hello.abi.json Hello.framework/Versions/A/Modules/
mv arm64/Hello.swiftsourceinfo Hello.framework/Versions/A/Modules/

mv arm64/Hello.swiftmodule Hello.framework/Versions/A/Modules/Hello.swiftmodule/arm64.swiftmodule
mv arm64/Hello.swiftinterface Hello.framework/Versions/A/Modules/Hello.swiftmodule/arm64.swiftinterface
mv arm64/Hello.private.swiftinterface Hello.framework/Versions/A/Modules/Hello.swiftmodule/arm64.private.swiftinterface

mv x86_64/Hello.swiftmodule Hello.framework/Versions/A/Modules/Hello.swiftmodule/x86_64.swiftmodule
mv x86_64/Hello.swiftinterface Hello.framework/Versions/A/Modules/Hello.swiftmodule/x86_64.swiftinterface
mv x86_64/Hello.private.swiftinterface Hello.framework/Versions/A/Modules/Hello.swiftmodule/x86_64.private.swiftinterface

# This modulemap is superfluous unless you use the library from Objetive-C.
cat << EOF > Hello.framework/Versions/A/Modules/module.modulemap
framework module Hello {
  header "HelloFramework.h"
  export *
}
EOF

# This header is superfluous unless you use the library from Objetive-C.
cat << EOF > Hello.framework/Versions/A/Headers/HelloFramework.h
#import <Foundation/Foundation.h>
void hello(void);
EOF

cat << EOF > Hello.framework/Versions/A/Info.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleDevelopmentRegion</key>
    <string>en</string>
    <key>CFBundleExecutable</key>
    <string>Hello</string>
    <key>CFBundleIdentifier</key>
    <string>com.example.Hello</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundleName</key>
    <string>Hello</string>
    <key>CFBundlePackageType</key>
    <string>FMWK</string>
    <key>CFBundleShortVersionString</key>
    <string>1.0</string>
    <key>CFBundleVersion</key>
    <string>1</string>
    <key>NSHumanReadableCopyright</key>
    <string>Copyright © 2024 Hello Company. All rights reserved.</string>
</dict>
</plist>
EOF

# Create symbolic links to speed up access
cd Hello.framework/Versions
ln -s A Current
cd ..
ln -s Versions/Current/Hello Hello
ln -s Versions/Current/Headers Headers
ln -s Versions/Current/Info.plist Info.plist
ln -s Versions/Current/Resources Resources
ln -s Versions/Current/Modules Modules

cd ..
chmod -R 755 Hello.framework


# BUILD THE CLIENT

# Create UseHello.swift
cat << EOF > UseHello.swift
import Hello
@main
public struct UseHello {
    public static func main() {
        let greeter = Greeter()
        greeter.hello()
    }
}
EOF

# Compile UseHello.swift into executables for each architecture
swiftc -parse-as-library \
    -o UseHello-arm64  UseHello.swift \
    -target arm64-apple-macosx10.9.0 \
    -F. -framework Hello -I Hello.framework/Modules/arm64

swiftc -parse-as-library \
    -o UseHello-x86_64 UseHello.swift \
    -target x86_64-apple-macosx10.9.0 \
    -F. -framework Hello -I Hello.framework/Modules/x86_64

# Create a universal (fat) binary
lipo -create UseHello-arm64 UseHello-x86_64 -output UseHello

# Clean up intermediate files
rm UseHello-arm64 UseHello-x86_64

# cleanup
rm -rf arm64 
rm -rf x86_64


# EXECUTE

# Execute
./UseHello


# CREATE EXECUTABLE PACKAGE

# Encapsulate the libHello.a in a xcframework
xcodebuild -create-xcframework \
    -framework Hello.framework \
    -output Hello.xcframework

# Optionally sign the framework
# security find-identity -v -p codesigning
# codesign --sign "YOUR_ID_HERE" --timestamp --options runtime Hello.xcframework

# Remove everything except the xcframework
# I’m discarding the message 'rm: Hello.xcframework: is a directory'
rm * 2>/dev/null

# Create an executable 
swift package init --type executable --name UseHello

# Overwrite the Package.swift to add the dependency
cat << EOF > Package.swift
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "UseHello",
    platforms: [.macOS(.v15)],
    products: [
        .executable(name: "UseHello", targets: ["UseHello"])
    ],
    dependencies: [],
    targets: [
        .executableTarget(name: "UseHello", dependencies: ["Hello"]),
        .binaryTarget(name: "Hello", path: "./Hello.xcframework")
    ]
)
EOF

# Replace the default main.swift file.
rm Sources/main.swift
mkdir -p Sources/UseHello
cat << EOF > Sources/UseHello/main.swift
import Hello
Greeter().hello()
EOF

swift run --arch x86_64 
swift run --arch arm64
swift build --arch x86_64 --arch arm64

Here are the key steps:

  1. Creates a Fat Static Library –which we discussed in a previous article.
  2. Adds a Info.plist with metadata.
  3. Arranges files into a framework structure.
  4. Passes some flags to the client to indicate the path to the framework.

Framework structure

The script produces a framework with this structure:

% tree -F Hello.framework 
Hello.framework/
├── Headers -> Versions/Current/Headers/
├── Hello -> Versions/Current/Hello*
├── Modules -> Versions/Current/Modules/
├── Resources -> Versions/Current/Resources/
└── Versions/
    ├── A/
    │   ├── Headers/
    │   │   └── HelloFramework.h*
    │   ├── Hello*
    │   ├── Modules/
    │   │   ├── Hello.abi.json*
    │   │   ├── Hello.swiftdoc*
    │   │   ├── Hello.swiftmodule/
    │   │   │   ├── arm64.private.swiftinterface*
    │   │   │   ├── arm64.swiftinterface*
    │   │   │   ├── arm64.swiftmodule*
    │   │   │   └── x86_64.swiftmodule*
    │   │   ├── Hello.swiftsourceinfo*
    │   │   └── module.modulemap*
    │   └── Resources/
    │       └── Info.plist*
    └── Current -> A/

This is the basic structure without symbolic links:

Hello.framework/
└── Versions/
    └── A/
        ├── Headers/
        ├── Modules/
        ├── Resources/
        │   └── Info.plist
        └── Executable

Headers

I included a header HelloFramework.h and a module.modulemap for compatibility with C-based languages. They are not really needed in a Swift-only project like this.

Modules

The structure of the Modules folder is the tricky part.

Given that files .abi.json, .swiftdoc, .swiftsourceinfo are the same for both architectures we just put them there. These files contain supplementary information that doesn’t depend on architecture, such as documentation comments, ABI (Application Binary Interface) details, and source information for debugging.

But the module/interface files are different, and the convention is to place them at

 <FW_NAME>.framework/Modules/<FW_NAME>.swiftmodule/<ARCH>.<extension>

Therefore:

Modules/Hello.swiftmodule/
├── arm64.private.swiftinterface
├── arm64.swiftinterface
├── arm64.swiftmodule
├── x86_64.private.swiftinterface
├── x86_64.swiftinterface
└── x86_64.swiftmodule

I learned this from looking inside xcarchive. This arrangement appears on several places, for instance, look at modules at /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/prebuilt-modules/15.0/. I tried it and it worked out. I don’t know of official documentation on this.

Resources

This folder contains non-code resources like image, audio, localization, storyboards, etc.

Version

That A could be a B, C, D, .. containing several versions of the framework where there is a need to support old and new clients. The Info.plist is intuitive to read. Then there are a number of symbolic links (->) that provide direct access to the latest version of the framework.

Client flags

To build a fat client compile twice and join the result. Here is the command for ARM64.

swiftc -parse-as-library \
    -o UseHello-arm64 \
    -target arm64-apple-macosx10.9.0 \
    -F. \
    -framework Hello \
    -I Hello.framework/Modules/arm64 \
    UseHello.swift

Mind the flags:

  • -F. adds the current folder to the framework search path.
  • -I Hello.framework/Modules/arm64 adds a search path for Swift module interfaces.
  • -framework Hello linker flag that tells the compiler to link against the Hello framework.

Binary Inspection

Commands to verify the export of the hello function and check the architectures included in the framework:

# was the hello function exported?
% nm Hello.framework/Versions/A/Hello | grep hello | swift demangle
000000000000005c T Hello.Hello.hello() -> ()
0000000000000408 S method descriptor for Hello.Hello.hello() -> ()

# what are the framework architectures?
% lipo -info Hello.framework/Versions/A/Hello
Architectures in file: Hello.framework/Versions/A/Hello are: x86_64 arm64

Using it from SPM

Same as Hello Static Library but passing -framework instead -library.

xcodebuild -create-xcframework \
    -framework Hello.framework \
    -output Hello.xcframework

Conclusion

This article encapsulates a static library into a static framework, detailing the framework structure.

This framework is ready to distribute. However, the optimization process that happens during static linking is too slow for development. That’s why we need dynamic libraries and frameworks for development. I’ll be doing that in Hello Dynamic Framework.

References

There are two main resources that describe the framework structure. They are old and none of them explain the .swiftmodule/ folder arrangement.

Articles

Sessions