diff --git a/Resources/Info.plist b/Resources/Info.plist
index e01e96d..8bb61df 100644
--- a/Resources/Info.plist
+++ b/Resources/Info.plist
@@ -19,9 +19,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.8.0
+ 0.8.1
CFBundleVersion
- 8
+ 9
CFBundleDocumentTypes
diff --git a/Scripts/smoke_babeldoc_runtime.sh b/Scripts/smoke_babeldoc_runtime.sh
new file mode 100755
index 0000000..781e523
--- /dev/null
+++ b/Scripts/smoke_babeldoc_runtime.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$ROOT_DIR"
+
+APP_VERSION="$(
+ /usr/libexec/PlistBuddy \
+ -c 'Print :CFBundleShortVersionString' \
+ Resources/Info.plist
+)"
+
+GLOSS_RUN_BABELDOC_RUNTIME_SMOKE=1 \
+ GLOSS_RUNTIME_SMOKE_APP_VERSION="$APP_VERSION" \
+ swift test \
+ --filter BabelDOCRuntimeDistributionTests/publicRuntimeReleaseInstallsEndToEnd
diff --git a/Sources/GlossCore/BabelDOCRuntimeDistribution.swift b/Sources/GlossCore/BabelDOCRuntimeDistribution.swift
index a54f275..5c755cb 100644
--- a/Sources/GlossCore/BabelDOCRuntimeDistribution.swift
+++ b/Sources/GlossCore/BabelDOCRuntimeDistribution.swift
@@ -188,23 +188,33 @@ public struct BabelDOCRuntimeTransport: Sendable {
public static let live = ephemeral()
public static func ephemeral(
- policy: BabelDOCRuntimeNetworkPolicy = BabelDOCRuntimeNetworkPolicy()
+ policy: BabelDOCRuntimeNetworkPolicy
) -> Self {
- let configuration = URLSessionConfiguration.ephemeral
- configuration.timeoutIntervalForRequest = policy.requestTimeout
- configuration.timeoutIntervalForResource = policy.resourceTimeout
- configuration.waitsForConnectivity = policy.waitsForConnectivity
- configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
- let session = URLSession(configuration: configuration)
+ ephemeral(
+ metadataPolicy: policy,
+ downloadPolicy: policy
+ )
+ }
+
+ public static func ephemeral(
+ metadataPolicy: BabelDOCRuntimeNetworkPolicy = .metadata,
+ downloadPolicy: BabelDOCRuntimeNetworkPolicy = .runtimeArchive
+ ) -> Self {
+ let metadataSession = URLSession(
+ configuration: sessionConfiguration(for: metadataPolicy)
+ )
+ let downloadSession = URLSession(
+ configuration: sessionConfiguration(for: downloadPolicy)
+ )
return Self(
fetchData: { url in
- let (data, response) = try await session.data(from: url)
+ let (data, response) = try await metadataSession.data(from: url)
try validateHTTPResponse(response, for: url)
return data
},
download: { url, destination in
- let (temporaryURL, response) = try await session.download(from: url)
+ let (temporaryURL, response) = try await downloadSession.download(from: url)
try validateHTTPResponse(response, for: url)
let fileManager = FileManager.default
@@ -216,6 +226,17 @@ public struct BabelDOCRuntimeTransport: Sendable {
)
}
+ static func sessionConfiguration(
+ for policy: BabelDOCRuntimeNetworkPolicy
+ ) -> URLSessionConfiguration {
+ let configuration = URLSessionConfiguration.ephemeral
+ configuration.timeoutIntervalForRequest = policy.requestTimeout
+ configuration.timeoutIntervalForResource = policy.resourceTimeout
+ configuration.waitsForConnectivity = policy.waitsForConnectivity
+ configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
+ return configuration
+ }
+
private static func validateHTTPResponse(_ response: URLResponse, for url: URL) throws {
guard let response = response as? HTTPURLResponse else {
return
@@ -230,6 +251,12 @@ public struct BabelDOCRuntimeTransport: Sendable {
}
public struct BabelDOCRuntimeNetworkPolicy: Equatable, Sendable {
+ public static let metadata = Self()
+ public static let runtimeArchive = Self(
+ requestTimeout: 60,
+ resourceTimeout: 60 * 60
+ )
+
public let requestTimeout: TimeInterval
public let resourceTimeout: TimeInterval
public let waitsForConnectivity: Bool
diff --git a/Tests/GlossCoreTests/BabelDOCRuntimeDistributionTests.swift b/Tests/GlossCoreTests/BabelDOCRuntimeDistributionTests.swift
index d1d5c88..95c4a1b 100644
--- a/Tests/GlossCoreTests/BabelDOCRuntimeDistributionTests.swift
+++ b/Tests/GlossCoreTests/BabelDOCRuntimeDistributionTests.swift
@@ -68,13 +68,71 @@ struct BabelDOCRuntimeDistributionTests {
)
}
- @Test("live transport uses bounded offline timeouts")
- func liveNetworkPolicyIsBounded() {
- let policy = BabelDOCRuntimeNetworkPolicy()
+ @Test("live transport keeps metadata fast and allows large archive downloads")
+ func liveNetworkPoliciesMatchPayloadSize() {
+ let metadataPolicy = BabelDOCRuntimeNetworkPolicy.metadata
+ let archivePolicy = BabelDOCRuntimeNetworkPolicy.runtimeArchive
+ let metadataConfiguration = BabelDOCRuntimeTransport.sessionConfiguration(
+ for: metadataPolicy
+ )
+ let archiveConfiguration = BabelDOCRuntimeTransport.sessionConfiguration(
+ for: archivePolicy
+ )
+
+ #expect(metadataConfiguration.timeoutIntervalForRequest == 12)
+ #expect(metadataConfiguration.timeoutIntervalForResource == 60)
+ #expect(!metadataConfiguration.waitsForConnectivity)
+ #expect(archiveConfiguration.timeoutIntervalForRequest == 60)
+ #expect(archiveConfiguration.timeoutIntervalForResource == 60 * 60)
+ #expect(!archiveConfiguration.waitsForConnectivity)
+ }
+
+ @Test(
+ "public runtime release installs and restores end to end",
+ .enabled(
+ if: ProcessInfo.processInfo.environment[
+ "GLOSS_RUN_BABELDOC_RUNTIME_SMOKE"
+ ] == "1",
+ "Set GLOSS_RUN_BABELDOC_RUNTIME_SMOKE=1 to download the public runtime."
+ )
+ )
+ func publicRuntimeReleaseInstallsEndToEnd() async throws {
+ let currentGlossVersion = try #require(
+ ProcessInfo.processInfo.environment[
+ "GLOSS_RUNTIME_SMOKE_APP_VERSION"
+ ]
+ )
- #expect(policy.requestTimeout == 12)
- #expect(policy.resourceTimeout == 60)
- #expect(!policy.waitsForConnectivity)
+ let root = try temporaryDirectory()
+ defer {
+ try? FileManager.default.removeItem(at: root)
+ }
+ let manager = try BabelDOCRuntimeManager(
+ rootDirectory: root,
+ currentGlossVersion: currentGlossVersion
+ )
+
+ let installed = try await manager.update { progress in
+ print(
+ "BabelDOC runtime smoke: \(progress.operation.rawValue)"
+ + (progress.version.map { " \($0)" } ?? "")
+ )
+ }
+ let installedVersion = try #require(installed.currentVersion)
+ let executableURL = try #require(installed.currentExecutableURL)
+ #expect(installed.operation == .ready)
+ #expect(installed.lastError == nil)
+ #expect(FileManager.default.isExecutableFile(atPath: executableURL.path))
+
+ let restoredManager = try BabelDOCRuntimeManager(
+ rootDirectory: root,
+ currentGlossVersion: currentGlossVersion
+ )
+ let restored = await restoredManager.snapshot()
+ #expect(restored.currentVersion == installedVersion)
+ #expect(restored.currentExecutableURL == executableURL)
+ #expect(restored.operation == .idle)
+ #expect(restored.lastError == nil)
}
@Test("raw runtime install verifies SHA, permissions, and persisted state")
diff --git a/docs/release-notes/v0.8.1.md b/docs/release-notes/v0.8.1.md
new file mode 100644
index 0000000..198099c
--- /dev/null
+++ b/docs/release-notes/v0.8.1.md
@@ -0,0 +1,27 @@
+# Gloss 0.8.1
+
+Gloss 0.8.1 fixes first-time installation of the managed BabelDOC runtime.
+
+## PDF runtime
+
+- Keeps signed manifest and signature requests on a short, bounded network
+ timeout.
+- Gives the 200+ MiB runtime archive its own download session with a one-hour
+ total timeout and a 60-second inactivity timeout, so slow connections can
+ finish without hiding a stalled transfer.
+- Preserves the existing single-policy transport API for callers that need the
+ same timeout behavior for metadata and downloads.
+
+## Validation
+
+- Adds an opt-in command-line smoke test that downloads the public runtime,
+ verifies its signed manifest, archive size and SHA-256, extracts it, checks
+ the executable, and restores the persisted installation state.
+- The arm64 public runtime completed this path on a real slow connection in
+ 1,766.952 seconds; the previous release stopped after 60 seconds.
+
+## Compatibility
+
+This release uses the same signed BabelDOC `0.6.4+gloss.4` runtime and remains
+compatible with macOS 14 or newer. Existing verified runtime installations do
+not need to be downloaded again.