diff --git a/HISTORY.md b/HISTORY.md index 9470cb338a..e0ff7e48c7 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # Keyman Version History +## 19.0.234 alpha 2026-05-19 + +* chore(deps): bump brace-expansion from 5.0.5 to 5.0.6 in /developer/src/server/src/win32/trayicon/addon-src (#15969) +* chore(deps): bump ws from 8.18.1 to 8.20.1 (#15971) +* chore: move to .localhost for local server URLs (#15963) + ## 19.0.233 alpha 2026-05-18 * chore(deps): bump path-to-regexp and express (#15902) diff --git a/VERSION.md b/VERSION.md index a9145f9f76..471ba0baf2 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.234 \ No newline at end of file +19.0.235 \ No newline at end of file diff --git a/common/windows/delphi/general/Upload_Settings.pas b/common/windows/delphi/general/Upload_Settings.pas index e1e8ce4294..8811ec6f10 100644 --- a/common/windows/delphi/general/Upload_Settings.pas +++ b/common/windows/delphi/general/Upload_Settings.pas @@ -66,11 +66,11 @@ const URLPath_Community = '/go/'+SKeymanVersion+'/community'; // Keyboard download and installation - URLPath_RegEx_MatchKeyboardsInstall = '^http(?:s)?://keyman(?:-staging)?\.com(?:\.local)?/keyboards/install/([^?/]+)(?:\?(.+))?$'; + URLPath_RegEx_MatchKeyboardsInstall = '^http(?:s)?://keyman(?:-staging)?\.com(?:\.localhost)?/keyboards/install/([^?/]+)(?:\?(.+))?$'; // e.g. https://keyman.com/keyboards/install/foo - UrlPath_RegEx_MatchKeyboardsRoot = '^http(?:s)?://keyman(?:-staging)?\.com(?:\.local)?/keyboards([/?].*)?$'; - // e.g. http://keyman.com.local/keyboards/foo - UrlPath_RegEx_MatchKeyboardsGo = '^http(?:s)?://keyman(?:-staging)?\.com(?:\.local)?/go/windows/[^/]+/download-keyboards'; + UrlPath_RegEx_MatchKeyboardsRoot = '^http(?:s)?://keyman(?:-staging)?\.com(?:\.localhost)?/keyboards([/?].*)?$'; + // e.g. http://keyman.com.localhost/keyboards/foo + UrlPath_RegEx_MatchKeyboardsGo = '^http(?:s)?://keyman(?:-staging)?\.com(?:\.localhost)?/go/windows/[^/]+/download-keyboards'; // e.g. https://keyman-staging.com/go/windows/14.0/download-keyboards?version=14.0.146.0 // Cloning keyboards - Keyman Developer diff --git a/core/src/action.cpp b/core/src/action.cpp index 1ff7917575..ca6b0b30d8 100644 --- a/core/src/action.cpp +++ b/core/src/action.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include "action.hpp" @@ -183,3 +184,34 @@ bool km::core::state::set_actions( return true; } +using namespace km::core; + +namespace { + +km_core_usv * duplicate_km_core_usv(const km_core_usv *src) { + if (!src) { + return nullptr; + } + size_t len = 0; + while (src[len]) { + ++len; + } + km_core_usv *result = new km_core_usv[len + 1]; + std::copy(src, src + len + 1, result); + return result; +} + +} // namespace + +km_core_actions km::core::clone_actions_object(km_core_actions const &src) { + km_core_actions result; + memset(&result, 0, sizeof(result)); + result.code_points_to_delete = src.code_points_to_delete; + result.do_alert = src.do_alert; + result.emit_keystroke = src.emit_keystroke; + result.new_caps_lock_state = src.new_caps_lock_state; + result.deleted_context = duplicate_km_core_usv(src.deleted_context); + result.output = duplicate_km_core_usv(src.output); + result.persist_options = clone_options(src.persist_options); + return result; +} diff --git a/core/src/action.hpp b/core/src/action.hpp index db1f610851..220acbad2d 100644 --- a/core/src/action.hpp +++ b/core/src/action.hpp @@ -39,5 +39,9 @@ namespace core unsigned int code_points_to_delete ); + km_core_actions clone_actions_object( + km_core_actions const &src + ); + } // namespace core } // namespace km diff --git a/core/src/km_core_processevent_api.cpp b/core/src/km_core_processevent_api.cpp index ade2665353..f48101073f 100644 --- a/core/src/km_core_processevent_api.cpp +++ b/core/src/km_core_processevent_api.cpp @@ -37,7 +37,9 @@ km_core_event( return KM_CORE_STATUS_INVALID_ARGUMENT; } - return state->processor().external_event(state, event, data); + km_core_status status = state->processor().external_event(state, event, data); + state->apply_actions_and_merge_app_context(); + return status; } km_core_status diff --git a/core/src/option.cpp b/core/src/option.cpp index 6c66c93d3a..53fbfaa50d 100644 --- a/core/src/option.cpp +++ b/core/src/option.cpp @@ -82,3 +82,18 @@ json & km::core::operator << (json &j, abstract_processor const &) return j; } + +km_core_option_item * +km::core::clone_options(km_core_option_item const *src) { + if (!src) { + return nullptr; + } + size_t count = km_core_options_list_size(src); + km_core_option_item *result = new km_core_option_item[count + 1]; + for (size_t i = 0; i < count; ++i) { + km::core::option opt(static_cast(src[i].scope), src[i].key, src[i].value); + result[i] = opt.release(); + } + result[count] = KM_CORE_OPTIONS_END; + return result; +} diff --git a/core/src/option.hpp b/core/src/option.hpp index 60026798cf..56e231c302 100644 --- a/core/src/option.hpp +++ b/core/src/option.hpp @@ -93,7 +93,7 @@ namespace core return key == nullptr; } - + km_core_option_item * clone_options(km_core_option_item const *src); } // namespace core } // namespace km diff --git a/core/src/state.cpp b/core/src/state.cpp index 47f29d0a67..8eb99d1b0a 100644 --- a/core/src/state.cpp +++ b/core/src/state.cpp @@ -56,6 +56,9 @@ state::state(km::core::abstract_processor & ap, km_core_option_item const *env) _imx_callback = nullptr; _imx_object = nullptr; memset(const_cast(&_action_struct), 0, sizeof(km_core_actions)); + // Ensure _action_struct is initialized to the default values + km_core_action_item no_actions = {KM_CORE_IT_END, {0,}, {0}}; + action_item_list_to_actions_object(&no_actions, &this->_action_struct); } void state::imx_register_callback( @@ -82,6 +85,19 @@ void state::imx_callback(uint32_t imx_id) { _imx_callback(static_cast(this), imx_id, _imx_object); } +state::state(state const &other) + : _ctxt(other._ctxt) + , _app_ctxt(other._app_ctxt) + , _processor(other._processor) + , _actions(other._actions) + , _action_struct(clone_actions_object(other._action_struct)) + , _debug_items(other._debug_items) + , _imx_callback(other._imx_callback) + , _imx_object(other._imx_object) + , _backspace_handled_internally(other._backspace_handled_internally) +{ +} + state::~state() { km::core::actions_dispose(this->_action_struct); } diff --git a/core/src/state.hpp b/core/src/state.hpp index 0cf58fb614..da2cf6bb75 100644 --- a/core/src/state.hpp +++ b/core/src/state.hpp @@ -135,7 +135,7 @@ protected: public: state(core::abstract_processor & kb, km_core_option_item const *env); - state(state const &) = default; + state(state const &other); state(state const &&) = delete; ~state(); diff --git a/core/tests/unit/kmnkbd/state_api.tests.cpp b/core/tests/unit/kmnkbd/state_api.tests.cpp index 70dcce3133..f17e448080 100644 --- a/core/tests/unit/kmnkbd/state_api.tests.cpp +++ b/core/tests/unit/kmnkbd/state_api.tests.cpp @@ -42,6 +42,165 @@ namespace return buf; } + inline + bool action_options_equal(km_core_option_item const * lhs, + km_core_option_item const * rhs) + { + if (lhs == rhs) return true; + if (!lhs || !rhs) return false; + + while (lhs->key && rhs->key) { + if (lhs->scope != rhs->scope) return false; + if (std::u16string(lhs->key) != std::u16string(rhs->key)) return false; + if (std::u16string(lhs->value) != std::u16string(rhs->value)) return false; + ++lhs; + ++rhs; + } + + return lhs->key == nullptr && rhs->key == nullptr; + } + + inline + bool expect_action_struct( + km_core_actions const & actions, + unsigned int expected_code_points_to_delete, + km_core_usv const * expected_output, + km_core_option_item const * expected_persist_options, + km_core_bool expected_do_alert, + km_core_bool expected_emit_keystroke, + km_core_caps_state expected_new_caps_lock_state, + km_core_usv const * expected_deleted_context + ) { + bool all_passed = true; + + std::cout << "\n=== Comparing action_struct fields ===" << std::endl; + + // code_points_to_delete + std::cout << "code_points_to_delete: " << actions.code_points_to_delete + << " (expected: " << expected_code_points_to_delete << ")"; + if (actions.code_points_to_delete != expected_code_points_to_delete) { + std::cout << " [FAIL]" << std::endl; + all_passed = false; + } else { + std::cout << " [PASS]" << std::endl; + } + + // output + std::cout << "output: " << (actions.output ? std::u32string(actions.output) : U"(null)") + << " expected: " << (expected_output ? std::u32string(expected_output) : U"(null)") << std::endl; + bool output_equal = false; + if (expected_output == actions.output) { // nullptr or same pointer + output_equal = true; + } else if (!expected_output || !actions.output) { + output_equal = false; + } else if (expected_output && actions.output) { + if (std::u32string(actions.output) != std::u32string(expected_output)) { + output_equal = false; + } else { + output_equal = true; + } + } + if (!output_equal) { + std::cout << " [FAIL]" << std::endl; + all_passed = false; + } else { + std::cout << " [PASS]" << std::endl; + } + + // persist_options + std::cout << "persist_options comparison:" << std::endl; + if (!action_options_equal(actions.persist_options, expected_persist_options)) { + std::cout << " actual: "; + if (actions.persist_options) { + for (auto opt = actions.persist_options; opt->scope; ++opt) { + std::cout << "[scope=" << opt->scope << ", key=" << opt->key << ", value=" << opt->value << "] "; + } + } else { + std::cout << "(null)"; + } + std::cout << std::endl; + std::cout << " expected: "; + if (expected_persist_options) { + for (auto opt = expected_persist_options; opt->key; ++opt) { + std::cout << "[scope=" << opt->scope << ", key=" << opt->key << ", value=" << opt->value << "] "; + } + } else { + std::cout << "(null)"; + } + std::cout << " [FAIL]" << std::endl; + all_passed = false; + } else { + std::cout << " [PASS]" << std::endl; + } + + // do_alert + std::cout << "do_alert: " << (int)actions.do_alert + << " (expected: " << (int)expected_do_alert << ")"; + if (actions.do_alert != expected_do_alert) { + std::cout << " [FAIL]" << std::endl; + all_passed = false; + } else { + std::cout << " [PASS]" << std::endl; + } + + // emit_keystroke + std::cout << "emit_keystroke: " << (int)actions.emit_keystroke + << " (expected: " << (int)expected_emit_keystroke << ")"; + if (actions.emit_keystroke != expected_emit_keystroke) { + std::cout << " [FAIL]" << std::endl; + all_passed = false; + } else { + std::cout << " [PASS]" << std::endl; + } + + // new_caps_lock_state + std::cout << "new_caps_lock_state: " << (int)actions.new_caps_lock_state + << " (expected: " << (int)expected_new_caps_lock_state << ")"; + if (actions.new_caps_lock_state != expected_new_caps_lock_state) { + std::cout << " [FAIL]" << std::endl; + all_passed = false; + } else { + std::cout << " [PASS]" << std::endl; + } + + // deleted_context + std::cout << "deleted_context: " << (actions.deleted_context ? std::u32string(actions.deleted_context) : U"(null)") + << " (expected: " << (expected_deleted_context ? std::u32string(expected_deleted_context) : U"(null)") << ")"; + if (expected_deleted_context != actions.deleted_context) { + if (std::u32string(actions.deleted_context) != std::u32string(expected_deleted_context)) { + std::cout << " [FAIL]" << std::endl; + all_passed = false; + } else { + std::cout << " [PASS]" << std::endl; + } + } else { + std::cout << " [PASS]" << std::endl; + } + bool deleted_equal = false; + if (expected_deleted_context == actions.deleted_context) { // nullptr or same pointer + deleted_equal = true; + } else if (!expected_deleted_context || !actions.deleted_context) { + deleted_equal = false; + } else if (expected_deleted_context && actions.deleted_context) { + if (std::u32string(actions.deleted_context) != std::u32string(expected_deleted_context)) { + deleted_equal = false; + } else { + deleted_equal = true; + } + } + if (!deleted_equal) { + std::cout << " [FAIL]" << std::endl; + all_passed = false; + } else { + std::cout << " [PASS]" << std::endl; + } + + + std::cout << "=== End comparison ===" << std::endl << std::endl; + + return all_passed; + } + km_core_option_item test_env_opts[] = { {u"hello", u"world", 0}, @@ -168,6 +327,13 @@ int main(int argc, char * argv[]) try_status(km_core_process_event(test_state, KM_CORE_VKEY_L, KM_CORE_MODIFIER_SHIFT, 1, KM_CORE_EVENT_FLAG_DEFAULT)); test_assert(action_items(test_state, {{KM_CORE_IT_CHAR, {0,}, {km_core_usv('L')}}, {KM_CORE_IT_END}})); + + // Without the calling `km_core_state_context_set_if_needed` action struct has a delete for 'L'? + // Issue raised to investigate further: https://github.com/keymanapp/keyman/issues/15962 + km_core_cu const *state_context = get_context_as_string(km_core_state_context(test_state)); + km_core_state_context_set_if_needed(test_state, state_context); + delete [] state_context; + try_status(km_core_process_event(test_state, KM_CORE_VKEY_F2, 0, 1, KM_CORE_EVENT_FLAG_DEFAULT)); km_core_action_item action = {KM_CORE_IT_PERSIST_OPT, {0,}, }; @@ -187,11 +353,51 @@ int main(int argc, char * argv[]) if (doc1 != doc1_expected) return __LINE__; if (doc2 != doc2_expected) return __LINE__; - // Destroy them - km_core_state_dispose(test_state); - km_core_state_dispose(test_clone); - km_core_keyboard_dispose(test_kb); + // Test the action_struct values for the active and cloned states are + // independent and match their respective expected values. + const unsigned int expected_state_code_points_to_delete = 0; + const km_core_usv * expected_state_output = U""; + const km_core_bool expected_state_do_alert = KM_CORE_FALSE; + const km_core_bool expected_state_emit_keystroke = KM_CORE_FALSE; + const km_core_caps_state expected_state_new_caps_lock_state = KM_CORE_CAPS_UNCHANGED; + km_core_option_item expected_options[] = {expected_persist_opt, KM_CORE_OPTIONS_END }; + const km_core_usv * expected_deleted_text = U""; + // Cloned expected values + const unsigned int clone_state_code_points_to_delete = 0; + const km_core_usv * clone_state_output = U""; + const km_core_bool clone_state_do_alert = KM_CORE_FALSE; + const km_core_bool clone_state_emit_keystroke = KM_CORE_FALSE; + const km_core_caps_state clone_state_new_caps_lock_state = KM_CORE_CAPS_UNCHANGED; + km_core_option_item clone_state_options[] = {KM_CORE_OPTIONS_END}; + const km_core_usv * clone_state_deleted_text = nullptr; + const auto & state_actions = test_state->action_struct(); + const auto & clone_actions = test_clone->action_struct(); + + test_assert (expect_action_struct(state_actions, + expected_state_code_points_to_delete, + expected_state_output, + expected_options, + expected_state_do_alert, + expected_state_emit_keystroke, + expected_state_new_caps_lock_state, + expected_deleted_text + )); + + test_assert (expect_action_struct(clone_actions, + clone_state_code_points_to_delete, + clone_state_output, + clone_state_options, + clone_state_do_alert, + clone_state_emit_keystroke, + clone_state_new_caps_lock_state, + clone_state_deleted_text + )); + + // Destroy them + km_core_state_dispose(test_state); + km_core_state_dispose(test_clone); + km_core_keyboard_dispose(test_kb); return 0; } diff --git a/developer/src/server/package.json b/developer/src/server/package.json index 72093eaad6..662be2add6 100644 --- a/developer/src/server/package.json +++ b/developer/src/server/package.json @@ -19,7 +19,7 @@ "restructure": "^3.0.1", "sax": ">=0.6.0", "semver": "^7.5.4", - "ws": "^8.17.1", + "ws": "^8.20.1", "xmlbuilder": "~11.0.0" }, "optionalDependencies": { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeymanHosts.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanHosts.swift index 897b162a5e..65ce860dcc 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeymanHosts.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanHosts.swift @@ -16,7 +16,7 @@ import Foundation */ public enum KeymanHosts { /** - * Used to enable '.local' variants of the endpoints for use in local development testing. + * Used to enable '.localhost' variants of the endpoints for use in local development testing. */ internal static let useLocal = false @@ -24,7 +24,7 @@ public enum KeymanHosts { // save for use in automated testing. internal static func getApiSiteURL(forTier: Version.Tier, useLocal: Bool) -> URL { if useLocal { - return URL.init(string: "http://api.keyman.com.local")! + return URL.init(string: "http://api.keyman.com.localhost")! } else { switch forTier { case .alpha: @@ -48,7 +48,7 @@ public enum KeymanHosts { // save for use in automated testing. internal static func getHelpSiteURL(forTier: Version.Tier, useLocal: Bool) -> URL { if useLocal { - return URL.init(string: "http://help.keyman.com.local")! + return URL.init(string: "http://help.keyman.com.localhost")! } else { switch forTier { case .alpha: @@ -72,7 +72,7 @@ public enum KeymanHosts { // save for use in automated testing. internal static func getMainSiteURL(forTier: Version.Tier, useLocal: Bool) -> URL { if useLocal { - return URL.init(string: "http://keyman.com.local")! + return URL.init(string: "http://keyman.com.localhost")! } else { switch forTier { case .alpha: diff --git a/ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift b/ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift index 842592e69f..e179de30e6 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/UniversalLinks.swift @@ -25,11 +25,11 @@ public class UniversalLinks { public static var externalLinkLauncher: ((URL) -> Void)? = nil // e.g. https://keyman.com/keyboards/install/foo - private static let KEYBOARD_INSTALL_LINK_REGEX = try! NSRegularExpression(pattern: "^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.local)?/keyboards/install/([^?/]+)(?:\\?(.+))?$") - // e.g. http://keyman.com.local/keyboards/foo - private static let KEYBOARD_MATCH_ROOT_REGEX = try! NSRegularExpression(pattern: "^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.local)?/keyboards([/?].*)?$"); + private static let KEYBOARD_INSTALL_LINK_REGEX = try! NSRegularExpression(pattern: "^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/keyboards/install/([^?/]+)(?:\\?(.+))?$") + // e.g. http://keyman.com.localhost/keyboards/foo + private static let KEYBOARD_MATCH_ROOT_REGEX = try! NSRegularExpression(pattern: "^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/keyboards([/?].*)?$"); // e.g. https://keyman-staging.com/go/windows/14.0/download-keyboards?version=14.0.146.0 - private static let KEYBOARD_MATCH_GO_REGEX = try! NSRegularExpression(pattern: "^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.local)?/go/windows/[^/]+/download-keyboards") + private static let KEYBOARD_MATCH_GO_REGEX = try! NSRegularExpression(pattern: "^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/go/windows/[^/]+/download-keyboards") public static func tryParseKeyboardInstallLink(_ link: URL) -> ParsedKeyboardInstallLink? { let linkString = link.absoluteString diff --git a/ios/engine/KMEI/KeymanEngineTests/KeymanHostTests.swift b/ios/engine/KMEI/KeymanEngineTests/KeymanHostTests.swift index 83cc6c9582..6ce6722835 100644 --- a/ios/engine/KMEI/KeymanEngineTests/KeymanHostTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/KeymanHostTests.swift @@ -18,14 +18,14 @@ class KeymanHostTests: XCTestCase { } /** - * Ensures no accidental permanent edits to the .local variant URLs occur. + * Ensures no accidental permanent edits to the .localhost variant URLs occur. */ func testLocalSitesUnchanged() { XCTAssertEqual(KeymanHosts.getApiSiteURL(forTier: .stable, useLocal: true), - URL.init(string: "http://api.keyman.com.local")) + URL.init(string: "http://api.keyman.com.localhost")) XCTAssertEqual(KeymanHosts.getHelpSiteURL(forTier: .stable, useLocal: true), - URL.init(string: "http://help.keyman.com.local")) + URL.init(string: "http://help.keyman.com.localhost")) XCTAssertEqual(KeymanHosts.getMainSiteURL(forTier: .stable, useLocal: true), - URL.init(string: "http://keyman.com.local")) + URL.init(string: "http://keyman.com.localhost")) } } diff --git a/linux/scripts/launchpad.sh b/linux/scripts/launchpad.sh index 2cb397e47d..b516d1eb53 100755 --- a/linux/scripts/launchpad.sh +++ b/linux/scripts/launchpad.sh @@ -1,6 +1,8 @@ #!/usr/bin/env bash # Build source packages from nightly builds and upload to PPA +# shellcheck disable=SC2310 # -e will be disabled in if + ## START STANDARD BUILD SCRIPT INCLUDE # adjust relative paths as necessary THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" @@ -18,6 +20,7 @@ builder_describe \ "--upload Upload to launchpad." \ "--simulate Simulate the upload to launchpad." \ "--no-lintian Don't run lintian while creating source package." \ + "--no-sign Don't sign the source package." \ "--dist=DIST Only upload this distribution. Default: upload all supported dists." \ "--packageversion=PACKAGEVERSION String to append to the package version. Default: '1~sil1'." \ "--outputdir=OUTPUTDIR Directory for resulting artifacts. Default: \$KEYMAN_ROOT/linux/launchpad." \ @@ -39,10 +42,13 @@ else SIM="" fi +DEBUILD_OPTS=() if builder_has_option --no-lintian; then - LINTIAN_OPTS="--no-lintian" -else - LINTIAN_OPTS="" + DEBUILD_OPTS+=("--no-lintian") +fi + +if builder_has_option --no-sign; then + DEBUILD_OPTS+=("--no-sign") fi if [[ "${KEYMAN_TIER}" == "stable" ]]; then @@ -115,8 +121,9 @@ for dist in ${distributions}; do cp "../keyman-changelog" debian/changelog dch -v "${version}-${packageversion}~${dist}" "source package for PPA" dch -D "${dist}" -r "" - # shellcheck disable=SC2248 # no quotes for $LINTIAN_OPTS - might be empty string - debuild ${LINTIAN_OPTS} -d -S -sa -Zxz + # According to the docs, -S is equivalent to --build=source, but that causes debuild to fail. + # There is no long option for -sa (passed to dpkg-genchanges) + debuild "${DEBUILD_OPTS[@]}" --no-check-builddeps --compression=xz -S -sa done if builder_has_option --upload || builder_has_option --simulate; then cd .. diff --git a/linux/scripts/verify_source.sh b/linux/scripts/verify_source.sh index a67a9ebfdd..bec62599f5 100755 --- a/linux/scripts/verify_source.sh +++ b/linux/scripts/verify_source.sh @@ -11,6 +11,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "${THIS_SCRIPT%/*}/../../resources/build/builder-full.inc.sh" ## END STANDARD BUILD SCRIPT INCLUDE +. "${KEYMAN_ROOT}/resources/locate_emscripten.inc.sh" + builder_describe \ "Verify source tarball and source package" \ build \ @@ -81,7 +83,7 @@ create_source_package() { cp -r "${KEYMAN_ROOT}/linux/debian" "keyman-${KEYMAN_VERSION}" cp "${target_dir}/keyman_${KEYMAN_VERSION}.pkg.tar.xz" "keyman_${KEYMAN_VERSION}.orig.tar.xz" "keyman-${KEYMAN_VERSION}/linux/scripts/launchpad.sh" --no-download \ - --dist "$(lsb_release -c -s)" --outputdir "${target_dir}/launchpad" --no-lintian + --dist "$(lsb_release -c -s)" --outputdir "${target_dir}/launchpad" --no-lintian --no-sign } verify_lintian() { @@ -89,6 +91,18 @@ verify_lintian() { lintian "keyman_${KEYMAN_VERSION}"*source.changes } +install_emscripten() { + local target_dir="$1" + if [[ ! -d "${target_dir}/emsdk" ]]; then + builder_echo heading "Installing Emscripten for build verification" + install_emscripten_into "${target_dir}/emsdk" + else + builder_echo heading "Emscripten already exists in ${target_dir}/emsdk, skipping installation" + fi + EMSCRIPTEN_BASE="${target_dir}/emsdk/upstream/emscripten" + export EMSCRIPTEN_BASE +} + cd "${KEYMAN_ROOT}/linux" if ! builder_has_option --no-create-tarball; then @@ -100,6 +114,9 @@ fi if ! builder_has_option --launchpad-only; then builder_echo start verifySource "Verifying source tarball" extract_source_tarball "${TARGET_DIR}" + if builder_is_ci_test_build; then + install_emscripten "${TARGET_DIR}" + fi verify_can_build "${TARGET_DIR}/keyman" rm -rf "${TARGET_DIR}/keyman" builder_echo end verifySource success "Finished verifying source tarball" diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m index 4b5ce44dea..93b5b1eef9 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m @@ -22,15 +22,15 @@ - (void)windowDidLoad { [super windowDidLoad]; - + [self.webView setFrameLoadDelegate:(id)self]; [self.webView setGroupName:@"KMDownloadKB"]; [self.webView setPolicyDelegate:(id)self]; - + NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]; KeymanVersionInfo keymanVersionInfo = [[self AppDelegate] versionInfo]; NSString *url = [NSString stringWithFormat:@"https://%@/go/macos/14.0/download-keyboards/?version=%@", keymanVersionInfo.keymanCom, version]; - + os_log_debug([KMLogs uiLog], "KMDownloadKBWindowController opening url = %@, version = '%@'", url, version); [self.webView.mainFrame loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:url]]]; } @@ -45,21 +45,21 @@ os_log_debug([KMLogs uiLog], "decidePolicyForNavigationAction, navigating to %@", url); // The pattern for matching links matches work in #3602 - NSString* urlPathMatchKeyboardsInstall = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.local)?/keyboards/install/([^?/]+)(?:\\?(.+))?$"; + NSString* urlPathMatchKeyboardsInstall = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/keyboards/install/([^?/]+)(?:\\?(.+))?$"; // e.g. https://keyman.com/keyboards/install/foo - NSString* urlPathMatchKeyboardsRoot = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.local)?/keyboards([/?].*)?$"; - // http://keyman.com.local/keyboards/foo - NSString* urlPathMatchKeyboardsGo = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.local)?/go/macos/[^/]+/download-keyboards"; + NSString* urlPathMatchKeyboardsRoot = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/keyboards([/?].*)?$"; + // http://keyman.com.localhost/keyboards/foo + NSString* urlPathMatchKeyboardsGo = @"^http(?:s)?://keyman(?:-staging)?\\.com(?:\\.localhost)?/go/macos/[^/]+/download-keyboards"; // https://keyman-staging.com/go/macos/14.0/download-keyboards?version=14.0.146.0 NSRange range = NSMakeRange(0, url.length); - + NSError* error; NSRegularExpression* regexInstall = [NSRegularExpression regularExpressionWithPattern: urlPathMatchKeyboardsInstall options: 0 error: &error]; NSRegularExpression* regexRoot = [NSRegularExpression regularExpressionWithPattern: urlPathMatchKeyboardsRoot options: 0 error: &error]; NSRegularExpression* regexGo = [NSRegularExpression regularExpressionWithPattern: urlPathMatchKeyboardsGo options: 0 error: &error]; - + NSArray* matchesInstall = [regexInstall matchesInString:url options:0 range:range]; - + if(matchesInstall.count > 0) { os_log_debug([KMLogs uiLog], "Delegating download to app delegate."); [listener ignore]; diff --git a/package-lock.json b/package-lock.json index ef2ca5f0b5..9f934b6bb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1139,7 +1139,7 @@ "restructure": "^3.0.1", "sax": ">=0.6.0", "semver": "^7.5.4", - "ws": "^8.17.1", + "ws": "^8.20.1", "xmlbuilder": "~11.0.0" }, "devDependencies": { @@ -1186,6 +1186,27 @@ "url": "https://opencollective.com/express" } }, + "developer/src/server/node_modules/ws": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", + "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@75lb/deep-merge": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@75lb/deep-merge/-/deep-merge-1.1.2.tgz", @@ -13680,6 +13701,7 @@ "version": "8.18.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", + "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/resources/build/ci/npm-publish.sh b/resources/build/ci/npm-publish.sh index e9b522e2aa..1e32e2e43a 100755 --- a/resources/build/ci/npm-publish.sh +++ b/resources/build/ci/npm-publish.sh @@ -24,6 +24,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")" . "$KEYMAN_ROOT/resources/build/ci/ci-publish.inc.sh" . "$KEYMAN_ROOT/resources/build/ci/npm-packages.inc.sh" . "$KEYMAN_ROOT/resources/build/minimum-versions.inc.sh" +. "$KEYMAN_ROOT/resources/locate_emscripten.inc.sh" builder_describe \ "Publish @keymanapp packages to NPM" \ @@ -107,17 +108,10 @@ function install_emscripten() { local EMSDK_TEMP EMSDK_TEMP=$(mktemp -d) - pushd "${EMSDK_TEMP}" - git clone https://github.com/emscripten-core/emsdk.git - cd emsdk - ./emsdk install "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" - ./emsdk activate "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" - cd upstream/emscripten - npm install - echo "EMSCRIPTEN_BASE=$(pwd)" >> $GITHUB_ENV - EMSCRIPTEN_BASE="$(pwd)" + install_emscripten_into "${EMSDK_TEMP}" + EMSCRIPTEN_BASE="${EMSDK_TEMP}/upstream/emscripten" export EMSCRIPTEN_BASE - popd + echo "EMSCRIPTEN_BASE=${EMSCRIPTEN_BASE}" >> "${GITHUB_ENV}" } function install_meson() { diff --git a/resources/build/zip.inc.sh b/resources/build/zip.inc.sh index 8ab4aa77fc..5748bc6ab2 100644 --- a/resources/build/zip.inc.sh +++ b/resources/build/zip.inc.sh @@ -124,7 +124,12 @@ function add_zip_files() { if [[ -z "${SEVENZ+x}" ]]; then if builder_is_windows; then if [[ -z "${SEVENZ_HOME+x}" ]]; then - SEVENZ="$(command -v 7z.exe)" + SEVENZ="$(command -v 7z.exe || true)" + if [[ -z "${SEVENZ}" ]]; then + builder_die "7z.exe not found on path. Please install 7-Zip " \ + "and ensure 7z.exe is on the path or set SEVENZ_HOME " \ + "environment variable to the folder containing 7z.exe." + fi else SEVENZ="${SEVENZ_HOME}/7z.exe" fi diff --git a/resources/locate_emscripten.inc.sh b/resources/locate_emscripten.inc.sh index 50c8534ef4..d212b7a427 100644 --- a/resources/locate_emscripten.inc.sh +++ b/resources/locate_emscripten.inc.sh @@ -38,7 +38,7 @@ locate_emscripten() { if [[ -z "${EMSCRIPTEN_BASE:-}" ]]; then if [[ -z "${EMCC:-}" ]]; then local EMCC - EMCC="$(command -v "${EMCC_EXECUTABLE}")" + EMCC="$(command -v "${EMCC_EXECUTABLE}" || true)" [[ -z "${EMCC}" ]] && builder_die "locate_emscripten: Could not locate emscripten (${EMCC_EXECUTABLE}) on the path or with \$EMCC or \$EMSCRIPTEN_BASE" fi [[ -f "${EMCC}" && ! -x "${EMCC}" ]] && builder_die "locate_emscripten: Variable EMCC (${EMCC}) points to ${EMCC_EXECUTABLE} but it is not executable" @@ -117,3 +117,22 @@ _select_emscripten_version_with_emsdk() { fi ) } + +install_emscripten_into() { + if [[ -z "${1:-}" ]]; then + builder_die "${FUNCNAME[0]} requires a directory argument" + fi + + local EMSDK_DIR=$1 + builder_heading "Installing emscripten into ${EMSDK_DIR}" + + mkdir -p "${EMSDK_DIR}" + ( + cd "${EMSDK_DIR}" + git clone https://github.com/emscripten-core/emsdk.git . + ./emsdk install "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" + ./emsdk activate "${KEYMAN_MIN_VERSION_EMSCRIPTEN}" + cd upstream/emscripten + npm install + ) +}