Skip to content
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
# Changelog
## v0.22.0 - 2026-10-05
### 🐞 Fixes
- [Patch] Show every line of an export's output in the Console (#143) (a204708…)
### 🚀 Features
- [Feature] Editor UI redesign, part 2: hierarchy with eye and lock, scene tabs and Inspector sections (#142) (046df92…)
- [Feature] Cook imported sources into the folder they were imported into (#144) (5cfecd6…)
- [Feature] Editor UI redesign, part 3: viewport header, input, overlays and play mode (#146) (3d87595…)
- [Feature] Editor UI redesign, part 4: selecting several entities (#147) (19a88ff…)
- [Feature] Component SDK: every C module the engine was built with, not only CShaderTypes (#148) (9ede444…)
## v0.21.0 - 2026-09-27
### 🐞 Fixes
- [Patch] Dividers: system resize cursor and a preview line instead of live resizing (e27b770…)
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ let package = Package(
// Use a branch during active development:
// .package(url: "https://github.com/untoldengine/UntoldEngine.git", branch: "develop"),
// Or pin to a release:
// .package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.21.0"),
// .package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.22.0"),
// Temporarily tracks the Gaussian splat streaming series on the fork until those engine
// PRs land in a release; restore the exact pin afterwards.
.package(url: "https://github.com/miolabs/UntoldEngine.git", branch: "develop"),
Expand Down
10 changes: 5 additions & 5 deletions Sources/UntoldEditor/Components/ComponentCompiler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -140,11 +140,11 @@ enum ComponentCompiler {
if request.unit.reloadableImports.isEmpty == false {
arguments += ["-I", request.outputDirectory.path]
}
arguments += [
"-Xcc", "-fmodule-map-file=\(request.sdk.cShaderTypesModuleMap.path)",
// Link nothing of the engine: its symbols are found in the running editor at load.
"-Xlinker", "-undefined", "-Xlinker", "dynamic_lookup",
]
for moduleMap in request.sdk.cModuleMaps {
arguments += ["-Xcc", "-fmodule-map-file=\(moduleMap.path)"]
}
// Link nothing of the engine: its symbols are found in the running editor at load.
arguments += ["-Xlinker", "-undefined", "-Xlinker", "dynamic_lookup"]
arguments += request.unit.sources.map(\.path)
return arguments
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/UntoldEditor/Components/ComponentProject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ struct ComponentProjectLayout: Equatable {
let pluginsDirectory: URL
/// In build order: package runtimes, package editor sources, then the project's folder.
let units: [ComponentSourceUnit]
/// Manifest and plugin package problems, shown in the Plugins panel.
/// Manifest, plugin package and SDK problems, shown in the Plugins panel.
let problems: [String]

var pluginsDirectoryExists: Bool {
Expand Down Expand Up @@ -159,6 +159,11 @@ enum ComponentSourceLocator {
problems.append("\(EditorProjectManifest.fileName) uses an old key: \(legacyKey).")
}

// The compiler would only say that a required module is missing.
for module in sdk?.cModulesWithoutModuleMap ?? [] {
problems.append("No module map was found for the C module \(module), which this editor was built with. Plugins do not compile if the engine imports it.")
}

let pluginsDirectory: URL
if let custom = manifest?.pluginsFolder, custom.isEmpty == false {
pluginsDirectory = resolve(custom, relativeTo: root)
Expand Down
39 changes: 27 additions & 12 deletions Sources/UntoldEditor/Components/ComponentSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

import Foundation

/// The Swift modules a component library compiles against.
/// The modules a component library compiles against.
///
/// Compiling against the editor's own modules, and linking nothing, is what makes loaded code
/// share the editor's engine: one `scene`, one registry. A packaged editor ships them in
Expand All @@ -27,13 +27,24 @@ struct ComponentSDK: Equatable {
var target: String
var languageMode: String
var providedModules: [String]
/// The C modules shipped beside `Modules`, each a folder of headers with a
/// `module.modulemap`. An SDK packaged before the list existed has CShaderTypes alone.
var cModules: [String]?
}

static let bundleDirectoryName = "ComponentSDK"
static let manifestFileName = "sdk.json"
/// The engine's own C module: every SDK has it.
static let cShaderTypesModule = "CShaderTypes"

let modulesDirectory: URL
let cShaderTypesModuleMap: URL
/// The module map of every C module the modules in `modulesDirectory` import: CShaderTypes
/// first, then any other the engine was built with. The compiler does not find these by
/// itself, and cannot import `UntoldEngine` without them.
let cModuleMaps: [URL]
/// C targets the editor was built with whose module map was not found. Always empty for a
/// packaged editor, where the packaging check has compiled against the SDK.
let cModulesWithoutModuleMap: [String]
/// Modules the editor itself links, which a plugin must therefore never bring a second copy of.
let providedModules: [String]
let targetTriple: String
Expand Down Expand Up @@ -70,17 +81,19 @@ struct ComponentSDK: Equatable {

static func resolveBundled(at sdkRoot: URL, fileManager: FileManager = .default) -> ComponentSDK? {
let modules = sdkRoot.appendingPathComponent("Modules", isDirectory: true)
let moduleMap = sdkRoot.appendingPathComponent("CShaderTypes/module.modulemap")
let manifestURL = sdkRoot.appendingPathComponent(manifestFileName)
guard hasRequiredModules(in: modules, fileManager: fileManager),
fileManager.fileExists(atPath: moduleMap.path),
let data = try? Data(contentsOf: manifestURL),
let manifest = try? JSONDecoder().decode(Manifest.self, from: data)
else { return nil }
let moduleMaps = (manifest.cModules ?? [cShaderTypesModule])
.map { sdkRoot.appendingPathComponent("\($0)/module.modulemap") }
guard moduleMaps.allSatisfy({ fileManager.fileExists(atPath: $0.path) }) else { return nil }

return ComponentSDK(
modulesDirectory: modules,
cShaderTypesModuleMap: moduleMap,
cModuleMaps: moduleMaps,
cModulesWithoutModuleMap: [],
providedModules: manifest.providedModules,
targetTriple: manifest.target,
recordedCompilerVersion: manifest.swiftCompilerVersion,
Expand All @@ -95,18 +108,20 @@ struct ComponentSDK: Equatable {
/// the module map in `CShaderTypes.build/`.
static func resolveFromBuildProducts(productsDirectory: URL, fileManager: FileManager = .default) -> ComponentSDK? {
let moduleCandidates = [productsDirectory, productsDirectory.appendingPathComponent("Modules", isDirectory: true)]
let moduleMapCandidates = [
productsDirectory.appendingPathComponent("../../Intermediates.noindex/GeneratedModuleMaps/CShaderTypes.modulemap").standardizedFileURL,
productsDirectory.appendingPathComponent("CShaderTypes.build/module.modulemap"),
]
guard let modules = moduleCandidates.first(where: { hasRequiredModules(in: $0, fileManager: fileManager) }),
let moduleMap = moduleMapCandidates.first(where: { fileManager.fileExists(atPath: $0.path) })
let layout = ComponentSDKBuildLayout.allCases.first(where: {
let moduleMap = $0.generatedModuleMap(of: cShaderTypesModule, productsDirectory: productsDirectory)
return fileManager.fileExists(atPath: moduleMap.path)
})
else { return nil }
let swiftModules = moduleNames(in: modules, fileManager: fileManager)
let cModules = layout.cModules(productsDirectory: productsDirectory, swiftModules: swiftModules, fileManager: fileManager)

return ComponentSDK(
modulesDirectory: modules,
cShaderTypesModuleMap: moduleMap,
providedModules: moduleNames(in: modules, fileManager: fileManager),
cModuleMaps: cModules.moduleMaps,
cModulesWithoutModuleMap: cModules.withoutModuleMap,
providedModules: swiftModules,
targetTriple: defaultTargetTriple,
recordedCompilerVersion: nil,
engineURL: nil,
Expand Down
Loading