Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
.DS_Store
/.build
/Derived
/OpenRenderBox.xcodeproj
/OpenRenderBox.xcworkspace
/Workspace.xcworkspace
/Packages
xcuserdata/
DerivedData/
Expand All @@ -9,4 +13,4 @@ DerivedData/
.claude/
.rb_template/
.augment/
/.build-debug
/.build-debug
5 changes: 3 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,9 @@ let openRenderBoxTarget = Target.target(
// The SwiftPM support for such usage is still in progress.
let openRenderBoxCxxTarget = Target.target(
name: "OpenRenderBoxCxx",
cSettings: sharedCSettings,
cxxSettings: sharedCxxSettings
publicHeadersPath: "include",
cSettings: sharedCSettings + [.headerSearchPath(".")],
cxxSettings: sharedCxxSettings + [.headerSearchPath(".")]
)
let openRenderBoxShimsTarget = Target.target(
name: "OpenRenderBoxShims",
Expand Down
130 changes: 130 additions & 0 deletions Project.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import Foundation
import ProjectDescription

let sourceRoot = "Sources/OpenRenderBoxCxx"
let root = URL(fileURLWithPath: #filePath).deletingLastPathComponent()
let sourceModuleMap = try String(
contentsOf: root.appendingPathComponent("\(sourceRoot)/include/module.modulemap"),
encoding: .utf8
)
let moduleHeaders = sourceModuleMap.components(separatedBy: .newlines).compactMap { line -> String? in
guard line.contains("header \"") else { return nil }
return line.components(separatedBy: "\"")[1]
}

// Use the same module declarations for SwiftPM and the framework.
let frameworkModuleMap = "framework " + sourceModuleMap
.replacingOccurrences(of: "module OpenRenderBoxCxx", with: "module OpenRenderBox")
.replacingOccurrences(of: "module OpenRenderBox.C {", with: "module OpenRenderBox.C {\n header \"ORBConfiguration.h\"")
.replacingOccurrences(of: "header \"OpenRenderBox/", with: "header \"")
.replacingOccurrences(of: "header \"../", with: "header \"")
let moduleMapDirectory = root.appendingPathComponent("Derived/ModuleMaps")
try FileManager.default.createDirectory(at: moduleMapDirectory, withIntermediateDirectories: true)
try frameworkModuleMap.write(
to: moduleMapDirectory.appendingPathComponent("OpenRenderBox.modulemap"),
atomically: true,
encoding: .utf8
)

// Consumers must use the same backend as the framework binary.
let frameworkConfiguration = """
#pragma once

#if defined(OPENRENDERBOX_CF_CGTYPES) && OPENRENDERBOX_CF_CGTYPES != 1
#error "This OpenRenderBox framework requires OPENRENDERBOX_CF_CGTYPES=1."
#endif
#ifndef OPENRENDERBOX_CF_CGTYPES
#define OPENRENDERBOX_CF_CGTYPES 1
#endif

"""

// Preserve feature paths for the C++ and Objective-C submodules.
let internalHeaders = moduleHeaders.filter { $0.hasPrefix("../") }.map { String($0.dropFirst(3)) }
// Tuist otherwise treats template headers as resources and adds a resource bundle.
let templateHeaders = internalHeaders.filter { $0.hasSuffix(".tpp") }
let headerCopyCommands = internalHeaders.map { header in
let directory = (header as NSString).deletingLastPathComponent
return """
mkdir -p "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/\(directory)"
cp "${SRCROOT}/\(sourceRoot)/\(header)" "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/\(header)"
"""
}.joined(separator: "\n")
let exportModuleHeaders = TargetScript.pre(
script: """
set -eu
mkdir -p "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}"
cat > "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/ORBConfiguration.h" <<'EOF'
\(frameworkConfiguration)
EOF
\(headerCopyCommands)
""",
name: "Export module headers",
inputPaths: internalHeaders.map { .glob(.path("\(sourceRoot)/\($0)")) },
outputPaths: [.path("$(TARGET_BUILD_DIR)/$(PUBLIC_HEADERS_FOLDER_PATH)/ORBConfiguration.h")]
+ internalHeaders.map { .path("$(TARGET_BUILD_DIR)/$(PUBLIC_HEADERS_FOLDER_PATH)/\($0)") },
basedOnDependencyAnalysis: true
)

let destinations: Destinations = [.mac, .iPhone, .iPad, .macCatalyst, .appleTv, .appleWatch, .appleVision]
let deploymentTargets = DeploymentTargets.multiplatform(
iOS: "18.0", macOS: "15.0", watchOS: "10.0", tvOS: "18.0", visionOS: "2.0"
)

let project = Project(
name: "OpenRenderBox",
settings: .settings(base: [
"CLANG_CXX_LANGUAGE_STANDARD": "c++20",
"GCC_PREPROCESSOR_DEFINITIONS": ["$(inherited)", "OPENRENDERBOX_CF_CGTYPES"],
"SWIFT_ACTIVE_COMPILATION_CONDITIONS": ["$(inherited)", "OPENRENDERBOX_CF_CGTYPES"],
Comment on lines +78 to +79

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Export the framework's backend configuration to consumers

For standalone framework consumers, these project-level definitions are not propagated when importing the copied bundle. The exported ORBPath.h, ORBPathCallbacks.h, and ORBPathStorage.h all guard their declarations with #if OPENRENDERBOX_CF_CGTYPES, but neither the exported headers nor the generated framework module map defines that macro. Consequently, the framework builds its native path implementation while ordinary C and Swift consumers cannot see its path API unless they manually supply an undocumented build flag. Encode the framework's fixed backend configuration in its exported headers so clients see the same API that was built; the current framework client test only exercises color and animation APIs and misses this mismatch.

Useful? React with 👍 / 👎.

"SWIFT_VERSION": "5.0",
]),
targets: [
.target(
name: "OpenRenderBoxCxx",
destinations: destinations,
product: .staticLibrary,
bundleId: "org.openswiftui.OpenRenderBoxCxx",
deploymentTargets: deploymentTargets,
infoPlist: nil,
buildableFolders: [
.folder(.path(sourceRoot), exceptions: [.exception(excluded: ["include/module.modulemap"] + templateHeaders)])
],
settings: .settings(base: [
"HEADER_SEARCH_PATHS": ["$(SRCROOT)/\(sourceRoot)", "$(SRCROOT)/\(sourceRoot)/include"],
// Match SwiftPM while path operations still have incomplete implementations.
"GCC_WARN_ABOUT_RETURN_TYPE": "YES",
"SKIP_INSTALL": "YES",
])
),
.target(
name: "OpenRenderBox",
destinations: destinations,
product: .framework,
bundleId: "org.openswiftui.OpenRenderBox",
deploymentTargets: deploymentTargets,
buildableFolders: ["Sources/OpenRenderBox"],
headers: .headers(public: .list([.glob(.path("\(sourceRoot)/include/OpenRenderBox/*.h"))])),
scripts: [exportModuleHeaders],
dependencies: [
.target(name: "OpenRenderBoxCxx"),
.sdk(name: "Foundation", type: .framework),
.sdk(name: "CoreGraphics", type: .framework),
.sdk(name: "QuartzCore", type: .framework),
.sdk(name: "Metal", type: .framework),
.sdk(name: "c++", type: .library),
],
settings: .settings(base: [
"BUILD_LIBRARY_FOR_DISTRIBUTION": "YES",
// Keep Xcode responsible for installing the framework module map.
"MODULEMAP_FILE[sdk=*]": "$(SRCROOT)/Derived/ModuleMaps/OpenRenderBox.modulemap",
// The Swift target does not reference every exported C or Objective-C symbol.
"OTHER_LDFLAGS": ["$(inherited)", "-force_load", "$(BUILT_PRODUCTS_DIR)/libOpenRenderBoxCxx.a"],
"OTHER_SWIFT_FLAGS": ["$(inherited)", "-package-name", "OpenRenderBox", "-enable-upcoming-feature", "InternalImportsByDefault"],
"SKIP_INSTALL": "NO",
// ORBVersion.c defines the framework version symbols.
"VERSIONING_SYSTEM": "",
])
),
]
)
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,65 @@ Currently, this project is in early development.

The current suggested toolchain to build the project is Swift 6.3.3 / Xcode 26.6.

Tuist builds `OpenRenderBoxCxx` as a static library and links it into the dynamic
`OpenRenderBox.framework`. This build uses the Darwin SDK's CoreGraphics types.
Use SwiftPM for the other backend configurations.

```sh
tuist generate run --no-open --cache-profile none
tuist xcodebuild build -workspace OpenRenderBox.xcworkspace -scheme OpenRenderBox \
-configuration Release -destination 'generic/platform=macOS' \
-derivedDataPath .build/tuist-framework CODE_SIGNING_ALLOWED=NO
Scripts/Tests/framework-test .build/tuist-framework/Build/Products/Release/OpenRenderBox.framework
```

The framework contains the public C headers and the C++, Objective-C, and Swift
modules. The client test uses a separate copy of the bundle with no source search
paths and compiles the Swift interface.
The framework exports `ORBConfiguration.h` with its fixed CoreGraphics backend
setting. Consumers can use its path APIs without extra compiler definitions.
SwiftPM continues to select the backend with `OPENRENDERBOX_CF_CGTYPES`.

## Source Layout

`Sources/OpenRenderBoxCxx` groups headers and implementations by feature.
`include/OpenRenderBox` contains relative symbolic links to the public C and
Objective-C headers, so each declaration has one source file. Clients keep paths such as
`<OpenRenderBox/ORBPath.h>`. Headers that share C and Objective-C declarations
use `ORB_OBJC_FOUNDATION` to guard the Objective-C declarations.

The target uses `publicHeadersPath: "include"`. The module map lists the exported
headers and preserves the Swift submodule names and language requirements.
Swift sources remain in the separate `OpenRenderBox` target.

Framework builds and template exports copy the linked header contents. The
framework client test checks that exported headers are regular files.

### Add an exported header

Create the header beside its implementation, then register it with Python 3.9 or
later. Pass a path relative to the repository root, or an absolute path, and an
existing submodule name:

```sh
Scripts/add_header.py Sources/OpenRenderBoxCxx/Animation/ORBNewAnimation.h C
Scripts/add_header.py Sources/OpenRenderBoxCxx/Animation/ORBNewObserver.h ObjC
Scripts/add_header.py Sources/OpenRenderBoxCxx/Animation/NewAnimation.hpp Animation
```

The script creates a relative public-header link for C and Objective-C modules
and adds the header to `include/module.modulemap`. Modules with `requires cplusplus`
keep feature paths and do not use flat links. Files with a `.tpp` extension use
`textual header`. Repeated calls do not add duplicates. The script rejects name
conflicts and headers already registered in another module.

If a C header must also be available through `<OpenRenderBox/OpenRenderBox.h>`,
add its include to that header. Internal headers do not need registration.
Regenerate the Tuist project after registration, then build the framework with
the commands above. SwiftPM reads the updated source module map directly.

Run the helper tests with `Scripts/Tests/add-header-test`.

## Shims

The `OpenRenderBoxShims` product provides Swift aliases and a C header for path APIs.
Expand Down
Loading
Loading