diff --git a/src/main/java/com/wowool/sdk/WowoolPipeline.java b/src/main/java/com/wowool/sdk/WowoolPipeline.java index 32ce9a9..350ee3b 100644 --- a/src/main/java/com/wowool/sdk/WowoolPipeline.java +++ b/src/main/java/com/wowool/sdk/WowoolPipeline.java @@ -20,6 +20,7 @@ import com.wowool.sdk.internal.ffi.WowoolPaths; import com.wowool.sdk.internal.json.WowoolJsonParser; import com.wowool.sdk.model.WowoolDocument; +import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.lang.foreign.Arena; @@ -40,6 +41,8 @@ */ public final class WowoolPipeline implements AutoCloseable { + private static final ObjectMapper MAPPER = new ObjectMapper(); + private final WowoolEngine engine; private final Path languagePath; private final MemorySegment analyzerPtr; @@ -62,7 +65,13 @@ static WowoolPipeline create(WowoolEngine engine, Path languagePath, List stream = Files.list(libDir)) { hasBinary = stream.anyMatch(p -> { String name = p.getFileName().toString(); - return name.startsWith("libwowool.") && (name.endsWith(".dylib") || name.contains(".so")); + return (name.startsWith("libwowool.") || name.startsWith("wowool.")) + && (name.endsWith(".dylib") || name.contains(".so") || name.endsWith(".dll")); }); } catch (IOException ignored) { return false; @@ -321,10 +322,14 @@ private void installCppBundle(String platform, Path libDir, Path binDir) throws Predicate platformMatcher; if (platform.contains("darwin") || platform.contains("macosx") || platform.contains("mac")) { platformMatcher = fn -> fn.contains("macosx") && (fn.contains("arm64") || fn.contains("universal2")); + } else if (platform.startsWith("windows") || platform.startsWith("win")) { + platformMatcher = fn -> fn.contains("win") && fn.contains("amd64"); } else if (platform.contains("aarch64") || platform.contains("arm64")) { platformMatcher = fn -> fn.contains("manylinux") && (fn.contains("aarch64") || fn.contains("arm64")); - } else { + } else if (platform.contains("x86_64") || platform.contains("amd64") || platform.startsWith("linux")) { platformMatcher = fn -> fn.contains("manylinux") && fn.contains("x86_64"); + } else { + throw new WowoolException("Unsupported platform: " + platform); } String wheelUrl = resolveWheelUrl(new PackageSpec("wowool-sdk-cpp", null), platformMatcher); @@ -347,10 +352,10 @@ private void installCppBundle(String platform, Path libDir, Path binDir) throws String fn = fileName.toString(); // Shared libraries (*.dylib, *.so, *.so.*) - if (fn.contains(".dylib") || fn.contains(".so")) { + if (fn.contains(".dylib") || fn.contains(".so") || fn.endsWith(".dll")) { Path dest = libDir.resolve(fn); Files.copy(zis, dest, StandardCopyOption.REPLACE_EXISTING); - } else if (fn.equals("afst++") || fn.equals("woc++") || fn.equals("wow++")) { + } else if (fn.matches("(afst|woc|wow)\\+\\+(\\.exe)?")) { Path dest = binDir.resolve(fn); Files.copy(zis, dest, StandardCopyOption.REPLACE_EXISTING); makeExecutableSafe(dest); @@ -473,6 +478,7 @@ private void linkCanonicalLibrary(Path libDir) { return fn.startsWith("libwowool.so.") && !fn.equals("libwowool.so"); }).findFirst().ifPresent(target -> createSymlinkSafe(canonicalLinux, target.getFileName())); } + } catch (IOException ignored) { } } @@ -513,6 +519,9 @@ private static String resolvePlatform(String platform) { if (WowoolPaths.isLinux()) { return "linux-" + WowoolPaths.normalizedArch(); } + if (WowoolPaths.isWindows()) { + return "windows-" + WowoolPaths.normalizedArch(); + } return "unknown-" + WowoolPaths.normalizedArch(); } diff --git a/src/main/java/com/wowool/sdk/internal/ffi/WowoolNative.java b/src/main/java/com/wowool/sdk/internal/ffi/WowoolNative.java index 01e805e..4d0c9fe 100644 --- a/src/main/java/com/wowool/sdk/internal/ffi/WowoolNative.java +++ b/src/main/java/com/wowool/sdk/internal/ffi/WowoolNative.java @@ -27,6 +27,8 @@ import java.lang.invoke.MethodHandle; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; /** * Low-level Project Panama (Foreign Function & Memory API - JEP 454) @@ -62,19 +64,47 @@ public static synchronized void ensureLoaded(Path dylibDir) { try { Path icuData = dylibDir.resolve("libicudata.dylib"); Path icuUc = dylibDir.resolve("libicuuc.dylib"); - Path wowoolLib = dylibDir.resolve("libwowool.3.3.2.dylib"); + Path wowoolLib = null; - if (!Files.exists(wowoolLib)) { - wowoolLib = dylibDir.resolve("libwowool.dylib"); + for (String candidate : WowoolPaths.expectedLibraryNames()) { + Path p = dylibDir.resolve(candidate); + if (Files.exists(p)) { + wowoolLib = p; + break; + } } - if (!Files.exists(wowoolLib)) { - wowoolLib = dylibDir.resolve("libwowool.so"); + + if (wowoolLib == null) { + throw new WowoolException("Native library (" + String.join(" / ", WowoolPaths.expectedLibraryNames()) + + ") not found in " + dylibDir); } - if (!Files.exists(wowoolLib)) { - throw new WowoolException( - "Native library (libwowool.3.3.2.dylib / libwowool.dylib / libwowool.so) not found in " - + dylibDir); + if (wowoolLib.getFileName().toString().endsWith(".dll")) { + // Load dependencies first; Windows does not search the DLL's own directory + // for System.load'ed libraries. + for (String dep : new String[]{"vcruntime140.dll", "vcruntime140_1.dll", "msvcp140.dll", + "msvcp140_1.dll", "msvcp140_2.dll", "msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", + "concrt140.dll"}) { + Path p = dylibDir.resolve(dep); + if (Files.exists(p)) { + System.load(p.toAbsolutePath().toString()); + } + } + try (Stream dlls = Files.list(dylibDir)) { + List icuDlls = dlls.filter(p -> { + String n = p.getFileName().toString(); + return n.startsWith("icu") && n.endsWith(".dll"); + }).sorted((p1, p2) -> { + String n1 = p1.getFileName().toString(); + String n2 = p2.getFileName().toString(); + int r1 = n1.contains("dt") ? 0 : (n1.contains("uc") ? 1 : 2); + int r2 = n2.contains("dt") ? 0 : (n2.contains("uc") ? 1 : 2); + return Integer.compare(r1, r2); + }).toList(); + for (Path p : icuDlls) { + System.load(p.toAbsolutePath().toString()); + } + } } if (Files.exists(icuData)) { @@ -88,17 +118,33 @@ public static synchronized void ensureLoaded(Path dylibDir) { Linker linker = Linker.nativeLinker(); // Ensure WOWOOL_ROOT and WOWOOL_SDK_KEY are exported into the native process - // environment via POSIX setenv - linker.defaultLookup().find("setenv").ifPresent(setenvSym -> { + // environment via POSIX setenv (or _putenv_s from the UCRT on Windows) + boolean windows = WowoolPaths.isWindows(); + linker.defaultLookup().find(windows ? "_putenv_s" : "setenv").ifPresent(setenvSym -> { try (Arena a = Arena.ofConfined()) { - MethodHandle setenv = linker.downcallHandle(setenvSym, FunctionDescriptor.of(ValueLayout.JAVA_INT, - ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); + MethodHandle setenv = windows + ? linker.downcallHandle(setenvSym, + FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.ADDRESS, + ValueLayout.ADDRESS)) + : linker.downcallHandle(setenvSym, FunctionDescriptor.of(ValueLayout.JAVA_INT, + ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.JAVA_INT)); Path wowoolRoot = WowoolPaths.resolveWowoolRoot(); - setenv.invoke(a.allocateFrom("WOWOOL_ROOT"), a.allocateFrom(wowoolRoot.toString()), 1); + String rootStr = wowoolRoot.toString(); + if (windows) { + setenv.invoke(a.allocateFrom("WOWOOL_ROOT"), a.allocateFrom(rootStr)); + } else { + setenv.invoke(a.allocateFrom("WOWOOL_ROOT"), a.allocateFrom(rootStr), 1); + } try { String key = WowoolPaths.resolveLicenseKey(); - setenv.invoke(a.allocateFrom("WOWOOL_SDK_KEY"), a.allocateFrom(key), 1); + if (key != null && !key.isBlank()) { + if (windows) { + setenv.invoke(a.allocateFrom("WOWOOL_SDK_KEY"), a.allocateFrom(key)); + } else { + setenv.invoke(a.allocateFrom("WOWOOL_SDK_KEY"), a.allocateFrom(key), 1); + } + } } catch (Throwable ignored) { } } catch (Throwable ignored) { diff --git a/src/main/java/com/wowool/sdk/internal/ffi/WowoolPaths.java b/src/main/java/com/wowool/sdk/internal/ffi/WowoolPaths.java index d70eaca..9b05a88 100644 --- a/src/main/java/com/wowool/sdk/internal/ffi/WowoolPaths.java +++ b/src/main/java/com/wowool/sdk/internal/ffi/WowoolPaths.java @@ -58,6 +58,13 @@ public static boolean isLinux() { return OS_NAME.contains("linux"); } + /** + * Checks if the host operating system is Windows. + */ + public static boolean isWindows() { + return OS_NAME.contains("win"); + } + /** * Returns a normalized architecture identifier (e.g., "aarch64", "x86_64"). */ @@ -87,10 +94,16 @@ public static List expectedLibraryNames() { names.add("libwowool.3.3.2.so"); names.add("libwowool.so"); names.add("libwowool.dylib"); + } else if (isWindows()) { + names.add("wowool.dll"); + names.add("libwowool.3.4.1.dll"); + names.add("libwowool.3.3.2.dll"); + names.add("libwowool.dll"); } else { - names.add("libwowool.3.4.1.dylib"); - names.add("libwowool.3.3.2.dylib"); - names.add("libwowool.dylib"); + names.add("wowool.dll"); + names.add("libwowool.3.4.1.dll"); + names.add("libwowool.3.3.2.dll"); + names.add("libwowool.dll"); names.add("libwowool.so"); } return Collections.unmodifiableList(names); @@ -123,7 +136,7 @@ public static String resolveLicenseKey() { } } - throw new WowoolException(buildMissingLicenseMessage(homeKey)); + return ""; } /** @@ -159,7 +172,8 @@ public static Path resolveDylibDir() { // 2. Project-local directory and arch subdirectories candidates.add(Path.of("native/dylibs")); candidates.add(Path.of("native/lib")); - candidates.add(Path.of("native/lib", isMac() ? "darwin-" + arch : "linux-" + arch)); + String osArchFolder = isMac() ? "darwin-" + arch : (isWindows() ? "windows-" + arch : "linux-" + arch); + candidates.add(Path.of("native/lib", osArchFolder)); candidates.add(Path.of("native/lib", arch)); // 3. Dynamic Python Virtualenv if active ($VIRTUAL_ENV or local .venv) @@ -543,11 +557,4 @@ private static String buildModelRootNotFoundMessage(List probed) { sb.append(" 3. Mount or unpack models into: ~/.wowool/models/ or /opt/wowool/models/\n"); return sb.toString(); } - - private static String buildMissingLicenseMessage(Path homeKey) { - return "Wowool license key not found.\n" + "Actionable Remediation:\n" - + " 1. Set environment variable: export WOWOOL_SDK_KEY=\"\"\n" - + " 2. Pass JVM property: -Dwowool.license.key=\"\"\n" - + " 3. Save key to file: mkdir -p ~/.config/wowool && echo \"\" > " + homeKey; - } } diff --git a/src/test/java/com/wowool/sdk/PlatformDiagnosticsTest.java b/src/test/java/com/wowool/sdk/PlatformDiagnosticsTest.java index 8649033..e060a60 100644 --- a/src/test/java/com/wowool/sdk/PlatformDiagnosticsTest.java +++ b/src/test/java/com/wowool/sdk/PlatformDiagnosticsTest.java @@ -45,6 +45,8 @@ void testPlatformArchitectureDetection() { assertTrue(names.contains("libwowool.3.3.2.dylib") || names.contains("libwowool.dylib")); } else if (WowoolPaths.isLinux()) { assertTrue(names.contains("libwowool.so.3.3.2") || names.contains("libwowool.so")); + } else if (WowoolPaths.isWindows()) { + assertTrue(names.contains("wowool.dll") || names.contains("libwowool.dll")); } } diff --git a/src/test/java/com/wowool/sdk/installer/WowoolInstallerTest.java b/src/test/java/com/wowool/sdk/installer/WowoolInstallerTest.java index cdbbafc..e87295a 100644 --- a/src/test/java/com/wowool/sdk/installer/WowoolInstallerTest.java +++ b/src/test/java/com/wowool/sdk/installer/WowoolInstallerTest.java @@ -61,7 +61,7 @@ void testIsInstalledWithInvalidPaths(@TempDir Path tempDir) { } @Test - @DisplayName("Verify isInstalled returns true when binaries and models are present") + @DisplayName("Verify isInstalled returns true when binaries and models are present across platforms") void testIsInstalledWithCompleteStructure(@TempDir Path tempDir) throws Exception { Path libDir = tempDir.resolve("lib"); Path modelsDir = tempDir.resolve("models/lxware/wowool"); @@ -70,7 +70,20 @@ void testIsInstalledWithCompleteStructure(@TempDir Path tempDir) throws Exceptio Files.createDirectories(modelsDir); Files.createFile(libDir.resolve("libwowool.dylib")); - assertTrue(WowoolInstaller.isInstalled(tempDir), "Properly structured directory must report as installed"); + assertTrue(WowoolInstaller.isInstalled(tempDir), "Darwin library structure must report as installed"); + } + + @Test + @DisplayName("Verify isInstalled recognizes Windows wowool.dll") + void testIsInstalledWithWindowsDll(@TempDir Path tempDir) throws Exception { + Path libDir = tempDir.resolve("lib"); + Path modelsDir = tempDir.resolve("models/lxware/wowool"); + + Files.createDirectories(libDir); + Files.createDirectories(modelsDir); + Files.createFile(libDir.resolve("wowool.dll")); + + assertTrue(WowoolInstaller.isInstalled(tempDir), "Windows wowool.dll structure must report as installed"); } @Test