diff --git a/Cesium.Sdk.Tests/Cesium.Sdk.Tests.csproj b/Cesium.Sdk.Tests/Cesium.Sdk.Tests.csproj
index 6a912da0..67b1e517 100644
--- a/Cesium.Sdk.Tests/Cesium.Sdk.Tests.csproj
+++ b/Cesium.Sdk.Tests/Cesium.Sdk.Tests.csproj
@@ -18,6 +18,7 @@ SPDX-License-Identifier: MIT
+
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Cesium.Sdk.Tests/FileUtilTests.cs b/Cesium.Sdk.Tests/FileUtilTests.cs
new file mode 100644
index 00000000..ef6a5c98
--- /dev/null
+++ b/Cesium.Sdk.Tests/FileUtilTests.cs
@@ -0,0 +1,85 @@
+// SPDX-FileCopyrightText: 2025 Cesium contributors
+//
+// SPDX-License-Identifier: MIT
+
+using System.Runtime.InteropServices;
+using TruePath;
+using TruePath.SystemIo;
+
+namespace Cesium.Sdk.Tests;
+
+public class FileUtilTests
+{
+ private static void CreateUnixFile(AbsolutePath file, AbsolutePath? link = null, UnixFileMode? mode = null)
+ {
+ file.WriteAllText("empty");
+
+ if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) &&
+ !RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return;
+ if (mode is { } m)
+ File.SetUnixFileMode(file.Value, m);
+ if (link is { } linkFile)
+ {
+ linkFile.Delete();
+ File.CreateSymbolicLink(linkFile.Value, file.Value);
+ }
+ }
+
+ private AbsolutePath TestFile = Temporary.CreateTempFile();
+ private AbsolutePath TestLink = Temporary.CreateTempFile();
+
+ [Fact]
+ public void ExecutablePermissionsCheckOnUnix()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ Assert.True(true);
+ else
+ {
+ var file = TestFile;
+ CreateUnixFile(file, mode: UnixFileMode.UserExecute);
+
+ Assert.True(FileSystemUtil.IsUnixFileExecutable(file.Value));
+ }
+ }
+
+ [Fact]
+ public void ExecutableLinkPermissionsCheckOnUnix()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ Assert.True(true);
+ else
+ {
+ var file = TestFile;
+ var link = TestLink;
+ CreateUnixFile(file, link: link, mode: UnixFileMode.UserExecute);
+
+ Assert.True(FileSystemUtil.IsUnixFileExecutable(link.Value));
+ }
+ }
+
+ [Fact]
+ public void NoExecutablePermissionsCheckOnUnix()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ Assert.True(true);
+ else
+ {
+ var file = TestFile;
+ CreateUnixFile(file, mode: UnixFileMode.UserRead | UnixFileMode.UserWrite);
+
+ Assert.False(FileSystemUtil.IsUnixFileExecutable(file.Value));
+ }
+ }
+
+ [Fact]
+ public void InvalidForDirOnUnix()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ Assert.True(true);
+ else
+ {
+ var dir = "/etc";
+ Assert.False(FileSystemUtil.IsUnixFileExecutable(dir));
+ }
+ }
+}
diff --git a/Cesium.Sdk/Cesium.Sdk.csproj b/Cesium.Sdk/Cesium.Sdk.csproj
index 667f0885..073bb49a 100644
--- a/Cesium.Sdk/Cesium.Sdk.csproj
+++ b/Cesium.Sdk/Cesium.Sdk.csproj
@@ -18,6 +18,7 @@ SPDX-License-Identifier: MIT
+
diff --git a/Cesium.Sdk/CesiumCompile.cs b/Cesium.Sdk/CesiumCompile.cs
index 222f96fa..92d4f45e 100644
--- a/Cesium.Sdk/CesiumCompile.cs
+++ b/Cesium.Sdk/CesiumCompile.cs
@@ -285,11 +285,11 @@ private static bool ExecutableFileExists(string path)
var pathExtWithDot = new Lazy(() =>
Environment.GetEnvironmentVariable("PATHEXT")?.Split(Path.PathSeparator) ?? []);
- if (IsExecutable(path)) return true;
+ if (File.Exists(path) && IsExecutable(path)) return true;
foreach (var pathEntry in Environment.GetEnvironmentVariable("PATH")?.Split(Path.PathSeparator) ?? [])
{
- var fullPath = Path.Combine(pathEntry, pathEntry);
+ var fullPath = Path.Combine(pathEntry, path);
if (IsExecutable(fullPath)) return true;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@@ -305,14 +305,12 @@ private static bool ExecutableFileExists(string path)
bool IsExecutable(string exePath)
{
- if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
- {
- var extension = Path.GetExtension(exePath);
- return pathExtWithDot.Value.Contains(extension);
- }
-
- return true; // TODO[#840]: Proper executable check for Unix
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
+ || RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
+ return FileSystemUtil.IsUnixFileExecutable(exePath);
+ var extension = Path.GetExtension(exePath);
+ return pathExtWithDot.Value.Contains(extension);
}
}
diff --git a/Cesium.Sdk/FileSystemUtil.cs b/Cesium.Sdk/FileSystemUtil.cs
new file mode 100644
index 00000000..5e5948f9
--- /dev/null
+++ b/Cesium.Sdk/FileSystemUtil.cs
@@ -0,0 +1,22 @@
+// SPDX-FileCopyrightText: 2025 Cesium contributors
+//
+// SPDX-License-Identifier: MIT
+
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Cesium.Sdk;
+
+internal static class FileSystemUtil
+{
+ [DllImport("libc", EntryPoint = "access", SetLastError = true)]
+ private static extern int Access(string path, int mode);
+
+ private const int X_OK = 1;
+
+ public static bool IsUnixFileExecutable(string path)
+ {
+ if (Directory.Exists(path)) return false;
+ return Access(Path.GetFullPath(path), X_OK) == 0;
+ }
+}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4821b01b..71f78430 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -19,6 +19,7 @@ SPDX-License-Identifier: MIT
+