Skip to content

Commit b18bbf5

Browse files
authored
Merge pull request #24 from kdroidFilter/fix/sandboxed-dylibs-to-frameworks
fix(packaging): move sandboxed dylibs to Contents/Frameworks/
2 parents da619a6 + 5ed3f23 commit b18bbf5

4 files changed

Lines changed: 69 additions & 22 deletions

File tree

docs/sandboxing.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ When at least one store format is configured, Nucleus registers additional Gradl
3434

3535
### 1. Extract Native Libraries from JARs
3636

37-
Sandboxed apps (especially macOS App Sandbox) cannot load unsigned native code extracted to temp directories at runtime. The plugin scans all dependency JARs for `.dylib`, `.jnilib`, `.so`, and `.dll` files and extracts them to the app's `resources/` directory.
37+
Sandboxed apps (especially macOS App Sandbox) cannot load unsigned native code extracted to temp directories at runtime. The plugin scans all dependency JARs for `.dylib`, `.jnilib`, `.so`, and `.dll` files and extracts them. On macOS, these are placed in the app's `Contents/Frameworks/` directory (Apple convention); on other platforms they go into the app's `resources/` directory.
3838

3939
**Task:** `extractNativeLibsForSandboxing`
4040

@@ -52,8 +52,18 @@ A separate `Sync` task merges the user's `appResources` with the extracted nativ
5252

5353
### 4. Inject Sandboxing JVM Arguments
5454

55-
The sandboxed app-image is configured with JVM arguments that redirect native library loading to the pre-extracted resources directory:
55+
The sandboxed app-image is configured with JVM arguments that redirect native library loading to the pre-extracted location. On macOS, the path points to `Contents/Frameworks/` (`$APPDIR/../Frameworks`); on other platforms it points to `$APPDIR/resources`:
5656

