diff --git a/.github/workflows/msvc.yml b/.github/workflows/msvc.yml index c8eaaa65b..d94420015 100644 --- a/.github/workflows/msvc.yml +++ b/.github/workflows/msvc.yml @@ -58,7 +58,7 @@ jobs: -DCMAKE_INSTALL_PREFIX:PATH="${{ github.workspace }}\dist\opencc-msvc-${{ matrix.package_suffix }}" cmake --build build-release --config Release --target install - - name: Build ARM64 compile-only targets + - name: Build ARM64 targets (data generation is skipped automatically) if: ${{ !matrix.can_run_binaries }} shell: pwsh run: | @@ -66,7 +66,7 @@ jobs: -A "${{ matrix.cmake_arch }}" ` -DCMAKE_BUILD_TYPE=Release ` -DBUILD_OPENCC_JIEBA_PLUGIN=ON - cmake --build build-release --config Release --target libopencc opencc opencc_dict opencc_phrase_extract opencc_jieba + cmake --build build-release --config Release - name: Build and test debug if: ${{ matrix.can_run_binaries }} diff --git a/CMakeLists.txt b/CMakeLists.txt index a74e1e283..08ffae27f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,105 @@ option(BUILD_SHARED_LIBS "Build opencc as shared library" ${_OPENCC_DEFAULT_BUIL option(OPENCC_ENABLE_INSTALL "Generate install targets" ${_OPENCC_DEFAULT_ENABLE_INSTALL}) option(ENABLE_GTEST "Build all tests." OFF) option(ENABLE_BENCHMARK "Build benchmark tests." OFF) +option(OPENCC_BUILD_TOOLS "Build OpenCC command-line tools" ON) + +# Host-built tool executables used to generate the dictionaries when the +# host cannot execute the just-built target tools (cross compiling without +# emulation). Dictionaries are architecture-independent data, so generating +# them with host tools produces the same result as generating them on the +# target. Both tools must be built from the same OpenCC source tree as this +# build: dictionaries are generated by opencc_dict and the ST phrase +# generator executes the opencc CLI, and other versions may produce +# inconsistent dictionaries or fail halfway. +set(OPENCC_DICT_EXECUTABLE "" CACHE FILEPATH "Path to a host-built opencc_dict executable used to generate dictionaries when cross compiling; must be built from the same OpenCC source version as this build") +set(OPENCC_CLI_EXECUTABLE "" CACHE FILEPATH "Path to a host-built opencc CLI executable used to generate dictionaries when cross compiling; must be built from the same OpenCC source version as this build") + +# Building the dictionaries executes the just-built target tools (opencc_dict +# and the opencc CLI) at build time. When the host cannot execute them and no +# host tools are provided, default to skipping the data build instead of +# failing mid-build. The default can be overridden explicitly, e.g. when the +# host can emulate the target (Rosetta 2). +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/OpenCCRunnability.cmake") + +if(OPENCC_DICT_EXECUTABLE OR OPENCC_CLI_EXECUTABLE) + if(NOT OPENCC_DICT_EXECUTABLE OR NOT OPENCC_CLI_EXECUTABLE) + message(FATAL_ERROR + "OPENCC_DICT_EXECUTABLE and OPENCC_CLI_EXECUTABLE must be provided " + "together: building the dictionaries runs both the opencc_dict tool " + "and the opencc CLI.") + endif() + foreach(_opencc_host_tool_var OPENCC_DICT_EXECUTABLE OPENCC_CLI_EXECUTABLE) + if(NOT EXISTS "${${_opencc_host_tool_var}}") + message(FATAL_ERROR + "${_opencc_host_tool_var} does not exist: ${${_opencc_host_tool_var}}") + endif() + endforeach() + # The host tools must at least be runnable on this host (existence above + # says nothing about their dynamic libraries); probe opencc_dict --help + # like the standalone Jieba plugin build does for its installed tool. + opencc_host_tool_command(_opencc_host_dict_command "${OPENCC_DICT_EXECUTABLE}") + execute_process( + COMMAND ${_opencc_host_dict_command} --help + RESULT_VARIABLE _opencc_host_dict_help_result + OUTPUT_VARIABLE _opencc_host_dict_help + ERROR_VARIABLE _opencc_host_dict_help_err + ) + if(NOT _opencc_host_dict_help_result EQUAL 0) + message(FATAL_ERROR + "Failed to run host tool ${OPENCC_DICT_EXECUTABLE} (exit status " + "${_opencc_host_dict_help_result}): ${_opencc_host_dict_help_err}") + endif() + set(OPENCC_USING_HOST_TOOLS ON) +else() + set(OPENCC_USING_HOST_TOOLS OFF) +endif() + +# Whether OPENCC_BUILD_DATA was provided explicitly — a plain variable +# already defined (a parent project setting it before add_subdirectory(), +# which CMP0077 option() semantics would honor) or a cache entry (-D or a +# previous configure) — so that the warning only fires when the default was +# flipped automatically rather than chosen by the user. When not provided, +# the value stays a plain variable recomputed on every configure: caching it +# would make the first automatic OFF look user-provided on later configures +# (e.g. after host tools are added) and silently ignore them. +get_property(_OPENCC_BUILD_DATA_CACHED CACHE OPENCC_BUILD_DATA PROPERTY TYPE SET) +if(DEFINED OPENCC_BUILD_DATA OR _OPENCC_BUILD_DATA_CACHED) + set(_OPENCC_BUILD_DATA_PROVIDED ON) +else() + set(_OPENCC_BUILD_DATA_PROVIDED OFF) +endif() +set(_OPENCC_BUILD_DATA_AUTO_OFF OFF) +if(NOT _OPENCC_BUILD_DATA_PROVIDED) + if(NOT OPENCC_CAN_RUN_TARGET_TOOLS + AND NOT OPENCC_DICT_EXECUTABLE AND NOT OPENCC_CLI_EXECUTABLE) + set(OPENCC_BUILD_DATA OFF) + set(_OPENCC_BUILD_DATA_AUTO_OFF ON) + else() + set(OPENCC_BUILD_DATA ON) + endif() +endif() +if(_OPENCC_BUILD_DATA_AUTO_OFF) + message(WARNING + "Cross compiling for an architecture the host cannot execute: " + "OPENCC_BUILD_DATA defaulted to OFF, so no dictionaries or config " + "files will be built or installed. Provide host-built tools via " + "OPENCC_DICT_EXECUTABLE and OPENCC_CLI_EXECUTABLE to build them " + "anyway, or set OPENCC_BUILD_DATA=ON explicitly if the host can run " + "the target binaries (e.g. through Rosetta 2 or " + "CMAKE_CROSSCOMPILING_EMULATOR).") +endif() +if(OPENCC_USING_HOST_TOOLS AND _OPENCC_BUILD_DATA_PROVIDED AND NOT OPENCC_BUILD_DATA) + message(WARNING + "Host tools are provided (OPENCC_DICT_EXECUTABLE and " + "OPENCC_CLI_EXECUTABLE) but OPENCC_BUILD_DATA is explicitly OFF: they " + "will not be used and no dictionaries or config files will be built.") +endif() +if(OPENCC_BUILD_DATA AND NOT OPENCC_BUILD_TOOLS AND NOT OPENCC_USING_HOST_TOOLS) + message(FATAL_ERROR + "OPENCC_BUILD_DATA requires OPENCC_BUILD_TOOLS, or host tools via " + "OPENCC_DICT_EXECUTABLE and OPENCC_CLI_EXECUTABLE.") +endif() + # Top-level macOS builds enable the Jieba plugin by default so package # managers that build with default options (e.g. Homebrew) ship it. Other # platforms and subproject builds (FetchContent / add_subdirectory) keep it @@ -383,13 +482,20 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/doc/CMakeLists.txt") elseif(BUILD_DOCUMENTATION) message(FATAL_ERROR "BUILD_DOCUMENTATION requires the doc source directory.") endif() -add_subdirectory(data) +if(OPENCC_BUILD_DATA) + add_subdirectory(data) +elseif(ENABLE_GTEST) + message(FATAL_ERROR "ENABLE_GTEST requires OPENCC_BUILD_DATA: the conversion tests run against the built dictionaries.") +endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/plugins/CMakeLists.txt") add_subdirectory(plugins) elseif(BUILD_OPENCC_JIEBA_PLUGIN) message(FATAL_ERROR "BUILD_OPENCC_JIEBA_PLUGIN requires the plugins source directory.") endif() if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/test/CMakeLists.txt") + if(ENABLE_GTEST AND NOT OPENCC_BUILD_TOOLS) + message(FATAL_ERROR "ENABLE_GTEST requires OPENCC_BUILD_TOOLS: the conversion tests execute the opencc CLI.") + endif() add_subdirectory(test) elseif(ENABLE_GTEST) message(FATAL_ERROR "ENABLE_GTEST requires the test source directory.") diff --git a/NEWS.md b/NEWS.md index a0613a6d6..db31000fc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -12,6 +12,9 @@ * 新增 Unicode 18.0 篆書區塊(Seal,U+3D000..U+3FC3F,共 11,328 字)與現代漢字的雙向對照。`SealCharacters.txt`(小篆 → 漢字)取自 UCD `SealSources.txt` 的 `kSEAL_MCJK` 屬性,反向詞典 `SealCharactersRev`(漢字 → 小篆)與 `TWVariantsRev` 等一樣於建置時生成。一個漢字對應多個小篆時(共 94 字),反向預設取碼位最小者(通常為《說文解字》正篆,重文在後),另有 4 字以 `@reverse-prefer` 改取版本來源(藤花榭本、陳昌治本、祁寯藻本《說文解字繫傳》、段注本)更完整者。 * 新增 `SealVariants.txt`(現代標準字 → 《說文》隸定字,約 1,000 字,如 `年 → 秊`、`前 → 歬`、`小 → 𡭔`),銜接 `kSEAL_MCJK` 的隸定字形;`t2seal` 先經此表再查 `SealCharactersRev`,`seal2t` 則先查 `SealCharacters` 再以建置時生成的 `SealVariantsRev` 還原為標準字。此表由 shuowen.org 開放數據(Apache-2.0)的字頭序列與 `kSEAL_MCJK` 對齊,並輔以人工審閱的 Unihan 異體資料整理而成。另,《說文》「玉」(U+3D06D)的 `kSEAL_MCJK` 為「王」,與「王」(U+3D068)衝突,詞典中改對應「玉」。 * 新增配置 `t2seal.json`(繁體漢字 → 小篆)、`seal2t.json`(小篆 → 繁體漢字)與 `s2seal.json`(簡體 → 小篆,即 `s2t` 後接 `t2seal`),已加入 CLI 說明、npm CLI 內建配置表與測試案例;僅供探索性研究,顯示需搭配支援 Unicode 18.0 篆書區塊的字型。 +* **交叉編譯**: + * 新增 `OPENCC_BUILD_TOOLS` 與 `OPENCC_BUILD_DATA` 選項(加 `OPENCC_` 前綴以免 FetchContent/`add_subdirectory` 場景繼承父專案的同名快取值),修復交叉編譯到宿主無法執行的目標架構(iOS、Android、Windows ARM64 等)時,因建置期執行剛編譯出的 `opencc_dict`/`opencc` 而必然失敗的問題([#615](https://github.com/BYVoid/OpenCC/issues/615))。宿主無法執行目標工具且未提供宿主工具時,`OPENCC_BUILD_DATA` 自動預設為 OFF 並輸出警告,僅產出程式庫與工具,且不安裝任何字典與配置 JSON——包括指向缺失字典的 jieba 配置(`*_jieba.json`);此自動預設值不寫入 cache,之後補傳宿主工具重新配置即恢復生成,明確設為 OFF 而又提供宿主工具時則另行警告。宿主可執行目標二進位的組態(如 x64 Windows 宿主建置 x86,含 Visual Studio `-A` 平台)維持生成字典;依賴 Rosetta 2 等模擬的組態可明確設定 `-DOPENCC_BUILD_DATA=ON`;設定 `CMAKE_CROSSCOMPILING_EMULATOR`(如 qemu)時 data 不自動關閉,建置期工具呼叫(含 `--opencc` 參數)自動冠上模擬器前綴。`ENABLE_GTEST` 現於 `OPENCC_BUILD_TOOLS` 或 `OPENCC_BUILD_DATA` 關閉時明確報錯(轉換測試需執行 opencc 並載入字典)。 + * 新增 `OPENCC_DICT_EXECUTABLE`/`OPENCC_CLI_EXECUTABLE` 快取變數,指向預先為宿主架構建置的工具(須與本次建置出自同一份 OpenCC 源碼版本,配置時以 `opencc_dict --help` 探測可執行性);交叉編譯時提供這兩個路徑即可照常生成並安裝完整字典與 jieba 合併詞典(字典屬架構無關資料,宿主生成與目標生成內容一致)。非 Windows 宿主上,安裝在非預設前綴的宿主工具以 `LD_LIBRARY_PATH`/`DYLD_LIBRARY_PATH` 包裹執行以找到同安裝前綴的 libopencc,data/ 與 jieba 插件共用同一包裹輔助函式。可執行性判斷與 jieba 插件既有的 `OPENCC_CAN_RUN_CPPJIEBA_DICT` 機制共用,上提至 `cmake/OpenCCRunnability.cmake`。 ## Version 1.4.2 diff --git a/cmake/OpenCCRunnability.cmake b/cmake/OpenCCRunnability.cmake new file mode 100644 index 000000000..99ad2c75c --- /dev/null +++ b/cmake/OpenCCRunnability.cmake @@ -0,0 +1,111 @@ +# Helpers to decide whether binaries built for the target architecture can +# be executed on the build host. The dictionary build (data/) and the Jieba +# plugin (plugins/jieba/) run the just-built tools at build time, so they +# need this to skip or redirect tool execution when cross compiling. +# +# Included from the top-level CMakeLists.txt (integrated builds) and from +# plugins/jieba/CMakeLists.txt (standalone plugin builds, with an inline +# fallback when the parent source tree is not available). + +function(opencc_normalize_arch output_var input_value) + string(TOUPPER "${input_value}" _arch) + if (_arch STREQUAL "AMD64" OR _arch STREQUAL "X86_64") + set(_arch "X64") + elseif (_arch STREQUAL "AARCH64") + set(_arch "ARM64") + elseif (_arch MATCHES "^I[3-6]86$" OR _arch STREQUAL "X86" OR + _arch STREQUAL "WIN32") + set(_arch "X86") + endif() + set(${output_var} "${_arch}" PARENT_SCOPE) +endfunction() + +function(opencc_can_run_target_on_host output_var host_arch target_arch) + set(_can_run FALSE) + if ("${host_arch}" STREQUAL "${target_arch}") + set(_can_run TRUE) + elseif ("${host_arch}" STREQUAL "X64" AND "${target_arch}" STREQUAL "X86") + # 32-bit Windows binaries run on x64 Windows hosts via WOW64. + set(_can_run TRUE) + endif() + set(${output_var} "${_can_run}" PARENT_SCOPE) +endfunction() + +# opencc_host_tool_env: the command prefix that sets up the environment for +# running a host-built tool — "" on a Windows host (the installed layout +# keeps the DLLs next to the executable), otherwise `cmake -E env` plus +# LD_LIBRARY_PATH/DYLD_LIBRARY_PATH pointing at the lib directory of the +# installation the tool comes from (two levels above the executable), for +# tools installed to a non-default prefix that carry no rpath. The env +# variable must be chosen for the HOST platform, not the target: the tool +# runs on the host (e.g. building for a Linux target from macOS still needs +# DYLD_LIBRARY_PATH for the host opencc_dict). Used to wrap whole build-time +# invocations whose tool path is passed on as an argument (the ST phrase +# generator's --opencc): a multi-token command cannot be carried through +# such an argument, but the spawned tool inherits the environment. +function(opencc_host_tool_env output_var tool_path) + if(CMAKE_HOST_WIN32) + set(${output_var} "" PARENT_SCOPE) + else() + get_filename_component(_opencc_tool_bindir "${tool_path}" DIRECTORY) + get_filename_component(_opencc_tool_prefix "${_opencc_tool_bindir}" DIRECTORY) + if(CMAKE_HOST_APPLE) + set(_opencc_tool_libpath + "DYLD_LIBRARY_PATH=${_opencc_tool_prefix}/lib:$ENV{DYLD_LIBRARY_PATH}") + else() + set(_opencc_tool_libpath + "LD_LIBRARY_PATH=${_opencc_tool_prefix}/lib:$ENV{LD_LIBRARY_PATH}") + endif() + set(${output_var} ${CMAKE_COMMAND} -E env "${_opencc_tool_libpath}" PARENT_SCOPE) + endif() +endfunction() + +# opencc_host_tool_command: a host-built tool executable wrapped into a +# ready-to-run command list (the env prefix above plus the tool itself). +function(opencc_host_tool_command output_var tool_path) + opencc_host_tool_env(_opencc_tool_env "${tool_path}") + set(${output_var} ${_opencc_tool_env} "${tool_path}" PARENT_SCOPE) +endfunction() + +# OPENCC_CAN_RUN_TARGET_TOOLS: TRUE when the host can plausibly execute +# binaries built for the target architecture. This covers native builds, +# x86 targets on x64 Windows hosts (including the Visual Studio generator, +# which does not set CMAKE_CROSSCOMPILING for -A platforms), and cross +# builds with CMAKE_CROSSCOMPILING_EMULATOR (e.g. qemu); emulation such as +# Rosetta 2 (x64 targets on Apple Silicon) is deliberately not detected — +# set OPENCC_BUILD_DATA explicitly when relying on it. +if(DEFINED CMAKE_VS_PLATFORM_NAME AND NOT CMAKE_VS_PLATFORM_NAME STREQUAL "") + set(_OPENCC_TARGET_ARCH_RAW "${CMAKE_VS_PLATFORM_NAME}") +else() + set(_OPENCC_TARGET_ARCH_RAW "${CMAKE_SYSTEM_PROCESSOR}") +endif() +opencc_normalize_arch(OPENCC_TARGET_ARCH "${_OPENCC_TARGET_ARCH_RAW}") +opencc_normalize_arch(OPENCC_HOST_ARCH "${CMAKE_HOST_SYSTEM_PROCESSOR}") + +set(OPENCC_CAN_RUN_TARGET_TOOLS TRUE) +if(CMAKE_CROSSCOMPILING) + set(OPENCC_CAN_RUN_TARGET_TOOLS FALSE) +endif() +# The host/target architecture comparison must stay outside the +# CMAKE_CROSSCOMPILING check: the Visual Studio generator builds -A ARM64 +# (or Win32) without setting CMAKE_CROSSCOMPILING, so the architectures are +# the only signal there. A mismatched architecture always downgrades to +# FALSE; a runnable one upgrades back to TRUE only on a Windows host, where +# the comparison means WOW64. Cross builds from other hosts to Windows +# (e.g. mingw triplets on Linux/macOS) produce PE binaries the host cannot +# execute even when the architectures match. +if(WIN32 AND OPENCC_TARGET_ARCH AND OPENCC_HOST_ARCH) + opencc_can_run_target_on_host(_opencc_can_run_target + "${OPENCC_HOST_ARCH}" + "${OPENCC_TARGET_ARCH}") + if(NOT _opencc_can_run_target) + set(OPENCC_CAN_RUN_TARGET_TOOLS FALSE) + elseif(CMAKE_HOST_WIN32) + set(OPENCC_CAN_RUN_TARGET_TOOLS TRUE) + endif() +endif() +# A cross-compiling emulator (e.g. qemu, set via CMAKE_CROSSCOMPILING_EMULATOR) +# lets the host execute target binaries as well. +if(CMAKE_CROSSCOMPILING_EMULATOR) + set(OPENCC_CAN_RUN_TARGET_TOOLS TRUE) +endif() diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 682b7930b..b445a370b 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -4,8 +4,52 @@ endif() find_package(Python3 REQUIRED COMPONENTS Interpreter) -set(OPENCC_DICT_BIN opencc_dict) -set(OPENCC_BIN opencc) +if(OPENCC_USING_HOST_TOOLS) + # Host-built tools provided via OPENCC_DICT_EXECUTABLE/OPENCC_CLI_EXECUTABLE + # at the top level: they run on the host, wrapped so their dynamic linker + # finds the libopencc they were built against. No build-tree copy + # dependency is needed on Windows. + set(OPENCC_DICT_BIN "${OPENCC_DICT_EXECUTABLE}") + set(OPENCC_BIN "${OPENCC_CLI_EXECUTABLE}") + opencc_host_tool_command(OPENCC_DICT_COMMAND "${OPENCC_DICT_EXECUTABLE}") + # The ST phrase generator takes the converter as its --opencc argument; + # an env wrapper cannot be carried through an argument, so the generator + # invocation itself gets the env prefix below and the spawned CLI + # inherits it. + opencc_host_tool_env(DICT_GENERATOR_ENV "${OPENCC_CLI_EXECUTABLE}") + set(OPENCC_CLI_COMMAND "--opencc=${OPENCC_CLI_EXECUTABLE}") + set(DICT_WIN32_DEPENDS) +else() + set(OPENCC_DICT_BIN opencc_dict) + set(OPENCC_BIN opencc) + set(DICT_GENERATOR_ENV) + # $ generator expressions only resolve for targets; host + # tools above are plain paths. CMAKE_CROSSCOMPILING_EMULATOR (e.g. qemu) + # runs target binaries on the host; add_custom_command does not prepend it + # automatically, so the build-time invocations wrap the just-built tools + # explicitly — including the --opencc argument of the ST phrase generator. + if(CMAKE_CROSSCOMPILING_EMULATOR) + set(OPENCC_DICT_COMMAND + ${CMAKE_CROSSCOMPILING_EMULATOR} "$") + # The generator script takes the converter as repeated --opencc= + # options, so the emulator prefix (and the target path) each get their + # own option; a token may not be passed as a bare argument when it + # starts with "-". + set(OPENCC_CLI_COMMAND) + foreach(_OPENCC_CLI_TOKEN + ${CMAKE_CROSSCOMPILING_EMULATOR} "$") + list(APPEND OPENCC_CLI_COMMAND "--opencc=${_OPENCC_CLI_TOKEN}") + endforeach() + else() + set(OPENCC_DICT_COMMAND "$") + set(OPENCC_CLI_COMMAND "--opencc=$") + endif() + if(WIN32) + set(DICT_WIN32_DEPENDS copy_libopencc_to_dir_of_opencc_dict) + else() + set(DICT_WIN32_DEPENDS) + endif() +endif() set(DICT_REVERSE_BIN "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/reverse.py") set(DICT_EXTRACT_TOFU_RISK_BIN "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/extract_tofu_risk.py") set(DICT_GENERATE_ST_PHRASES_BIN "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/generate_st_phrases_from_regional_phrases.py") @@ -102,11 +146,12 @@ set( ) set( DICT_STPhrases_GeneratedFromRegionalPhrases_GENERATING_COMMAND + ${DICT_GENERATOR_ENV} ${DICT_GENERATE_ST_PHRASES_BIN} --input ${DICT_DIR}/HKPhrases.txt --input ${DICT_DIR}/TWPhrases.txt --output STPhrases_GeneratedFromRegionalPhrases.txt - --opencc $ + ${OPENCC_CLI_COMMAND} --config ${CMAKE_CURRENT_SOURCE_DIR}/config/t2s.json --dict-dir ${DICT_GENERATED_DIR} ) @@ -156,19 +201,6 @@ set( ${DICT_REVERSE_BIN} ${DICT_SealVariantsRev_GENERATING_INPUT} SealVariantsRev.txt ) -add_custom_target( - copy_libopencc_to_dir_of_opencc_dict - COMMENT - "Copying libopencc to directory of opencc_dict" - COMMAND - ${CMAKE_COMMAND} -E copy "$" "$" -) -if (WIN32) - set(DICT_WIN32_DEPENDS copy_libopencc_to_dir_of_opencc_dict) -else() - set(DICT_WIN32_DEPENDS) -endif() - foreach(DICT ${DICTS_GENERATED}) add_custom_command( OUTPUT @@ -181,6 +213,7 @@ foreach(DICT ${DICTS_GENERATED}) ${DICT_WIN32_DEPENDS} ${OPENCC_BIN} ${DICT_${DICT}_GENERATING_INPUT} + VERBATIM ) set_directory_properties( PROPERTIES @@ -216,7 +249,7 @@ foreach(DICT ${DICTS}) COMMENT "Building ${DICT}.ocd2" COMMAND - ${OPENCC_DICT_BIN} + ${OPENCC_DICT_COMMAND} --input ${DICT_${DICT}_INPUT} --output ${DICT}.ocd2 --from text @@ -225,6 +258,7 @@ foreach(DICT ${DICTS}) ${DICT_WIN32_DEPENDS} ${OPENCC_DICT_BIN} ${DICT_${DICT}_INPUT} + VERBATIM ) if(OPENCC_ENABLE_INSTALL AND OPENCC_DICT_FORMAT STREQUAL "ocd2") @@ -260,7 +294,7 @@ if(OPENCC_DICT_FORMAT STREQUAL "ocd") COMMENT "Building ${DICT}.ocd" COMMAND - ${OPENCC_DICT_BIN} + ${OPENCC_DICT_COMMAND} --input ${DICT_${DICT}_INPUT} --output ${DICT}.ocd --from text @@ -269,6 +303,7 @@ if(OPENCC_DICT_FORMAT STREQUAL "ocd") ${DICT_WIN32_DEPENDS} ${OPENCC_DICT_BIN} ${DICT_${DICT}_INPUT} + VERBATIM ) if(OPENCC_ENABLE_INSTALL) diff --git a/data/scripts/generate_st_phrases_from_regional_phrases.py b/data/scripts/generate_st_phrases_from_regional_phrases.py index 24b0a0e5c..2ec31fb50 100644 --- a/data/scripts/generate_st_phrases_from_regional_phrases.py +++ b/data/scripts/generate_st_phrases_from_regional_phrases.py @@ -17,7 +17,13 @@ def parse_args(): parser.add_argument("--append-to") parser.add_argument("--output", required=True) backend = parser.add_mutually_exclusive_group(required=True) - backend.add_argument("--opencc") + # Repeated --opencc= options build the converter command prefix: + # a cross-compiling emulator (CMAKE_CROSSCOMPILING_EMULATOR) or an env + # wrapper for a host-built opencc installed to a non-default prefix. + # The tokens may start with "-" (e.g. "cmake -E env ..."), so they are + # passed as separate --opencc= options rather than one + # space-separated list, which the option parser would cut short. + backend.add_argument("--opencc", action="append") backend.add_argument("--node-binding") parser.add_argument("--config", required=True) parser.add_argument("--dict-dir", required=True) @@ -28,10 +34,10 @@ def clean_path_arg(path): return path.replace('"', "") -def convert_keys(opencc, config, dict_dir, keys): +def convert_keys(opencc_command, config, dict_dir, keys): result = subprocess.run( [ - opencc, + *opencc_command, "-c", config, "--path", @@ -106,7 +112,7 @@ def convert_keys_node(node_binding, config, dict_dir, keys): def main(): args = parse_args() if args.opencc: - args.opencc = clean_path_arg(args.opencc) + args.opencc = [clean_path_arg(part) for part in args.opencc] if args.node_binding: args.node_binding = clean_path_arg(args.node_binding) args.config = clean_path_arg(args.config) diff --git a/plugins/jieba/CMakeLists.txt b/plugins/jieba/CMakeLists.txt index aa5371bed..87d2d2591 100644 --- a/plugins/jieba/CMakeLists.txt +++ b/plugins/jieba/CMakeLists.txt @@ -102,53 +102,119 @@ target_compile_definitions(opencc_jieba PRIVATE OPENCC_PLUGIN_BUILD) set(JIEBA_MERGED_DICT_DIR "${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}/jieba_dict") set(JIEBA_MERGED_DICT "${JIEBA_MERGED_DICT_DIR}/jieba_merged.ocd2") -set(OPENCC_CAN_RUN_CPPJIEBA_DICT TRUE) -function(opencc_normalize_arch output_var input_value) - string(TOUPPER "${input_value}" _arch) - if (_arch STREQUAL "AMD64" OR _arch STREQUAL "X86_64") - set(_arch "X64") - elseif (_arch STREQUAL "AARCH64") - set(_arch "ARM64") - elseif (_arch MATCHES "^I[3-6]86$" OR _arch STREQUAL "X86" OR - _arch STREQUAL "WIN32") - set(_arch "X86") - endif() - set(${output_var} "${_arch}" PARENT_SCOPE) -endfunction() +# Whether the merged Jieba dictionary can be generated at build time: the +# generation executes opencc_dict on the host. Integrated builds that were +# given host tools (OPENCC_DICT_EXECUTABLE/OPENCC_CLI_EXECUTABLE at the top +# level) can always run the host tool. Otherwise the helper shared with the +# top-level data build decides whether the just-built (integrated mode) or +# installed (standalone mode) opencc_dict can actually run on this host. A +# standalone plugin build without the parent source tree falls back to an +# inline copy kept in sync with cmake/OpenCCRunnability.cmake. +set(_opencc_jieba_using_host_tools OFF) +if (NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR AND OPENCC_USING_HOST_TOOLS) + set(_opencc_jieba_using_host_tools ON) + set(OPENCC_CAN_RUN_CPPJIEBA_DICT TRUE) +elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/OpenCCRunnability.cmake") + include("${CMAKE_CURRENT_SOURCE_DIR}/../../cmake/OpenCCRunnability.cmake") + set(OPENCC_CAN_RUN_CPPJIEBA_DICT ${OPENCC_CAN_RUN_TARGET_TOOLS}) +else() + function(opencc_normalize_arch output_var input_value) + string(TOUPPER "${input_value}" _arch) + if (_arch STREQUAL "AMD64" OR _arch STREQUAL "X86_64") + set(_arch "X64") + elseif (_arch STREQUAL "AARCH64") + set(_arch "ARM64") + elseif (_arch MATCHES "^I[3-6]86$" OR _arch STREQUAL "X86" OR + _arch STREQUAL "WIN32") + set(_arch "X86") + endif() + set(${output_var} "${_arch}" PARENT_SCOPE) + endfunction() -function(opencc_can_run_target_on_host output_var host_arch target_arch) - set(_can_run FALSE) - if ("${host_arch}" STREQUAL "${target_arch}") - set(_can_run TRUE) - elseif ("${host_arch}" STREQUAL "X64" AND "${target_arch}" STREQUAL "X86") - set(_can_run TRUE) - endif() - set(${output_var} "${_can_run}" PARENT_SCOPE) -endfunction() + function(opencc_can_run_target_on_host output_var host_arch target_arch) + set(_can_run FALSE) + if ("${host_arch}" STREQUAL "${target_arch}") + set(_can_run TRUE) + elseif ("${host_arch}" STREQUAL "X64" AND "${target_arch}" STREQUAL "X86") + set(_can_run TRUE) + endif() + set(${output_var} "${_can_run}" PARENT_SCOPE) + endfunction() -if (DEFINED CMAKE_VS_PLATFORM_NAME AND NOT CMAKE_VS_PLATFORM_NAME STREQUAL "") - set(_opencc_target_arch "${CMAKE_VS_PLATFORM_NAME}") -else() - set(_opencc_target_arch "${CMAKE_SYSTEM_PROCESSOR}") -endif() -set(_opencc_host_arch "${CMAKE_HOST_SYSTEM_PROCESSOR}") -opencc_normalize_arch(_opencc_target_arch "${_opencc_target_arch}") -opencc_normalize_arch(_opencc_host_arch "${_opencc_host_arch}") + function(opencc_host_tool_env output_var tool_path) + if (CMAKE_HOST_WIN32) + set(${output_var} "" PARENT_SCOPE) + else() + get_filename_component(_opencc_tool_bindir "${tool_path}" DIRECTORY) + get_filename_component(_opencc_tool_prefix + "${_opencc_tool_bindir}" DIRECTORY) + if (CMAKE_HOST_APPLE) + set(_opencc_tool_libpath + "DYLD_LIBRARY_PATH=${_opencc_tool_prefix}/lib:$ENV{DYLD_LIBRARY_PATH}") + else() + set(_opencc_tool_libpath + "LD_LIBRARY_PATH=${_opencc_tool_prefix}/lib:$ENV{LD_LIBRARY_PATH}") + endif() + set(${output_var} + ${CMAKE_COMMAND} -E env "${_opencc_tool_libpath}" PARENT_SCOPE) + endif() + endfunction() -if (CMAKE_CROSSCOMPILING) - set(OPENCC_CAN_RUN_CPPJIEBA_DICT FALSE) -endif() -if (WIN32 AND _opencc_target_arch AND _opencc_host_arch) - opencc_can_run_target_on_host(_opencc_can_run_target "${_opencc_host_arch}" - "${_opencc_target_arch}") - if (NOT _opencc_can_run_target) + function(opencc_host_tool_command output_var tool_path) + opencc_host_tool_env(_opencc_tool_env "${tool_path}") + set(${output_var} ${_opencc_tool_env} "${tool_path}" PARENT_SCOPE) + endfunction() + + set(OPENCC_CAN_RUN_CPPJIEBA_DICT TRUE) + if (DEFINED CMAKE_VS_PLATFORM_NAME AND NOT CMAKE_VS_PLATFORM_NAME STREQUAL "") + set(_opencc_target_arch "${CMAKE_VS_PLATFORM_NAME}") + else() + set(_opencc_target_arch "${CMAKE_SYSTEM_PROCESSOR}") + endif() + set(_opencc_host_arch "${CMAKE_HOST_SYSTEM_PROCESSOR}") + opencc_normalize_arch(_opencc_target_arch "${_opencc_target_arch}") + opencc_normalize_arch(_opencc_host_arch "${_opencc_host_arch}") + + if (CMAKE_CROSSCOMPILING) set(OPENCC_CAN_RUN_CPPJIEBA_DICT FALSE) endif() + if (WIN32 AND _opencc_target_arch AND _opencc_host_arch) + opencc_can_run_target_on_host(_opencc_can_run_target "${_opencc_host_arch}" + "${_opencc_target_arch}") + # Downgrade on a mismatched architecture; upgrade on a runnable one only + # on a Windows host, where the comparison means WOW64 (cross builds from + # other hosts to Windows produce PE binaries the host cannot execute + # even when the architectures match). + if (NOT _opencc_can_run_target) + set(OPENCC_CAN_RUN_CPPJIEBA_DICT FALSE) + elseif (CMAKE_HOST_WIN32) + set(OPENCC_CAN_RUN_CPPJIEBA_DICT TRUE) + endif() + endif() + if (CMAKE_CROSSCOMPILING_EMULATOR) + set(OPENCC_CAN_RUN_CPPJIEBA_DICT TRUE) + endif() +endif() + +if (OPENCC_CAN_RUN_CPPJIEBA_DICT AND + NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR AND + NOT OPENCC_BUILD_TOOLS AND + NOT _opencc_jieba_using_host_tools) + message(FATAL_ERROR + "Building the merged Jieba dictionary requires the opencc_dict build " + "tool: set OPENCC_BUILD_TOOLS=ON or provide host tools via " + "OPENCC_DICT_EXECUTABLE and OPENCC_CLI_EXECUTABLE.") endif() if (OPENCC_CAN_RUN_CPPJIEBA_DICT) - if (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + if (_opencc_jieba_using_host_tools) + # Integrated build with host tools: run the host opencc_dict instead of + # the just-built target tool, wrapped so its dynamic linker finds the + # libopencc it was built against (same helper as the data build). + opencc_host_tool_command(OPENCC_DICT_COMMAND "${OPENCC_DICT_EXECUTABLE}") + set(OPENCC_DICT_TARGET_DEPENDS) + elseif (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) # Standalone plugin build: use the opencc_dict installed alongside the # OpenCC package found by find_package. Merging the bundled Jieba # dictionaries requires the cppjieba_utf8 input format (OpenCC >= 1.4.1). @@ -161,22 +227,7 @@ if (OPENCC_CAN_RUN_CPPJIEBA_DICT) endif() # An opencc_dict installed to a non-default prefix carries no rpath, so # help the dynamic linker find libopencc from the same installation. - get_filename_component(_opencc_dict_bindir - "${OPENCC_DICT_EXECUTABLE}" DIRECTORY) - get_filename_component(_opencc_dict_prefix - "${_opencc_dict_bindir}" DIRECTORY) - if (WIN32) - # The installed layout keeps opencc.dll next to opencc_dict.exe. - set(OPENCC_DICT_COMMAND "${OPENCC_DICT_EXECUTABLE}") - elseif (APPLE) - set(OPENCC_DICT_COMMAND ${CMAKE_COMMAND} -E env - "DYLD_LIBRARY_PATH=${_opencc_dict_prefix}/lib:$ENV{DYLD_LIBRARY_PATH}" - "${OPENCC_DICT_EXECUTABLE}") - else() - set(OPENCC_DICT_COMMAND ${CMAKE_COMMAND} -E env - "LD_LIBRARY_PATH=${_opencc_dict_prefix}/lib:$ENV{LD_LIBRARY_PATH}" - "${OPENCC_DICT_EXECUTABLE}") - endif() + opencc_host_tool_command(OPENCC_DICT_COMMAND "${OPENCC_DICT_EXECUTABLE}") execute_process( COMMAND ${OPENCC_DICT_COMMAND} --help RESULT_VARIABLE _opencc_dict_help_result @@ -196,16 +247,26 @@ if (OPENCC_CAN_RUN_CPPJIEBA_DICT) endif() set(OPENCC_DICT_TARGET_DEPENDS) else() - set(OPENCC_DICT_COMMAND opencc_dict) + # Just-built target tool. CMAKE_CROSSCOMPILING_EMULATOR (e.g. qemu) runs + # it on the host; add_custom_command does not prepend it automatically, + # so the invocation wraps the tool explicitly (same as the data build). + if (CMAKE_CROSSCOMPILING_EMULATOR) + set(OPENCC_DICT_COMMAND + ${CMAKE_CROSSCOMPILING_EMULATOR} "$") + else() + set(OPENCC_DICT_COMMAND opencc_dict) + endif() set(OPENCC_DICT_TARGET_DEPENDS opencc_dict) endif() endif() if (WIN32 AND OPENCC_CAN_RUN_CPPJIEBA_DICT AND - NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR AND + NOT _opencc_jieba_using_host_tools) # opencc_dict in the build tree needs libopencc next to it to run; data/ # already defines this copy target for its own dictionary generation. An - # installed opencc_dict (standalone build) ships next to its runtime. + # installed opencc_dict (standalone build) ships next to its runtime, and + # host tools (integrated build) run with their own libraries. set(JIEBA_MERGED_DICT_WIN32_DEPENDS copy_libopencc_to_dir_of_opencc_dict) else() set(JIEBA_MERGED_DICT_WIN32_DEPENDS) @@ -281,18 +342,25 @@ if (BUILD_OPENCC_JIEBA_PLUGIN AND OPENCC_ENABLE_INSTALL) endif() endif() - install( - FILES - ${CMAKE_CURRENT_SOURCE_DIR}/data/config/hk2sp_jieba.json - ${CMAKE_CURRENT_SOURCE_DIR}/data/config/s2hk_jieba.json - ${CMAKE_CURRENT_SOURCE_DIR}/data/config/s2hkp_jieba.json - ${CMAKE_CURRENT_SOURCE_DIR}/data/config/s2t_jieba.json - ${CMAKE_CURRENT_SOURCE_DIR}/data/config/s2tw_jieba.json - ${CMAKE_CURRENT_SOURCE_DIR}/data/config/s2twp_jieba.json - ${CMAKE_CURRENT_SOURCE_DIR}/data/config/tw2sp_jieba.json - DESTINATION - ${DIR_SHARE_OPENCC} - ) + # The *_jieba.json configs reference both the merged Jieba dictionary and + # the dictionaries built by data/ (or the installation, in standalone + # builds); do not install configs pointing at dictionaries that are not + # built (cross compiling without host tools, OPENCC_BUILD_DATA=OFF). + if (OPENCC_CAN_RUN_CPPJIEBA_DICT AND + (CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR OR OPENCC_BUILD_DATA)) + install( + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/data/config/hk2sp_jieba.json + ${CMAKE_CURRENT_SOURCE_DIR}/data/config/s2hk_jieba.json + ${CMAKE_CURRENT_SOURCE_DIR}/data/config/s2hkp_jieba.json + ${CMAKE_CURRENT_SOURCE_DIR}/data/config/s2t_jieba.json + ${CMAKE_CURRENT_SOURCE_DIR}/data/config/s2tw_jieba.json + ${CMAKE_CURRENT_SOURCE_DIR}/data/config/s2twp_jieba.json + ${CMAKE_CURRENT_SOURCE_DIR}/data/config/tw2sp_jieba.json + DESTINATION + ${DIR_SHARE_OPENCC} + ) + endif() install( FILES diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aaa028691..55f82269a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -263,4 +263,19 @@ endif() # Subdir -add_subdirectory(tools) +if(OPENCC_BUILD_TOOLS) + add_subdirectory(tools) + + # opencc_dict in the build tree needs libopencc next to it to run on + # Windows; data/ and plugins/jieba/ use this copy target for their + # build-time tool execution. + if(WIN32) + add_custom_target( + copy_libopencc_to_dir_of_opencc_dict + COMMENT + "Copying libopencc to directory of opencc_dict" + COMMAND + ${CMAKE_COMMAND} -E copy "$" "$" + ) + endif() +endif() diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index b51d353f2..011fdc306 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -63,5 +63,8 @@ if(OPENCC_ENABLE_INSTALL) install( TARGETS opencc opencc_dict opencc_phrase_extract RUNTIME DESTINATION bin + # Executables built for iOS are MACOSX_BUNDLE targets and install through + # the BUNDLE destination instead of RUNTIME. + BUNDLE DESTINATION bin ) endif()