From b402ccf3aa9f12f157d5119a4815e3aa6643eb1a Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Sun, 21 Dec 2025 17:41:51 +0800 Subject: [PATCH 1/8] gitIgnore: exclude scratch folders. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 6fee6de41..d8ca9fb6a 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,9 @@ config.h /CMakeFiles/ /cmake_install.cmake +# Scratch folders +.scratch/ + # test suite test/genkeystroke test/performance From 225ebbedbdc3d3d9850cc0b233ef3f78cdfacc87 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Sun, 21 Dec 2025 17:37:41 +0800 Subject: [PATCH 2/8] gitignore: add new entries for macOS and Swift Package Manager. --- .gitignore | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d8ca9fb6a..fa7318179 100644 --- a/.gitignore +++ b/.gitignore @@ -135,6 +135,7 @@ contrib/test.sqlite3 # OS X .DS_Store +.Spotlight-V100 # python *.pyc @@ -147,4 +148,10 @@ contrib/test.sqlite3 # fuzzer /fuzzer/in -/fuzzer/out \ No newline at end of file +/fuzzer/out + +# Swift +./[Bb]uild/ +.build/ +.swiftpm/ +index-build/ From 79dd240544e635d13ef9574c57498bdb6c2a8397 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Sun, 21 Dec 2025 17:45:19 +0800 Subject: [PATCH 3/8] vscode: add excluded folders. --- .vscode/settings.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..b16c80360 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,19 @@ +{ + "makefile.extensionOutputFolder": "./.vscode", + "makefile.configureOnOpen": false, + + "files.exclude": { + "./Build": true, + ".build": true, + ".swiftpm": true, + "**/.build": true, + "**/.swiftpm": true + }, + "search.exclude": { + "./Build": true, + ".build": true, + ".swiftpm": true, + "**/.build": true, + "**/.swiftpm": true + } +} \ No newline at end of file From ee92e338f2812b0a7fd275aeaf869dcf31efd172 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Sun, 21 Dec 2025 20:32:43 +0800 Subject: [PATCH 4/8] spm: initial implementation with some tests. --- Package.swift | 45 ++ scripts/build-cargo.sh | 16 + tests_swift/ChewingTests_Suite1.swift | 40 ++ tests_swift/ChewingTests_Suite2.swift | 575 ++++++++++++++++++++++++++ tests_swift/TestUtilAPIs.swift | 194 +++++++++ tools/CargoBuildPlugin/Plugin.swift | 69 ++++ 6 files changed, 939 insertions(+) create mode 100644 Package.swift create mode 100644 scripts/build-cargo.sh create mode 100644 tests_swift/ChewingTests_Suite1.swift create mode 100644 tests_swift/ChewingTests_Suite2.swift create mode 100644 tests_swift/TestUtilAPIs.swift create mode 100644 tools/CargoBuildPlugin/Plugin.swift diff --git a/Package.swift b/Package.swift new file mode 100644 index 000000000..28fd22f60 --- /dev/null +++ b/Package.swift @@ -0,0 +1,45 @@ +// swift-tools-version:6.1 +import PackageDescription + +let package = Package( + name: "libchewing", + products: [ + .library( + name: "Chewing", + targets: ["CChewing"] + ) + ], + targets: [ + // Expose the existing C public headers in `capi/include` as a Clang module + // Attach the `CargoBuild` plugin which runs `cargo build` before linking. + .target( + name: "CChewing", + path: "capi", + publicHeadersPath: "include", + // Instruct the linker to search common Cargo target dirs where the built library may be placed + linkerSettings: [ + .unsafeFlags(["-L", "./target/cargo-target/release"]), + .unsafeFlags([ + "-L", + ".build/plugins/outputs/libchewing-spm/CChewing/destination/CargoBuild/cargo-target/release", + ]), + .linkedLibrary("chewing_capi"), + ], + plugins: [ + .plugin(name: "CargoBuild") + ] + ), + // Build-tool plugin that invokes cargo to produce the static library + .plugin( + name: "CargoBuild", + capability: .buildTool(), + path: "tools/CargoBuildPlugin" + ), + // Swift test target in `tests_swift` to validate C API accessibility from Swift + .testTarget( + name: "ChewingTests", + dependencies: ["CChewing"], + path: "tests_swift" + ), + ] +) diff --git a/scripts/build-cargo.sh b/scripts/build-cargo.sh new file mode 100644 index 000000000..a11393cb0 --- /dev/null +++ b/scripts/build-cargo.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build the chewing_capi Rust crate for use with Swift Package +# Outputs static library and artifacts into `target/cargo-target/release`. + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +CARGO_MANIFEST_PATH="$REPO_ROOT/capi/Cargo.toml" +TARGET_DIR="$REPO_ROOT/target/cargo-target" + +echo "Building chewing_capi (release) -> $TARGET_DIR" +mkdir -p "$TARGET_DIR" + +cargo build --release --manifest-path "$CARGO_MANIFEST_PATH" --target-dir "$TARGET_DIR" + +echo "Done. Built artifacts are in $TARGET_DIR/release" \ No newline at end of file diff --git a/tests_swift/ChewingTests_Suite1.swift b/tests_swift/ChewingTests_Suite1.swift new file mode 100644 index 000000000..738d5ee9c --- /dev/null +++ b/tests_swift/ChewingTests_Suite1.swift @@ -0,0 +1,40 @@ +import Foundation +import Testing + +@testable import CChewing + +@MainActor +struct ChewingTestsSuite1 { + @Test func testCreateDelete() { + let ctx = chewing_new() + #expect(ctx != nil, "chewing_new() should return non-null context") + if ctx != nil { + chewing_delete(ctx) + } + } + + @Test func testDefaultDictionaryNames() { + let names = chewing_get_defaultDictionaryNames() + #expect(names != nil, "chewing_get_defaultDictionaryNames() should return non-null") + if let names = names { + let s = String(cString: names) + #expect(!s.isEmpty, "Default dictionary names should not be empty") + } + } + + @Test func testSetGetKBType() { + let ctx = chewing_new() + guard let ctx else { + #expect(ctx != nil, "chewing_new() should return non-null context") + return + } + defer { chewing_delete(ctx) } + + // KB_HSU == 1 according to C header enum + let rc = chewing_set_KBType(ctx, 1) + #expect(rc == 0, "chewing_set_KBType should return 0 on success") + + let kb = chewing_get_KBType(ctx) + #expect(kb == 1, "chewing_get_KBType should reflect set value") + } +} diff --git a/tests_swift/ChewingTests_Suite2.swift b/tests_swift/ChewingTests_Suite2.swift new file mode 100644 index 000000000..b109bd40d --- /dev/null +++ b/tests_swift/ChewingTests_Suite2.swift @@ -0,0 +1,575 @@ +import Foundation +import Testing + +@testable import CChewing + +@MainActor +final class ChewingTestsSuite2: DataBackedTestSuite { + @Test func testSequenceCommit() { + // Reproduce the "綠茶" sequence from contrib/simple-select.c + let ctx = chewing_new() + guard let ctx else { + #expect(Bool(Bool(false)), "failed to create context") + return + } + defer { chewing_delete(ctx) } + + // Prepare environment similar to the example + let initialSelKeysStr = "123456789" + let selKeys: [Int32] = initialSelKeysStr.utf8.map { Int32($0) } + [0] + selKeys.withUnsafeBufferPointer { ptr in + chewing_set_selKey(ctx, ptr.baseAddress, 9) + } + chewing_set_candPerPage(ctx, 9) + chewing_set_maxChiSymbolLen(ctx, 16) + + // Input sequence for "綠茶": 'x','m','4','t','8','6' + let inputSeqStr = "xm4t86" + let keys: [UInt8] = inputSeqStr.utf8.map { UInt8($0) } + for k in keys { + let rc = chewing_handle_Default(ctx, Int32(k)) + #expect(rc == 0, "chewing_handle_Default failed for \(Character(UnicodeScalar(k)))") + } + + // Commit + _ = chewing_handle_Enter(ctx) + + if chewing_commit_Check(ctx) != 0 { + if let cptr = chewing_commit_String_static(ctx) { + let s = String(cString: cptr) + // Expect non-empty; exact characters may depend on dictionary, but example is "綠茶". + #expect(!s.isEmpty, "commit string should not be empty") + } else { + #expect(Bool(Bool(false)), "chewing_commit_String_static returned NULL") + } + } else { + #expect(Bool(Bool(false)), "chewing_commit_Check returned false after sequence") + } + } + + @Test func testCandidateEnumerationFlow() { + // Test candidate enumeration after a short input sequence + let ctx = chewing_new() + guard let ctx else { + #expect(Bool(Bool(false)), "failed to create context") + return + } + defer { chewing_delete(ctx) } + + // Prepare selection keys and options + let initialSelKeysStr = "123456789" + let selKeys: [Int32] = initialSelKeysStr.utf8.map { Int32($0) } + [0] + selKeys.withUnsafeBufferPointer { ptr in + chewing_set_selKey(ctx, ptr.baseAddress, 9) + } + chewing_set_candPerPage(ctx, 9) + + // Enter a short input that will produce candidates (e.g., '5' as in example) + _ = chewing_handle_Default(ctx, Int32(Character("5").asciiValue!)) + _ = chewing_handle_Space(ctx) + _ = chewing_handle_Down(ctx) + + // Enumerate candidates + chewing_cand_Enumerate(ctx) + var foundAny = false + while chewing_cand_hasNext(ctx) != 0 { + foundAny = true + if let cstr = chewing_cand_String(ctx) { + let s = String(cString: cstr) + #expect(!s.isEmpty, "candidate string should not be empty") + // Free the returned string + chewing_free(UnsafeMutableRawPointer(mutating: cstr)) + } else { + #expect(Bool(Bool(false)), "chewing_cand_String returned NULL") + } + } + #expect(foundAny, "expected at least one candidate") + } + + @Test func testNullHandling() { + // Verify various APIs behave safely when passed NULL + // Most functions should return -1 for error cases, or return empty strings or NULL pointers. + + // Simple returns + chewing_Reset(nil) // shall not crash + + #expect(chewing_set_KBType(nil, 0) == -1, "chewing_set_KBType(NULL) == -1") + #expect(chewing_get_KBType(nil) == -1, "chewing_get_KBType(NULL) == -1") + + if let buf = chewing_get_KBString(nil) { + let s = String(cString: buf) + #expect(s.isEmpty, "chewing_get_KBString(NULL) should return empty string") + chewing_free(UnsafeMutableRawPointer(buf)) + } else { + #expect(Bool(Bool(false)), "chewing_get_KBString(NULL) returned NULL") + } + + chewing_delete(nil) // shall not crash + chewing_free(nil) // shall not crash + + chewing_set_candPerPage(nil, 0) + #expect(chewing_get_candPerPage(nil) == -1, "chewing_get_candPerPage(NULL) == -1") + + chewing_set_maxChiSymbolLen(nil, 0) + #expect(chewing_get_maxChiSymbolLen(nil) == -1, "chewing_get_maxChiSymbolLen(NULL) == -1") + + chewing_set_selKey(nil, nil, 0) + #expect(chewing_get_selKey(nil) == nil, "chewing_get_selKey(NULL) == NULL") + + chewing_set_addPhraseDirection(nil, 0) + #expect(chewing_get_addPhraseDirection(nil) == -1, "chewing_get_addPhraseDirection(NULL) == -1") + + chewing_set_spaceAsSelection(nil, 0) + #expect(chewing_get_spaceAsSelection(nil) == -1, "chewing_get_spaceAsSelection(NULL) == -1") + + chewing_set_escCleanAllBuf(nil, 0) + #expect(chewing_get_escCleanAllBuf(nil) == -1, "chewing_get_escCleanAllBuf(NULL) == -1") + + chewing_set_autoShiftCur(nil, 0) + #expect(chewing_get_autoShiftCur(nil) == -1, "chewing_get_autoShiftCur(NULL) == -1") + + chewing_set_easySymbolInput(nil, 0) + #expect(chewing_get_easySymbolInput(nil) == -1, "chewing_get_easySymbolInput(NULL) == -1") + + chewing_set_phraseChoiceRearward(nil, 0) + #expect( + chewing_get_phraseChoiceRearward(nil) == -1, "chewing_get_phraseChoiceRearward(NULL) == -1") + + chewing_set_ChiEngMode(nil, 0) + #expect(chewing_get_ChiEngMode(nil) == -1, "chewing_get_ChiEngMode(NULL) == -1") + + chewing_set_ShapeMode(nil, 0) + + #expect(chewing_handle_Space(nil) == -1, "chewing_handle_Space(NULL) == -1") + #expect(chewing_handle_Esc(nil) == -1, "chewing_handle_Esc(NULL) == -1") + #expect(chewing_handle_Enter(nil) == -1, "chewing_handle_Enter(NULL) == -1") + #expect(chewing_handle_Del(nil) == -1, "chewing_handle_Del(NULL) == -1") + #expect(chewing_handle_Backspace(nil) == -1, "chewing_handle_Backspace(NULL) == -1") + #expect(chewing_handle_Up(nil) == -1, "chewing_handle_Up(NULL) == -1") + #expect(chewing_handle_Down(nil) == -1, "chewing_handle_Down(NULL) == -1") + #expect(chewing_handle_ShiftLeft(nil) == -1, "chewing_handle_ShiftLeft(NULL) == -1") + #expect(chewing_handle_Left(nil) == -1, "chewing_handle_Left(NULL) == -1") + #expect(chewing_handle_ShiftRight(nil) == -1, "chewing_handle_ShiftRight(NULL) == -1") + #expect(chewing_handle_Right(nil) == -1, "chewing_handle_Right(NULL) == -1") + #expect(chewing_handle_Tab(nil) == -1, "chewing_handle_Tab(NULL) == -1") + #expect(chewing_handle_DblTab(nil) == -1, "chewing_handle_DblTab(NULL) == -1") + #expect(chewing_handle_Capslock(nil) == -1, "chewing_handle_Capslock(NULL) == -1") + #expect(chewing_handle_Home(nil) == -1, "chewing_handle_Home(NULL) == -1") + #expect(chewing_handle_PageUp(nil) == -1, "chewing_handle_PageUp(NULL) == -1") + #expect(chewing_handle_PageDown(nil) == -1, "chewing_handle_PageDown(NULL) == -1") + #expect(chewing_handle_Default(nil, 0) == -1, "chewing_handle_Default(NULL) == -1") + #expect(chewing_handle_CtrlNum(nil, 0) == -1, "chewing_handle_CtrlNum(NULL) == -1") + #expect(chewing_handle_ShiftSpace(nil) == -1, "chewing_handle_ShiftSpace(NULL) == -1") + #expect(chewing_handle_Numlock(nil, 0) == -1, "chewing_handle_Numlock(NULL) == -1") + + if let phone = chewing_get_phoneSeq(nil) { + #expect(Bool(false), "chewing_get_phoneSeq(NULL) should return NULL or empty") + chewing_free(UnsafeMutableRawPointer(mutating: phone)) + } else { + #expect(Bool(true), "chewing_get_phoneSeq(NULL) returned NULL as expected") + } + #expect(chewing_get_phoneSeqLen(nil) == -1, "chewing_get_phoneSeqLen(NULL) == -1") + + // Logger + chewing_set_logger(nil, nil, nil) + + #expect(chewing_userphrase_enumerate(nil) == -1, "chewing_userphrase_enumerate(NULL) == -1") + #expect( + chewing_userphrase_has_next(nil, nil, nil) == 0, "chewing_userphrase_has_next(NULL) == 0") + #expect(chewing_userphrase_get(nil, nil, 0, nil, 0) == -1, "chewing_userphrase_get(NULL) == -1") + #expect(chewing_userphrase_add(nil, nil, nil) == -1, "chewing_userphrase_add(NULL) == -1") + #expect(chewing_userphrase_remove(nil, nil, nil) == -1, "chewing_userphrase_remove(NULL) == -1") + #expect(chewing_userphrase_lookup(nil, nil, nil) == 0, "chewing_userphrase_lookup(NULL) == 0") + + #expect(chewing_cand_open(nil) == -1, "chewing_cand_open(NULL) == -1") + #expect(chewing_cand_close(nil) == -1, "chewing_cand_close(NULL) == -1") + #expect(chewing_cand_choose_by_index(nil, 0) == -1, "chewing_cand_choose_by_index(NULL) == -1") + + #expect(chewing_cand_list_first(nil) == -1, "chewing_cand_list_first(NULL) == -1") + #expect(chewing_cand_list_last(nil) == -1, "chewing_cand_list_last(NULL) == -1") + #expect(chewing_cand_list_has_next(nil) == 0, "chewing_cand_list_has_next(NULL) == 0") + #expect(chewing_cand_list_has_prev(nil) == 0, "chewing_cand_list_has_prev(NULL) == 0") + #expect(chewing_cand_list_next(nil) == -1, "chewing_cand_list_next(NULL) == -1") + #expect(chewing_cand_list_prev(nil) == -1, "chewing_cand_list_prev(NULL) == -1") + + #expect(chewing_commit_preedit_buf(nil) == -1, "chewing_commit_preedit_buf(NULL) == -1") + #expect(chewing_clean_preedit_buf(nil) == -1, "chewing_clean_preedit_buf(NULL) == -1") + #expect(chewing_clean_bopomofo_buf(nil) == -1, "chewing_clean_bopomofo_buf(NULL) == -1") + + #expect(chewing_commit_Check(nil) == -1, "chewing_commit_Check(NULL) == -1") + if let c = chewing_commit_String(nil) { + let s = String(cString: c) + #expect(s.isEmpty, "chewing_commit_String(NULL) should be empty") + chewing_free(UnsafeMutableRawPointer(c)) + } else { + #expect(Bool(Bool(false)), "chewing_commit_String(NULL) returned NULL") + } + + if let c2 = chewing_commit_String_static(nil) { + let s = String(cString: c2) + #expect(s.isEmpty, "chewing_commit_String_static(NULL) should be empty") + } else { + #expect(Bool(Bool(false)), "chewing_commit_String_static(NULL) returned NULL") + } + + #expect(chewing_buffer_Check(nil) == -1, "chewing_buffer_Check(NULL) == -1") + #expect(chewing_buffer_Len(nil) == -1, "chewing_buffer_Len(NULL) == -1") + if let b = chewing_buffer_String(nil) { + #expect(String(cString: b).isEmpty, "chewing_buffer_String(NULL) should be empty") + chewing_free(UnsafeMutableRawPointer(b)) + } else { + #expect(Bool(Bool(false)), "chewing_buffer_String(NULL) returned NULL") + } + + if let b2 = chewing_buffer_String_static(nil) { + #expect(String(cString: b2).isEmpty, "chewing_buffer_String_static(NULL) should be empty") + } else { + #expect(Bool(Bool(false)), "chewing_buffer_String_static(NULL) returned NULL") + } + + if let bp = chewing_bopomofo_String(nil) { + #expect(String(cString: bp).isEmpty, "chewing_bopomofo_String(NULL) should be empty") + chewing_free(UnsafeMutableRawPointer(bp)) + } else { + #expect(Bool(Bool(false)), "chewing_bopomofo_String(NULL) returned NULL") + } + + if let bp2 = chewing_bopomofo_String_static(nil) { + #expect(String(cString: bp2).isEmpty, "chewing_bopomofo_String_static(NULL) should be empty") + } else { + #expect(Bool(Bool(false)), "chewing_bopomofo_String_static(NULL) returned NULL") + } + + // deprecated zuin + if let zuin = chewing_zuin_String(nil, nil) { + #expect(String(cString: zuin).isEmpty, "chewing_zuin_String(NULL) should be empty") + chewing_free(UnsafeMutableRawPointer(zuin)) + } else { + #expect(Bool(Bool(false)), "chewing_zuin_String(NULL) returned NULL") + } + + #expect(chewing_bopomofo_Check(nil) == -1, "chewing_bopomofo_Check(NULL) == -1") + chewing_zuin_Check(nil) // shall not crash + + #expect(chewing_cursor_Current(nil) == -1, "chewing_cursor_Current(NULL) == -1") + + #expect(chewing_cand_CheckDone(nil) == -1, "chewing_cand_CheckDone(NULL) == -1") + #expect(chewing_cand_TotalPage(nil) == -1, "chewing_cand_TotalPage(NULL) == -1") + #expect(chewing_cand_ChoicePerPage(nil) == -1, "chewing_cand_ChoicePerPage(NULL) == -1") + #expect(chewing_cand_TotalChoice(nil) == -1, "chewing_cand_TotalChoice(NULL) == -1") + #expect(chewing_cand_CurrentPage(nil) == -1, "chewing_cand_CurrentPage(NULL) == -1") + + chewing_cand_Enumerate(nil) // shall not crash + #expect(chewing_cand_hasNext(nil) == -1, "chewing_cand_hasNext(NULL) == -1") + + if let s = chewing_cand_String_static(nil) { + #expect(String(cString: s).isEmpty, "chewing_cand_String_static(NULL) should be empty") + } else { + #expect(Bool(Bool(false)), "chewing_cand_String_static(NULL) returned NULL") + } + + if let sb = chewing_cand_String(nil) { + #expect(String(cString: sb).isEmpty, "chewing_cand_String(NULL) should be empty") + chewing_free(UnsafeMutableRawPointer(sb)) + } else { + #expect(Bool(Bool(false)), "chewing_cand_String(NULL) returned NULL") + } + + chewing_interval_Enumerate(nil) // shall not crash + #expect(chewing_interval_hasNext(nil) == -1, "chewing_interval_hasNext(NULL) == -1") + chewing_interval_Get(nil, nil) // shall not crash + + #expect(chewing_aux_Check(nil) == -1, "chewing_aux_Check(NULL) == -1") + #expect(chewing_aux_Length(nil) == -1, "chewing_aux_Length(NULL) == -1") + + if let as2 = chewing_aux_String_static(nil) { + #expect(String(cString: as2).isEmpty, "chewing_aux_String_static(NULL) should be empty") + } else { + #expect(Bool(Bool(false)), "chewing_aux_String_static(NULL) returned NULL") + } + + if let asb = chewing_aux_String(nil) { + #expect(String(cString: asb).isEmpty, "chewing_aux_String(NULL) should be empty") + chewing_free(UnsafeMutableRawPointer(asb)) + } else { + #expect(Bool(Bool(false)), "chewing_aux_String(NULL) returned NULL") + } + + #expect(chewing_keystroke_CheckIgnore(nil) == -1, "chewing_keystroke_CheckIgnore(NULL) == -1") + #expect(chewing_keystroke_CheckAbsorb(nil) == -1, "chewing_keystroke_CheckAbsorb(NULL) == -1") + + chewing_kbtype_Enumerate(nil) // shall not crash + #expect(chewing_kbtype_hasNext(nil) == -1, "chewing_kbtype_hasNext(NULL) == -1") + + if let kbs = chewing_kbtype_String_static(nil) { + #expect(String(cString: kbs).isEmpty, "chewing_kbtype_String_static(NULL) should be empty") + } else { + #expect(Bool(Bool(false)), "chewing_kbtype_String_static(NULL) returned NULL") + } + + if let kb = chewing_kbtype_String(nil) { + #expect(String(cString: kb).isEmpty, "chewing_kbtype_String(NULL) should be empty") + chewing_free(UnsafeMutableRawPointer(kb)) + } else { + #expect(Bool(Bool(false)), "chewing_kbtype_String(NULL) returned NULL") + } + } + + @Test func testHasOption() { + let ctx = chewing_new() + guard let ctx else { + #expect(Bool(false), "failed to create context") + return + } + defer { chewing_delete(ctx) } + + let options = [ + "chewing.user_phrase_add_direction", + "chewing.disable_auto_learn_phrase", + "chewing.auto_shift_cursor", + "chewing.candidates_per_page", + "chewing.language_mode", + "chewing.easy_symbol_input", + "chewing.esc_clear_all_buffer", + "chewing.keyboard_type", + "chewing.auto_commit_threshold", + "chewing.phrase_choice_rearward", + "chewing.selection_keys", + "chewing.character_form", + "chewing.space_is_select_key", + "chewing.conversion_engine", + "chewing.enable_fullwidth_toggle_key", + ] + + for opt in options { + var rc: Int32 = -1 + opt.withCString { ptr in rc = chewing_config_has_option(ctx, ptr) } + #expect(rc == 1, "should have option '\(opt)'") + } + } + + @Test func testDefaultValue() { + let ctx = chewing_new() + guard let ctx else { + #expect(Bool(false), "failed to create context") + return + } + defer { chewing_delete(ctx) } + + // select key + if let sk = chewing_get_selKey(ctx) { + let buf = UnsafeBufferPointer(start: sk, count: 10) + let expected: [Int32] = "1234567890".utf8.map { Int32($0) } + for i in 0..<10 { #expect(buf[i] == expected[i], "select key index \(i) shall match") } + chewing_free(UnsafeMutableRawPointer(mutating: sk)) + } else { + #expect(Bool(false), "chewing_get_selKey returned NULL") + } + + #expect(chewing_get_candPerPage(ctx) == 10, "default candPerPage shall be 10") + #expect(chewing_get_addPhraseDirection(ctx) == 0, "default addPhraseDirection shall be 0") + #expect(chewing_get_spaceAsSelection(ctx) == 0, "default spaceAsSelection shall be 0") + #expect(chewing_get_escCleanAllBuf(ctx) == 0, "default escCleanAllBuf shall be 0") + #expect(chewing_get_autoShiftCur(ctx) == 0, "default autoShiftCur shall be 0") + #expect(chewing_get_easySymbolInput(ctx) == 0, "default easySymbolInput shall be 0") + #expect(chewing_get_phraseChoiceRearward(ctx) == 0, "default phraseChoiceRearward shall be 0") + #expect(chewing_get_autoLearn(ctx) == 0, "default autoLearn shall be 0") + #expect(chewing_get_ChiEngMode(ctx) == CHINESE_MODE, "default ChiEngMode shall be CHINESE_MODE") + #expect( + chewing_get_ShapeMode(ctx) == HALFSHAPE_MODE, "default ShapeMode shall be HALFSHAPE_MODE") + } + + @Test func testDefaultValueOptions() { + let ctx = chewing_new() + guard let ctx else { + #expect(Bool(false), "failed to create context") + return + } + defer { chewing_delete(ctx) } + + var ptr: UnsafeMutablePointer? = nil + let rc1 = "chewing.selection_keys".withCString { chewing_config_get_str(ctx, $0, &ptr) } + #expect(rc1 == 0, "chewing_config_get_str should return OK") + if let p = ptr { + let s = String(cString: p) + #expect(s == "1234567890", "default select key shall be default value") + chewing_free(UnsafeMutableRawPointer(p)) + } else { + #expect(Bool(false), "chewing_config_get_str returned NULL") + } + + var rc2: Int32 = -1 + "chewing.candidates_per_page".withCString { rc2 = chewing_config_get_int(ctx, $0) } + #expect(rc2 == 10, "default candPerPage shall be 10") + + var rc3: Int32 = -1 + "chewing.auto_commit_threshold".withCString { rc3 = chewing_config_get_int(ctx, $0) } + #expect( + rc3 == chewing_get_maxChiSymbolLen(ctx), + "default chewing.auto_commit_threshold shall equal maxChiSymbolLen") + + var rc4: Int32 = -1 + "chewing.user_phrase_add_direction".withCString { rc4 = chewing_config_get_int(ctx, $0) } + #expect(rc4 == 0, "default chewing.user_phrase_add_direction shall be 0") + + var rc5: Int32 = -1 + "chewing.space_is_select_key".withCString { rc5 = chewing_config_get_int(ctx, $0) } + #expect(rc5 == 0, "default chewing.space_is_select_key shall be 0") + + var rc6: Int32 = -1 + "chewing.esc_clear_all_buffer".withCString { rc6 = chewing_config_get_int(ctx, $0) } + #expect(rc6 == 0, "default chewing.esc_clear_all_buffer shall be 0") + + var rc7: Int32 = -1 + "chewing.auto_shift_cursor".withCString { rc7 = chewing_config_get_int(ctx, $0) } + #expect(rc7 == 0, "default chewing.auto_shift_cursor shall be 0") + + var rc8: Int32 = -1 + "chewing.easy_symbol_input".withCString { rc8 = chewing_config_get_int(ctx, $0) } + #expect(rc8 == 0, "default chewing.easy_symbol_input shall be 0") + + var rc9: Int32 = -1 + "chewing.phrase_choice_rearward".withCString { rc9 = chewing_config_get_int(ctx, $0) } + #expect(rc9 == 0, "default chewing.phrase_choice_rearward shall be 0") + + var rc10: Int32 = -1 + "chewing.disable_auto_learn_phrase".withCString { rc10 = chewing_config_get_int(ctx, $0) } + #expect(rc10 == 0, "default chewing.disable_auto_learn_phrase shall be 0") + + var rc11: Int32 = -1 + "chewing.language_mode".withCString { rc11 = chewing_config_get_int(ctx, $0) } + #expect(rc11 == CHINESE_MODE, "default chewing.language_mode shall be CHINESE_MODE") + + var rc12: Int32 = -1 + "chewing.character_form".withCString { rc12 = chewing_config_get_int(ctx, $0) } + #expect(rc12 == HALFSHAPE_MODE, "default chewing.character_form shall be HALFSHAPE_MODE") + + var rc13: Int32 = -1 + "chewing.conversion_engine".withCString { rc13 = chewing_config_get_int(ctx, $0) } + #expect(rc13 == 1, "default chewing.conversion_engine shall be 1") + } + + @Test func testSetCandPerPage() { + let ctx = chewing_new() + guard let ctx else { + #expect(Bool(false), "failed to create context") + return + } + defer { chewing_delete(ctx) } + + chewing_set_maxChiSymbolLen(ctx, 10) + let valid: [Int32] = [1, 10] + let invalid: [Int32] = [0, 11] + + for v in valid { + chewing_set_candPerPage(ctx, v) + #expect(chewing_get_candPerPage(ctx) == v, "candPerPage shall be \(v)") + #expect(chewing_get_maxChiSymbolLen(ctx) == 10, "maxChiSymbolLen shall be 10") + + for inv in invalid { + chewing_set_candPerPage(ctx, inv) + #expect(chewing_get_candPerPage(ctx) == v, "candPerPage shall remain \(v) on invalid set") + } + } + } + + @Test func testSetMaxChiSymbolLen() { + let ctx = chewing_new() + guard let ctx else { + #expect(Bool(false), "failed to create context") + return + } + defer { chewing_delete(ctx) } + + chewing_set_maxChiSymbolLen(ctx, 16) + #expect(chewing_get_maxChiSymbolLen(ctx) == 16, "maxChiSymbolLen shall be 16") + + chewing_set_maxChiSymbolLen(ctx, MIN_CHI_SYMBOL_LEN - 1) + #expect( + chewing_get_maxChiSymbolLen(ctx) == 16, "maxChiSymbolLen shall not change on invalid value") + + chewing_set_maxChiSymbolLen(ctx, MAX_CHI_SYMBOL_LEN + 1) + #expect( + chewing_get_maxChiSymbolLen(ctx) == 16, "maxChiSymbolLen shall not change on invalid value") + } + + @Test func testSetSelKeyNormal() { + let ctx = chewing_new() + guard let ctx else { + #expect(Bool(false), "failed to create context") + return + } + defer { chewing_delete(ctx) } + + let alt = "asdfghjkl;" + let arr: [Int32] = alt.utf8.map { Int32($0) } + [0] + arr.withUnsafeBufferPointer { ptr in + chewing_set_selKey(ctx, ptr.baseAddress, Int32(alt.count)) + } + + if let sk = chewing_get_selKey(ctx) { + let buf = UnsafeBufferPointer(start: sk, count: Int(MAX_SELKEY)) + let expected: [Int32] = "asdfghjkl;".utf8.map { Int32($0) } + [0] + for i in 0..<10 { #expect(buf[i] == expected[i], "select key index \(i) shall match") } + chewing_free(UnsafeMutableRawPointer(mutating: sk)) + } else { + #expect(Bool(false), "chewing_get_selKey returned NULL") + } + + // test config set/get + let setRc = "chewing.selection_keys".withCString { + chewing_config_set_str(ctx, $0, "asdfghjkl;") + } + #expect(setRc == 0, "chewing_config_set_str should return OK") + + var ptr: UnsafeMutablePointer? = nil + let getRc = "chewing.selection_keys".withCString { chewing_config_get_str(ctx, $0, &ptr) } + #expect(getRc == 0, "chewing_config_get_str should return OK") + if let p = ptr { + let s = String(cString: p) + #expect(s == "asdfghjkl;", "select key shall be updated") + chewing_free(UnsafeMutableRawPointer(p)) + } else { + #expect(Bool(false), "chewing_config_get_str returned NULL") + } + } + + @Test func testSetSelKeyErrorHandling() { + let ctx = chewing_new() + guard let ctx else { + #expect(Bool(false), "failed to create context") + return + } + defer { chewing_delete(ctx) } + + // Passing NULLs shall not crash and defaults shall remain + let alt: [Int32] = "asdfghjkl;".utf8.map { Int32($0) } + [0] + alt.withUnsafeBufferPointer { ptr in chewing_set_selKey(nil, ptr.baseAddress, Int32(alt.count)) + } + if let sk = chewing_get_selKey(ctx) { + let buf = UnsafeBufferPointer(start: sk, count: 10) + let expected: [Int32] = "1234567890".utf8.map { Int32($0) } + for i in 0..<10 { #expect(buf[i] == expected[i], "select key shall be default value") } + chewing_free(UnsafeMutableRawPointer(mutating: sk)) + } else { + #expect(Bool(false), "chewing_get_selKey returned NULL") + } + + // invalid set via config + let rcSet = "chewing.selection_keys".withCString { + chewing_config_set_str(ctx, $0, "asdfghjkl;1234") + } + #expect(rcSet == -1, "chewing_config_set_str should return ERROR on invalid value") + + var ptr: UnsafeMutablePointer? = nil + let rcGet = "chewing.selection_keys".withCString { chewing_config_get_str(ctx, $0, &ptr) } + #expect(rcGet == 0, "chewing_config_get_str should return OK") + if let p = ptr { + let s = String(cString: p) + #expect(s == "1234567890", "select key shall be default value") + chewing_free(UnsafeMutableRawPointer(p)) + } else { + #expect(Bool(false), "chewing_config_get_str returned NULL") + } + } +} diff --git a/tests_swift/TestUtilAPIs.swift b/tests_swift/TestUtilAPIs.swift new file mode 100644 index 000000000..efea0def8 --- /dev/null +++ b/tests_swift/TestUtilAPIs.swift @@ -0,0 +1,194 @@ +import Foundation +import Testing + +@testable import CChewing + +#if os(Linux) + import Glibc +#else + import Darwin +#endif + +// Cross-platform env setter +func setEnv(_ key: String, _ value: String) { + #if os(Linux) + Glibc.setenv(key, value, 1) + #else + Darwin.setenv(key, value, 1) + #endif +} + +@MainActor +class TestBaseClass { + @MainActor + // Helper: minimal keystroke parser for test strings like "", "", "", etc. + func type_keystroke_by_string(_ ctx: OpaquePointer?, _ keystroke: String) { + var i = keystroke.startIndex + while i < keystroke.endIndex { + if keystroke[i] == "<" { + // parse token until '>' + let j = keystroke[i...].firstIndex(of: ">") ?? keystroke.index(after: i) + let token = String(keystroke[keystroke.index(after: i)..?) -> Int { + guard let cstr else { return 0 } + return String(cString: cstr).count + } + + @MainActor + func ok_preedit_buffer(_ ctx: OpaquePointer?, _ expected: String) { + if let b = chewing_buffer_String(ctx) { + let s = String(cString: b) + #expect(s == expected, "preedit buffer should be '\(expected)' but was '\(s)'") + chewing_free(UnsafeMutableRawPointer(mutating: b)) + } else { + #expect(Bool(false), "chewing_buffer_String returned NULL") + } + } + @MainActor + func ok_bopomofo_buffer(_ ctx: OpaquePointer?, _ expected: String) { + if let b = chewing_bopomofo_String(ctx) { + let s = String(cString: b) + #expect(s == expected, "bopomofo buffer should be '\(expected)' but was '\(s)'") + chewing_free(UnsafeMutableRawPointer(mutating: b)) + } else { + #expect(Bool(false), "chewing_bopomofo_String returned NULL") + } + } +} + +@MainActor +class DataBackedTestSuite: TestBaseClass { + + override init() { + // Minimal environment setup for C API accessibility tests. + // Keep it lightweight and cross-platform to run under Linux swift images. + let cwd = FileManager.default.currentDirectoryPath + let scratch = URL(fileURLWithPath: cwd).appendingPathComponent("scratch") + + // Ensure a clean scratch directory + try? FileManager.default.removeItem(at: scratch) + do { + try FileManager.default.createDirectory(at: scratch, withIntermediateDirectories: true) + } catch { + print("Failed to create scratch directory: \(error)") + } + + // Prefer repository data dir if present, otherwise use tests/data + let repoDataDir = "\(cwd)/data/dict/chewing" + let srcDir = + FileManager.default.fileExists(atPath: repoDataDir) ? repoDataDir : "\(cwd)/tests/data" + + setEnv("CHEWING_PATH", srcDir) + + // create a userpath under scratch + let userScratch = scratch.appendingPathComponent("tests") + try? FileManager.default.createDirectory(at: userScratch, withIntermediateDirectories: true) + setEnv("CHEWING_USER_PATH", userScratch.path) + + print("CHEWING_PATH set to \(srcDir), CHEWING_USER_PATH=\(userScratch.path)") + } + + deinit { + // Remove the temporary scratch database after the test suite finishes + let cwd = FileManager.default.currentDirectoryPath + let scratch = URL(fileURLWithPath: cwd).appendingPathComponent("scratch") + try? FileManager.default.removeItem(at: scratch) + } +} diff --git a/tools/CargoBuildPlugin/Plugin.swift b/tools/CargoBuildPlugin/Plugin.swift new file mode 100644 index 000000000..7b50ce527 --- /dev/null +++ b/tools/CargoBuildPlugin/Plugin.swift @@ -0,0 +1,69 @@ +import Foundation +import PackagePlugin + +@main +struct CargoBuildPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + // Paths + let packageDir = context.package.directoryURL + let capiDir = packageDir.appendingPathComponent("capi") + // Use the plugin's work directory (sandboxed) to avoid macOS permission issues + let scratchTarget = context.pluginWorkDirectoryURL.appendingPathComponent("cargo-target") + + // Auto-build is enabled by default. To disable automatic Cargo build, set `LIBCHEWING_AUTO_BUILD_CARGO=0`. + if ProcessInfo.processInfo.environment["LIBCHEWING_AUTO_BUILD_CARGO"] == "0" { + return [] + } + + // Prefer common cargo install locations; if not found, abort with a clear error + let fm = FileManager.default + let home = fm.homeDirectoryForCurrentUser.path + let candidates: [URL] = [ + URL(fileURLWithPath: "/usr/bin/cargo"), + URL(fileURLWithPath: "/usr/local/bin/cargo"), + URL(fileURLWithPath: "/opt/homebrew/bin/cargo"), + URL(fileURLWithPath: "\(home)/.cargo/bin/cargo"), + ] + + var cargoURL: URL? = nil + for url in candidates { + if fm.fileExists(atPath: url.path) { + cargoURL = url + break + } + } + + guard let cargo = cargoURL else { + struct UserError: Error, CustomStringConvertible { + let description: String + init(_ s: String) { description = s } + } + throw UserError( + "`cargo` not found on the system. Please install Rust (https://rustup.rs/) to enable automatic builds, or disable automatic Cargo build by setting `LIBCHEWING_AUTO_BUILD_CARGO=0` and run `scripts/build-cargo.sh` manually to produce the library before running `swift build`." + ) + } + + let manifestPath = capiDir.appendingPathComponent("Cargo.toml").path + let targetDir = scratchTarget.path + + // Arguments: build the chewing_capi crate in release mode into the plugin workdir target + let args = [ + "build", + "--release", + "--manifest-path", + manifestPath, + "--target-dir", + targetDir, + ] + + return [ + .prebuildCommand( + displayName: "Building chewing_capi via cargo", + executable: cargo, + arguments: args, + environment: [:], + outputFilesDirectory: scratchTarget + ) + ] + } +} From ee14c28e1f80362eb90a2e3aa6d994b96df4aa56 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Mon, 29 Dec 2025 13:07:13 +0800 Subject: [PATCH 5/8] chore: moving Swift files to a dedicated folder. --- Package.swift | 6 +- {scripts => swift/scripts}/build-cargo.sh | 3 +- swift/tools/CargoBuildPlugin/Plugin.swift | 69 +++++++++++++++++++ .../unit_tests}/ChewingTests_Suite1.swift | 0 .../unit_tests}/ChewingTests_Suite2.swift | 0 .../unit_tests}/TestUtilAPIs.swift | 0 6 files changed, 74 insertions(+), 4 deletions(-) rename {scripts => swift/scripts}/build-cargo.sh (78%) create mode 100644 swift/tools/CargoBuildPlugin/Plugin.swift rename {tests_swift => swift/unit_tests}/ChewingTests_Suite1.swift (100%) rename {tests_swift => swift/unit_tests}/ChewingTests_Suite2.swift (100%) rename {tests_swift => swift/unit_tests}/TestUtilAPIs.swift (100%) diff --git a/Package.swift b/Package.swift index 28fd22f60..4d31b4f96 100644 --- a/Package.swift +++ b/Package.swift @@ -33,13 +33,13 @@ let package = Package( .plugin( name: "CargoBuild", capability: .buildTool(), - path: "tools/CargoBuildPlugin" + path: "swift/tools/CargoBuildPlugin" ), - // Swift test target in `tests_swift` to validate C API accessibility from Swift + // Swift test target in `swift/unit_tests` to validate C API accessibility from Swift .testTarget( name: "ChewingTests", dependencies: ["CChewing"], - path: "tests_swift" + path: "swift/unit_tests" ), ] ) diff --git a/scripts/build-cargo.sh b/swift/scripts/build-cargo.sh similarity index 78% rename from scripts/build-cargo.sh rename to swift/scripts/build-cargo.sh index a11393cb0..a5debd97a 100644 --- a/scripts/build-cargo.sh +++ b/swift/scripts/build-cargo.sh @@ -4,7 +4,8 @@ set -euo pipefail # Build the chewing_capi Rust crate for use with Swift Package # Outputs static library and artifacts into `target/cargo-target/release`. -REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +# NOTE: script moved under `swift/scripts`, so go up two levels to reach repo root +REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" CARGO_MANIFEST_PATH="$REPO_ROOT/capi/Cargo.toml" TARGET_DIR="$REPO_ROOT/target/cargo-target" diff --git a/swift/tools/CargoBuildPlugin/Plugin.swift b/swift/tools/CargoBuildPlugin/Plugin.swift new file mode 100644 index 000000000..b6436fbbf --- /dev/null +++ b/swift/tools/CargoBuildPlugin/Plugin.swift @@ -0,0 +1,69 @@ +import Foundation +import PackagePlugin + +@main +struct CargoBuildPlugin: BuildToolPlugin { + func createBuildCommands(context: PluginContext, target: Target) throws -> [Command] { + // Paths + let packageDir = context.package.directoryURL + let capiDir = packageDir.appendingPathComponent("capi") + // Use the plugin's work directory (sandboxed) to avoid macOS permission issues + let scratchTarget = context.pluginWorkDirectoryURL.appendingPathComponent("cargo-target") + + // Auto-build is enabled by default. To disable automatic Cargo build, set `LIBCHEWING_AUTO_BUILD_CARGO=0`. + if ProcessInfo.processInfo.environment["LIBCHEWING_AUTO_BUILD_CARGO"] == "0" { + return [] + } + + // Prefer common cargo install locations; if not found, abort with a clear error + let fm = FileManager.default + let home = fm.homeDirectoryForCurrentUser.path + let candidates: [URL] = [ + URL(fileURLWithPath: "/usr/bin/cargo"), + URL(fileURLWithPath: "/usr/local/bin/cargo"), + URL(fileURLWithPath: "/opt/homebrew/bin/cargo"), + URL(fileURLWithPath: "\(home)/.cargo/bin/cargo"), + ] + + var cargoURL: URL? = nil + for url in candidates { + if fm.fileExists(atPath: url.path) { + cargoURL = url + break + } + } + + guard let cargo = cargoURL else { + struct UserError: Error, CustomStringConvertible { + let description: String + init(_ s: String) { description = s } + } + throw UserError( + "`cargo` not found on the system. Please install Rust (https://rustup.rs/) to enable automatic builds, or disable automatic Cargo build by setting `LIBCHEWING_AUTO_BUILD_CARGO=0` and run `swift/scripts/build-cargo.sh` manually to produce the library before running `swift build`." + ) + } + + let manifestPath = capiDir.appendingPathComponent("Cargo.toml").path + let targetDir = scratchTarget.path + + // Arguments: build the chewing_capi crate in release mode into the plugin workdir target + let args = [ + "build", + "--release", + "--manifest-path", + manifestPath, + "--target-dir", + targetDir, + ] + + return [ + .prebuildCommand( + displayName: "Building chewing_capi via cargo", + executable: cargo, + arguments: args, + environment: [:], + outputFilesDirectory: scratchTarget + ) + ] + } +} diff --git a/tests_swift/ChewingTests_Suite1.swift b/swift/unit_tests/ChewingTests_Suite1.swift similarity index 100% rename from tests_swift/ChewingTests_Suite1.swift rename to swift/unit_tests/ChewingTests_Suite1.swift diff --git a/tests_swift/ChewingTests_Suite2.swift b/swift/unit_tests/ChewingTests_Suite2.swift similarity index 100% rename from tests_swift/ChewingTests_Suite2.swift rename to swift/unit_tests/ChewingTests_Suite2.swift diff --git a/tests_swift/TestUtilAPIs.swift b/swift/unit_tests/TestUtilAPIs.swift similarity index 100% rename from tests_swift/TestUtilAPIs.swift rename to swift/unit_tests/TestUtilAPIs.swift From 9f573c189e29f89ee7b4cac5d9a8a4780b830289 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Mon, 29 Dec 2025 13:20:33 +0800 Subject: [PATCH 6/8] ci: add test target against macOS 26. --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11dbc950d..889885af1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,6 +134,36 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + # On macOS we run Swift Package Manager tests to validate SPM integration. + # Use macOS 26 runner if available—fall back to macos-latest if not. + # No need to add code-coverage for this target as we already do that on Linux above. + # The essense of this Swift package is to allow all C APIs to be accessible from Swift. + macos-swift: + # If your org's runner image naming differs, replace `macos-26` with `macos-latest`. + runs-on: macos-26 + name: macOS Swift tests (macOS 26) + + steps: + - uses: actions/checkout@v5 + with: + submodules: true + + - name: Setup rustup + run: | + rustup set auto-self-update disable + rustup install ${{ env.RUST_VERSION }} + rustup default ${{ env.RUST_VERSION }} + + - name: Build chewing_capi via cargo (macOS) + # Ensure the cargo-built static lib is present for the Swift package to link + run: | + chmod +x ./swift/scripts/build-cargo.sh + ./swift/scripts/build-cargo.sh + + - name: Run Swift tests + run: | + swift test -v + # https://github.com/orgs/community/discussions/26822 results: if: ${{ always() }} From c32f54b1859c78a49115df9b7e443656fb1c9f0c Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Mon, 29 Dec 2025 13:24:18 +0800 Subject: [PATCH 7/8] ci: use `checkout@v6` on macOS. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e5979d8b..ce1bd557c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,7 +144,7 @@ jobs: name: macOS Swift tests (macOS 26) steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 with: submodules: true From 3a3df8b5bdf7e8377afab87825e1174c0d471c00 Mon Sep 17 00:00:00 2001 From: Kan-Ru Chen Date: Mon, 29 Dec 2025 18:46:36 +0900 Subject: [PATCH 8/8] build: simplify Swift package and CI --- .github/workflows/ci.yml | 35 +++++------------------------------ Package.swift | 3 +-- swift/scripts/build-cargo.sh | 17 ----------------- 3 files changed, 6 insertions(+), 49 deletions(-) delete mode 100644 swift/scripts/build-cargo.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce1bd557c..210791569 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,6 +63,11 @@ jobs: run: | ctest --test-dir build --output-on-failure + - if: ${{ matrix.os != 'windows-latest' }} + name: Swift Test + run: | + swift test -v + aarch64-smoke: runs-on: ubuntu-latest name: Aarch64 cross-compile smoke test @@ -134,36 +139,6 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - # On macOS we run Swift Package Manager tests to validate SPM integration. - # Use macOS 26 runner if available—fall back to macos-latest if not. - # No need to add code-coverage for this target as we already do that on Linux above. - # The essense of this Swift package is to allow all C APIs to be accessible from Swift. - macos-swift: - # If your org's runner image naming differs, replace `macos-26` with `macos-latest`. - runs-on: macos-26 - name: macOS Swift tests (macOS 26) - - steps: - - uses: actions/checkout@v6 - with: - submodules: true - - - name: Setup rustup - run: | - rustup set auto-self-update disable - rustup install ${{ env.RUST_VERSION }} - rustup default ${{ env.RUST_VERSION }} - - - name: Build chewing_capi via cargo (macOS) - # Ensure the cargo-built static lib is present for the Swift package to link - run: | - chmod +x ./swift/scripts/build-cargo.sh - ./swift/scripts/build-cargo.sh - - - name: Run Swift tests - run: | - swift test -v - # https://github.com/orgs/community/discussions/26822 results: if: ${{ always() }} diff --git a/Package.swift b/Package.swift index 4d31b4f96..bafebc59a 100644 --- a/Package.swift +++ b/Package.swift @@ -18,10 +18,9 @@ let package = Package( publicHeadersPath: "include", // Instruct the linker to search common Cargo target dirs where the built library may be placed linkerSettings: [ - .unsafeFlags(["-L", "./target/cargo-target/release"]), .unsafeFlags([ "-L", - ".build/plugins/outputs/libchewing-spm/CChewing/destination/CargoBuild/cargo-target/release", + ".build/plugins/outputs/libchewing/CChewing/destination/CargoBuild/cargo-target/release", ]), .linkedLibrary("chewing_capi"), ], diff --git a/swift/scripts/build-cargo.sh b/swift/scripts/build-cargo.sh deleted file mode 100644 index a5debd97a..000000000 --- a/swift/scripts/build-cargo.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Build the chewing_capi Rust crate for use with Swift Package -# Outputs static library and artifacts into `target/cargo-target/release`. - -# NOTE: script moved under `swift/scripts`, so go up two levels to reach repo root -REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)" -CARGO_MANIFEST_PATH="$REPO_ROOT/capi/Cargo.toml" -TARGET_DIR="$REPO_ROOT/target/cargo-target" - -echo "Building chewing_capi (release) -> $TARGET_DIR" -mkdir -p "$TARGET_DIR" - -cargo build --release --manifest-path "$CARGO_MANIFEST_PATH" --target-dir "$TARGET_DIR" - -echo "Done. Built artifacts are in $TARGET_DIR/release" \ No newline at end of file