57+
**macOS:**
58+
```
59+
-Djava.library.path=$APPDIR/../Frameworks
60+
-Djna.nounpack=true
61+
-Djna.nosys=true
62+
-Djna.boot.library.path=$APPDIR/../Frameworks
63+
-Djna.library.path=$APPDIR/../Frameworks
64+
```
65+
66+
**Windows / Linux:**
5767
```
5868
-Djava.library.path=$APPDIR/resources
5969
-Djna.nounpack=true
@@ -64,13 +74,13 @@ The sandboxed app-image is configured with JVM arguments that redirect native li
6474

6575
This ensures JNA/JNI libraries are loaded from signed, pre-extracted locations instead of being dynamically extracted to temp at runtime.
6676

67-
### 5. Sign Native Libraries in Resources (macOS)
77+
### 5. Sign Native Libraries (macOS)
6878

69-
On macOS, all `.dylib` files in the sandboxed app's `resources/` directory are individually code-signed so they pass Gatekeeper checks.
79+
On macOS, all `.dylib` files in the sandboxed app's `Contents/Frameworks/` directory are individually code-signed so they pass Gatekeeper checks.
7080

7181
### 6. Handle Skiko and icudtl.dat
7282

73-
The Skiko library path is adjusted to point to `resources/` instead of the app root. The companion `icudtl.dat` file is copied alongside the Skiko native library.
83+
The Skiko library path is adjusted to point to `Contents/Frameworks/` (macOS) or `resources/` (other platforms) instead of the app root. The companion `icudtl.dat` file is placed alongside the Skiko native library.
7484

7585
## macOS App Sandbox
7686

plugin-build/plugin/src/main/kotlin/io/github/kdroidfilter/nucleus/desktop/application/internal/configureJvmApplication.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -586,7 +586,8 @@ private fun JvmApplicationContext.configurePackageTask(
586586
args = args + "-splash:\$APPDIR/resources/$splash"
587587
}
588588
if (sandboxed) {
589-
args = args + sandboxingJvmArgs("\$APPDIR/resources")
589+
val nativeLibPath = if (currentOS == OS.MacOS) "\$APPDIR/../Frameworks" else "\$APPDIR/resources"
590+
args = args + sandboxingJvmArgs(nativeLibPath)
590591
}
591592
args
592593
},

plugin-build/plugin/src/main/kotlin/io/github/kdroidfilter/nucleus/desktop/application/tasks/AbstractElectronBuilderPackageTask.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -523,10 +523,10 @@ abstract class AbstractElectronBuilderPackageTask
523523
signer.sign(runtimeDir, runtimeEntitlements, forceEntitlements = true)
524524
}
525525

526-
// Re-sign native libs in resources directory (PKG is always sandboxed)
527-
val resourcesDir = appDir.resolve("Contents/app/resources")
528-
if (resourcesDir.exists()) {
529-
resourcesDir.walk().forEach { file ->
526+
// Re-sign native libs in Frameworks directory (PKG is always sandboxed)
527+
val frameworksDir = appDir.resolve("Contents/Frameworks")
528+
if (frameworksDir.exists()) {
529+
frameworksDir.walk().forEach { file ->
530530
val path = file.toPath()
531531
if (path.isRegularFile(LinkOption.NOFOLLOW_LINKS) && file.name.isDylibPath) {
532532
signer.sign(file, appEntitlements)

plugin-build/plugin/src/main/kotlin/io/github/kdroidfilter/nucleus/desktop/application/tasks/AbstractJPackageTask.kt

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import java.io.File
7575
import java.io.ObjectInputStream
7676
import java.io.ObjectOutputStream
7777
import java.io.Serializable
78+
import java.nio.file.Files
7879
import java.nio.file.LinkOption
7980
import java.util.ArrayList
8081
import java.util.Calendar
@@ -373,7 +374,12 @@ abstract class AbstractJPackageTask
373374
launcherJvmArgs.orNull?.forEach {
374375
javaOption(it)
375376
}
376-
val skikoPath = if (sandboxingEnabled.get()) appDir("resources") else appDir()
377+
val skikoPath =
378+
when {
379+
sandboxingEnabled.get() && currentOS == OS.MacOS -> appDir("..", "Frameworks")
380+
sandboxingEnabled.get() -> appDir("resources")
381+
else -> appDir()
382+
}
377383
javaOption("-D$SKIKO_LIBRARY_PATH=$skikoPath")
378384
if (currentOS == OS.MacOS) {
379385
macDockName.orNull?.let { dockName ->
@@ -588,18 +594,8 @@ abstract class AbstractJPackageTask
588594
}
589595
}
590596

591-
// When sandboxing is enabled, also sign native libs in the resources directory
592-
// so they pass Gatekeeper checks in sandboxed App Store apps.
593597
if (sandboxingEnabled.get()) {
594-
val resourcesDir = appDir.resolve("Contents/app/resources")
595-
if (resourcesDir.exists()) {
596-
resourcesDir.walk().forEach { file ->
597-
val path = file.toPath()
598-
if (path.isRegularFile(LinkOption.NOFOLLOW_LINKS) && file.name.isDylibPath) {
599-
macSigner.sign(file, appEntitlementsFile)
600-
}
601-
}
602-
}
598+
moveNativeLibsToFrameworks(appDir, macSigner, appEntitlementsFile)
603599
}
604600

605601
macSigner.sign(runtimeDir, runtimeEntitlementsFile, forceEntitlements = true)
@@ -615,6 +611,46 @@ abstract class AbstractJPackageTask
615611
}
616612
}
617613

614+
/**
615+
* Moves native libraries from `Contents/app/resources/` to `Contents/Frameworks/`
616+
* (Apple convention for sandboxed apps) and signs them.
617+
*/
618+
private fun moveNativeLibsToFrameworks(
619+
appDir: File,
620+
macSigner: MacSigner,
621+
entitlementsFile: File?,
622+
) {
623+
val resourcesDir = appDir.resolve("Contents/app/resources")
624+
val frameworksDir = appDir.resolve("Contents/Frameworks")
625+
if (resourcesDir.exists()) {
626+
frameworksDir.mkdirs()
627+
resourcesDir.walk().forEach { file ->
628+
if (file == resourcesDir) return@forEach
629+
if (file.isDirectory) return@forEach
630+
if (file.name.isDylibPath || file.name == "icudtl.dat") {
631+
val target = frameworksDir.resolve(file.relativeTo(resourcesDir).path)
632+
target.parentFile.mkdirs()
633+
Files.move(file.toPath(), target.toPath())
634+
}
635+
}
636+
// Clean up empty directories left in resources/
637+
resourcesDir
638+
.walk()
639+
.sortedDescending()
640+
.filter { it.isDirectory && it != resourcesDir && it.listFiles()?.isEmpty() == true }
641+
.forEach { it.delete() }
642+
}
643+
// Sign native libs in Frameworks/
644+
if (frameworksDir.exists()) {
645+
frameworksDir.walk().forEach { file ->
646+
val path = file.toPath()
647+
if (path.isRegularFile(LinkOption.NOFOLLOW_LINKS) && file.name.isDylibPath) {
648+
macSigner.sign(file, entitlementsFile)
649+
}
650+
}
651+
}
652+
}
653+
618654
override fun initState() {
619655
jvmRuntimeInfo = JvmRuntimeProperties.readFromFile(javaRuntimePropertiesFile.ioFile)
620656

0 commit comments

Comments
 (0)