From 486d96cd63e5f371d8d2735470632dbbe858e4d5 Mon Sep 17 00:00:00 2001 From: ghostflyby Date: Wed, 30 Sep 2026 00:23:30 +0800 Subject: [PATCH 1/3] Add cross-compilation support for tools and data builds Building the dictionaries executes the just-built opencc_dict and opencc tools at build time, so configuring for a target architecture the host cannot execute (iOS, Android, Windows ARM64 on x64, ...) always failed mid-build. Add BUILD_TOOLS/BUILD_DATA options and host-tool overrides so cross builds either skip data generation automatically or generate the architecture-independent dictionaries with host-built tools, as proposed in #615. Detailed Changes: **Build options** - BUILD_TOOLS (default ON) gates src/tools; BUILD_DATA (default ON) gates data/. Cross compiling to an architecture the host cannot execute now defaults BUILD_DATA to OFF with a warning instead of failing later in the build; host-runnable cross configurations (x86 on x64 Windows) keep building data, and emulation setups (Rosetta 2) can force -DBUILD_DATA=ON. - ENABLE_GTEST now fails configuration when tools or data are disabled, since the conversion tests execute the opencc CLI against the built dictionaries. **Host tools** - OPENCC_DICT_EXECUTABLE and OPENCC_CLI_EXECUTABLE accept host-built tool paths; dictionaries generated with host tools are byte-identical to target-built ones. **Shared runnability helpers** - The jieba plugin's architecture normalization and can-run-on-host logic moved to cmake/OpenCCRunnability.cmake and is reused by the top level; the plugin keeps an inline fallback for standalone builds without the parent source tree. **Windows and iOS build fixes** - copy_libopencc_to_dir_of_opencc_dict moved next to the tools (data/ and jieba/ both use it) so it survives BUILD_DATA=OFF; tool executables now install with a BUNDLE destination so MACOSX_BUNDLE (iOS) installs work. - The MSVC ARM64 job no longer needs an explicit compile-only target list. --- .github/workflows/msvc.yml | 4 +- CMakeLists.txt | 71 ++++++++++++++++++++++++++- NEWS.md | 3 ++ cmake/OpenCCRunnability.cmake | 58 ++++++++++++++++++++++ data/CMakeLists.txt | 37 ++++++++------ plugins/jieba/CMakeLists.txt | 92 +++++++++++++++++++++-------------- src/CMakeLists.txt | 17 ++++++- src/tools/CMakeLists.txt | 3 ++ 8 files changed, 228 insertions(+), 57 deletions(-) create mode 100644 cmake/OpenCCRunnability.cmake 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 b75ce9cf3..6ed5b1d4a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,6 +49,68 @@ 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(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. +set(OPENCC_DICT_EXECUTABLE "" CACHE FILEPATH "Path to a host-built opencc_dict executable used to generate dictionaries when cross compiling") +set(OPENCC_CLI_EXECUTABLE "" CACHE FILEPATH "Path to a host-built opencc CLI executable used to generate dictionaries when cross compiling") + +# Building the dictionaries executes the just-built target tools (opencc_dict +# and the opencc CLI) at build time. When cross compiling to an architecture +# the host cannot execute, 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), or host tools can be provided via +# OPENCC_DICT_EXECUTABLE/OPENCC_CLI_EXECUTABLE above to keep building data. +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/OpenCCRunnability.cmake") +# Whether BUILD_DATA was provided explicitly (-D or a previous configure) +# before option() normalizes it, so that the warning below only fires when +# the default was flipped automatically rather than chosen by the user. +get_property(_OPENCC_BUILD_DATA_PROVIDED CACHE BUILD_DATA PROPERTY TYPE SET) +set(_OPENCC_BUILD_DATA_DEFAULT ON) +set(_OPENCC_BUILD_DATA_AUTO_OFF OFF) +if(NOT _OPENCC_BUILD_DATA_PROVIDED + AND CMAKE_CROSSCOMPILING AND NOT OPENCC_CAN_RUN_TARGET_TOOLS + AND NOT OPENCC_DICT_EXECUTABLE AND NOT OPENCC_CLI_EXECUTABLE) + set(_OPENCC_BUILD_DATA_DEFAULT OFF) + set(_OPENCC_BUILD_DATA_AUTO_OFF ON) +endif() +option(BUILD_DATA "Build OpenCC dictionary data" ${_OPENCC_BUILD_DATA_DEFAULT}) +if(_OPENCC_BUILD_DATA_AUTO_OFF) + message(WARNING + "Cross compiling for an architecture the host cannot execute: BUILD_DATA " + "defaulted to OFF, so no dictionaries will be built or installed. " + "Provide host-built tools via OPENCC_DICT_EXECUTABLE and " + "OPENCC_CLI_EXECUTABLE to build the dictionaries anyway, or set " + "BUILD_DATA=ON explicitly if the host can run the target binaries " + "(e.g. through Rosetta 2).") +endif() +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() + set(OPENCC_USING_HOST_TOOLS ON) +else() + set(OPENCC_USING_HOST_TOOLS OFF) +endif() +if(BUILD_DATA AND NOT BUILD_TOOLS AND NOT OPENCC_USING_HOST_TOOLS) + message(FATAL_ERROR + "BUILD_DATA requires 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 @@ -482,13 +544,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(BUILD_DATA) + add_subdirectory(data) +elseif(ENABLE_GTEST) + message(FATAL_ERROR "ENABLE_GTEST requires 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 BUILD_TOOLS) + message(FATAL_ERROR "ENABLE_GTEST requires 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 62671d551..f2d7d9983 100644 --- a/NEWS.md +++ b/NEWS.md @@ -15,6 +15,9 @@ * **詞庫更新**: * 以 cn-era 的 437 個歷代年號(漢武帝建元至清宣統)逐一驗證 `t2s`/`s2t`/`s2tw`/`s2hk` 的往返轉換,修正 `乾` 字年號被誤轉的問題:`t2s` 不再把「乾符/乾寧/乾祐/乾德/乾亨/乾封/乾明/乾定/乾統/乾興」轉成「干×」,`s2t` 也不再把簡體輸入的這些年號還原為「幹×」([#1517](https://github.com/BYVoid/OpenCC/issues/1517), [#1518](https://github.com/BYVoid/OpenCC/pull/1518))。「乾化」(後梁)在繁體中與「乾化」(gān huà,如「污泥乾化」)同形,故 `t2s` 只在「乾化元年~五年」「乾化年間」「後梁乾化」等語境下保留「乾」,單獨的「乾化」仍轉為「干化」;`s2t` 方向則新增「干化 → 乾化」。另補上「征和」(漢武帝)、「應曆」(遼穆宗;`应历元年~十九年`、`应历年间`、`辽应历`)、「聖曆」(武周;`圣历元年~三年`、`圣历年间`、`武周圣历`)的 `s2t` 詞條;後兩者刻意不收單獨的「应历/圣历」,以免「相应历史」「神圣历史」被拆錯。「升平」(晉穆帝)的 `s2t` 詞條改為「昇平 升平」,預設輸出不變,但多出「升平」候選,供 `GetAllConversions`/`--ambiguities` 等一對多查詢取用。 * **依賴更新**:附帶的 `tclap` 由 1.2.5 更新到 1.4.0(header-only;上游改以 CMake 建置,附帶目錄改為 `deps/tclap-1.4.0/include`),Bazel 亦改用 BCR 上的 `tclap@1.4.0`。OpenCC 只使用 `CmdLine`/`ValueArg`/`SwitchArg`/`MultiArg`/`ValuesConstraint` 與 `StdOutput` 等穩定介面,程式碼無需修改;`USE_SYSTEM_TCLAP` 下仍可與系統的 1.2.x 相容。另,`USE_SYSTEM_TCLAP=ON` 時 CMake 現在會在 configure 階段尋找系統 TCLAP(先試 1.4 的 CMake package,再試 pkg-config,最後直接找標頭)並印出版本,且會試編 OpenCC 實際用到的 TCLAP 介面;找不到或介面不相容時立即以明確訊息報錯,而不是等到編譯 `src/tools` 時才出現模板錯誤。 +* **交叉編譯**: + * 新增 `BUILD_TOOLS` 與 `BUILD_DATA` 選項,修復交叉編譯到宿主無法執行的目標架構(iOS、Android、Windows ARM64 等)時,因建置期執行剛編譯出的 `opencc_dict`/`opencc` 而必然失敗的問題([#615](https://github.com/BYVoid/OpenCC/issues/615))。交叉編譯時 `BUILD_DATA` 自動預設為 OFF 並輸出警告,僅產出程式庫與工具;宿主可執行目標二進位的組態(如 x64 Windows 宿主建置 x86)維持生成字典,依賴 Rosetta 2 等模擬的組態可明確設定 `-DBUILD_DATA=ON`。`ENABLE_GTEST` 現於 `BUILD_TOOLS` 或 `BUILD_DATA` 關閉時明確報錯(轉換測試需執行 opencc 並載入字典)。 + * 新增 `OPENCC_DICT_EXECUTABLE`/`OPENCC_CLI_EXECUTABLE` 快取變數,指向預先為宿主架構建置的工具;交叉編譯時提供這兩個路徑即可照常生成並安裝完整字典(字典屬架構無關資料,宿主生成與目標生成內容一致)。可執行性判斷與 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..143b6a355 --- /dev/null +++ b/cmake/OpenCCRunnability.cmake @@ -0,0 +1,58 @@ +# 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_CAN_RUN_TARGET_TOOLS: TRUE when the host can plausibly execute +# binaries built for the target architecture. This covers native builds and +# x86 targets on x64 Windows hosts; emulation such as Rosetta 2 (x64 targets +# on Apple Silicon) is deliberately not detected — set the relevant BUILD_* +# options 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) + 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(_opencc_can_run_target) + set(OPENCC_CAN_RUN_TARGET_TOOLS TRUE) + endif() + endif() +endif() diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt index 682b7930b..f0ed2618f 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -4,8 +4,26 @@ 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 with their own libraries, so no + # build-tree copy dependency is needed on Windows. + set(OPENCC_DICT_BIN "${OPENCC_DICT_EXECUTABLE}") + set(OPENCC_BIN "${OPENCC_CLI_EXECUTABLE}") + set(OPENCC_CLI_COMMAND "${OPENCC_CLI_EXECUTABLE}") + set(DICT_WIN32_DEPENDS) +else() + set(OPENCC_DICT_BIN opencc_dict) + set(OPENCC_BIN opencc) + # $ generator expressions only resolve for targets; host + # tools above are plain paths. + set(OPENCC_CLI_COMMAND "$") + 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") @@ -106,7 +124,7 @@ set( --input ${DICT_DIR}/HKPhrases.txt --input ${DICT_DIR}/TWPhrases.txt --output STPhrases_GeneratedFromRegionalPhrases.txt - --opencc $ + --opencc ${OPENCC_CLI_COMMAND} --config ${CMAKE_CURRENT_SOURCE_DIR}/config/t2s.json --dict-dir ${DICT_GENERATED_DIR} ) @@ -156,19 +174,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 diff --git a/plugins/jieba/CMakeLists.txt b/plugins/jieba/CMakeLists.txt index aa5371bed..764581973 100644 --- a/plugins/jieba/CMakeLists.txt +++ b/plugins/jieba/CMakeLists.txt @@ -102,49 +102,67 @@ 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 just-built (integrated mode) or installed (standalone mode) +# opencc_dict can actually run on this host: dictionary generation executes +# it at build time. The helper is shared with the top-level data build; a +# standalone plugin build without the parent source tree falls back to an +# inline copy kept in sync with cmake/OpenCCRunnability.cmake. +if(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}") + 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}") - if (NOT _opencc_can_run_target) + 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) + set(OPENCC_CAN_RUN_CPPJIEBA_DICT FALSE) + endif() + endif() +endif() + +if (OPENCC_CAN_RUN_CPPJIEBA_DICT AND + NOT CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR AND + NOT BUILD_TOOLS) + message(FATAL_ERROR + "Building the merged Jieba dictionary requires the opencc_dict build " + "tool: set BUILD_TOOLS=ON or provide host tools.") endif() if (OPENCC_CAN_RUN_CPPJIEBA_DICT) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7a7afc033..311d18afd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -263,4 +263,19 @@ endif() # Subdir -add_subdirectory(tools) +if(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() From 9959d19c0a8aa44cb81c040680044acd95278211 Mon Sep 17 00:00:00 2001 From: ghostflyby Date: Sun, 4 Oct 2026 15:23:17 +0800 Subject: [PATCH 2/3] Address the cross-compilation review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (#1) 可运行性判断嵌套回归(MSVC ARM64 CI 失败)+ 复审 blocking:提升分支 gate CMAKE_HOST_WIN32: WIN32 宿主/目标架构对比为平级独立 if(在 CMAKE_CROSSCOMPILING 检查外,附注释)。按复审 blocking 修正提升方向:不可运行恒降 FALSE;可运行仅在 CMAKE_HOST_WIN32 时回 TRUE(架构对比的 WOW64 语义只对 Windows 宿主成立,非 Windows 宿主交叉到 Windows(mingw triplet)即使架构相同也维持 FA (#2) 自动关 DATA 的条件不应叠加 CMAKE_CROSSCOMPILING: 顶层默认值判断条件仅 NOT OPENCC_CAN_RUN_TARGET_TOOLS 且未提供宿主工具;CMAKE_CROSSCOMPILING 不再出现于条件(复审判定放行,未改动)。 (#3) jieba 集成模式接入宿主工具: 集成模式 + OPENCC_USING_HOST_TOOLS 时 OPENCC_CAN_RUN_CPPJIEBA_DICT 置 TRUE,OPENCC_DICT_COMMAND 用宿主 OPENCC_DICT_EXECUTABLE 经 opencc_host_tool_command 包裹(非 WIN32 走 cmake -E env LD/DYLD_LIBRARY_PATH,前缀取工具上两级), (#4) 自动 OFF 不应写入 cache: get_property(... CACHE OPENCC_BUILD_DATA PROPERTY TYPE SET) 探测;未提供设普通变量每次重算、完全不落 cache;提供则尊重 cache;宿主工具已设置但显式 OFF 时输出警告(复审判定放行,未改动)。 (#5) 选项加 OPENCC_ 前缀: 全局改名覆盖所有 CMake 文件、注释、错误文案、NEWS.md 与 PR 草稿(复审判定放行,未改动)。 (#6) 宿主工具能力探测与版本说明: 存在性检查后以(env 包裹后的)命令执行 opencc_dict --help,非零退出 FATAL;两个 cache 文档字符串注明须与同一份 OpenCC 源码版本同源(复审判定达标,未改动;复审次要观察的 CLI 探测为可选增强,未做)。 (#7) 非 Windows 宿主工具的动态库路径: opencc_host_tool_env(仅 cmake -E env + LD/DYLD_LIBRARY_PATH 前缀,Windows 为空)+ opencc_host_tool_command(前缀+工具)两个共享辅助函数;data/ 直接命令与 jieba 集成/standalone 宿主工具命令复用;ST 生成器例外:env 前缀包住整个 Python 调用(spawn 的 CLI 继承 (#8) 警告涵盖 configs;DATA=OFF 时不安装指向缺失字典的 jieba 配置: 警告文案同时提及 dictionaries 与 config files(逐字包含 OPENCC_BUILD_DATA defaulted to OFF);*_jieba.json 安装门控为 CAN_RUN 且(standalone 或 OPENCC_BUILD_DATA);NEWS 说明行为(复审判定放行,未改动)。 (#9) CMAKE_CROSSCOMPILING_EMULATOR 不自动关 data + 复审 blocking:jieba merged 字典也加 emulator 前缀: 模块末尾 if(CMAKE_CROSSCOMPILING_EMULATOR) 置 TRUE(fallback 同步)。data/:直接命令冠 ${CMAKE_CROSSCOMPILING_EMULATOR} 前缀,转换器以重复 --opencc= 选项(emulator 与目标路径各一项,foreach 构造)传给 ST 生成器,脚本 --opencc 为 action=append (follow-up P1) 宿主工具动态库环境误用目标平台变量:opencc_host_tool_env 及 jieba 内联回退改用 CMAKE_HOST_WIN32/CMAKE_HOST_APPLE —— 工具在宿主执行,macOS 交叉编译到 Linux 目标时也应设置 DYLD_LIBRARY_PATH(单元级复现验证:模拟 Linux 目标 + macOS 宿主,修复前给 LD、修复后给 DYLD)。 (follow-up P2) OPENCC_BUILD_DATA 显式探测只查 cache、漏普通变量:补 DEFINED 普通 变量判断(CMP0077=NEW 下父项目 add_subdirectory 前的 set 不再被自动默认值覆写;单元级验证:父项目同时设 DATA/TOOLS=OFF 经 add_subdirectory 消费,配置成功无 FATAL)。自动默认不落 cache 的行为保持不变。全部 13 个构建验证门回归通过。 --- CMakeLists.txt | 111 ++++++++++----- NEWS.md | 4 +- cmake/OpenCCRunnability.cmake | 75 +++++++++-- data/CMakeLists.txt | 46 +++++-- ...nerate_st_phrases_from_regional_phrases.py | 14 +- plugins/jieba/CMakeLists.txt | 126 ++++++++++++------ src/CMakeLists.txt | 2 +- 7 files changed, 277 insertions(+), 101 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6ed5b1d4a..6a3d65c3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -49,45 +49,26 @@ 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(BUILD_TOOLS "Build OpenCC command-line tools" ON) +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. -set(OPENCC_DICT_EXECUTABLE "" CACHE FILEPATH "Path to a host-built opencc_dict executable used to generate dictionaries when cross compiling") -set(OPENCC_CLI_EXECUTABLE "" CACHE FILEPATH "Path to a host-built opencc CLI executable used to generate dictionaries when cross compiling") +# 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 cross compiling to an architecture -# the host cannot execute, default to skipping the data build instead of +# 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), or host tools can be provided via -# OPENCC_DICT_EXECUTABLE/OPENCC_CLI_EXECUTABLE above to keep building data. +# host can emulate the target (Rosetta 2). include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/OpenCCRunnability.cmake") -# Whether BUILD_DATA was provided explicitly (-D or a previous configure) -# before option() normalizes it, so that the warning below only fires when -# the default was flipped automatically rather than chosen by the user. -get_property(_OPENCC_BUILD_DATA_PROVIDED CACHE BUILD_DATA PROPERTY TYPE SET) -set(_OPENCC_BUILD_DATA_DEFAULT ON) -set(_OPENCC_BUILD_DATA_AUTO_OFF OFF) -if(NOT _OPENCC_BUILD_DATA_PROVIDED - AND CMAKE_CROSSCOMPILING AND NOT OPENCC_CAN_RUN_TARGET_TOOLS - AND NOT OPENCC_DICT_EXECUTABLE AND NOT OPENCC_CLI_EXECUTABLE) - set(_OPENCC_BUILD_DATA_DEFAULT OFF) - set(_OPENCC_BUILD_DATA_AUTO_OFF ON) -endif() -option(BUILD_DATA "Build OpenCC dictionary data" ${_OPENCC_BUILD_DATA_DEFAULT}) -if(_OPENCC_BUILD_DATA_AUTO_OFF) - message(WARNING - "Cross compiling for an architecture the host cannot execute: BUILD_DATA " - "defaulted to OFF, so no dictionaries will be built or installed. " - "Provide host-built tools via OPENCC_DICT_EXECUTABLE and " - "OPENCC_CLI_EXECUTABLE to build the dictionaries anyway, or set " - "BUILD_DATA=ON explicitly if the host can run the target binaries " - "(e.g. through Rosetta 2).") -endif() + if(OPENCC_DICT_EXECUTABLE OR OPENCC_CLI_EXECUTABLE) if(NOT OPENCC_DICT_EXECUTABLE OR NOT OPENCC_CLI_EXECUTABLE) message(FATAL_ERROR @@ -101,14 +82,70 @@ if(OPENCC_DICT_EXECUTABLE OR OPENCC_CLI_EXECUTABLE) "${_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() -if(BUILD_DATA AND NOT BUILD_TOOLS AND NOT OPENCC_USING_HOST_TOOLS) + +# 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 - "BUILD_DATA requires BUILD_TOOLS, or host tools via OPENCC_DICT_EXECUTABLE " - "and OPENCC_CLI_EXECUTABLE.") + "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 @@ -544,10 +581,10 @@ if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/doc/CMakeLists.txt") elseif(BUILD_DOCUMENTATION) message(FATAL_ERROR "BUILD_DOCUMENTATION requires the doc source directory.") endif() -if(BUILD_DATA) +if(OPENCC_BUILD_DATA) add_subdirectory(data) elseif(ENABLE_GTEST) - message(FATAL_ERROR "ENABLE_GTEST requires BUILD_DATA: the conversion tests run against the built dictionaries.") + 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) @@ -555,8 +592,8 @@ 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 BUILD_TOOLS) - message(FATAL_ERROR "ENABLE_GTEST requires BUILD_TOOLS: the conversion tests execute the opencc CLI.") + 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) diff --git a/NEWS.md b/NEWS.md index f2d7d9983..58139efe9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -16,8 +16,8 @@ * 以 cn-era 的 437 個歷代年號(漢武帝建元至清宣統)逐一驗證 `t2s`/`s2t`/`s2tw`/`s2hk` 的往返轉換,修正 `乾` 字年號被誤轉的問題:`t2s` 不再把「乾符/乾寧/乾祐/乾德/乾亨/乾封/乾明/乾定/乾統/乾興」轉成「干×」,`s2t` 也不再把簡體輸入的這些年號還原為「幹×」([#1517](https://github.com/BYVoid/OpenCC/issues/1517), [#1518](https://github.com/BYVoid/OpenCC/pull/1518))。「乾化」(後梁)在繁體中與「乾化」(gān huà,如「污泥乾化」)同形,故 `t2s` 只在「乾化元年~五年」「乾化年間」「後梁乾化」等語境下保留「乾」,單獨的「乾化」仍轉為「干化」;`s2t` 方向則新增「干化 → 乾化」。另補上「征和」(漢武帝)、「應曆」(遼穆宗;`应历元年~十九年`、`应历年间`、`辽应历`)、「聖曆」(武周;`圣历元年~三年`、`圣历年间`、`武周圣历`)的 `s2t` 詞條;後兩者刻意不收單獨的「应历/圣历」,以免「相应历史」「神圣历史」被拆錯。「升平」(晉穆帝)的 `s2t` 詞條改為「昇平 升平」,預設輸出不變,但多出「升平」候選,供 `GetAllConversions`/`--ambiguities` 等一對多查詢取用。 * **依賴更新**:附帶的 `tclap` 由 1.2.5 更新到 1.4.0(header-only;上游改以 CMake 建置,附帶目錄改為 `deps/tclap-1.4.0/include`),Bazel 亦改用 BCR 上的 `tclap@1.4.0`。OpenCC 只使用 `CmdLine`/`ValueArg`/`SwitchArg`/`MultiArg`/`ValuesConstraint` 與 `StdOutput` 等穩定介面,程式碼無需修改;`USE_SYSTEM_TCLAP` 下仍可與系統的 1.2.x 相容。另,`USE_SYSTEM_TCLAP=ON` 時 CMake 現在會在 configure 階段尋找系統 TCLAP(先試 1.4 的 CMake package,再試 pkg-config,最後直接找標頭)並印出版本,且會試編 OpenCC 實際用到的 TCLAP 介面;找不到或介面不相容時立即以明確訊息報錯,而不是等到編譯 `src/tools` 時才出現模板錯誤。 * **交叉編譯**: - * 新增 `BUILD_TOOLS` 與 `BUILD_DATA` 選項,修復交叉編譯到宿主無法執行的目標架構(iOS、Android、Windows ARM64 等)時,因建置期執行剛編譯出的 `opencc_dict`/`opencc` 而必然失敗的問題([#615](https://github.com/BYVoid/OpenCC/issues/615))。交叉編譯時 `BUILD_DATA` 自動預設為 OFF 並輸出警告,僅產出程式庫與工具;宿主可執行目標二進位的組態(如 x64 Windows 宿主建置 x86)維持生成字典,依賴 Rosetta 2 等模擬的組態可明確設定 `-DBUILD_DATA=ON`。`ENABLE_GTEST` 現於 `BUILD_TOOLS` 或 `BUILD_DATA` 關閉時明確報錯(轉換測試需執行 opencc 並載入字典)。 - * 新增 `OPENCC_DICT_EXECUTABLE`/`OPENCC_CLI_EXECUTABLE` 快取變數,指向預先為宿主架構建置的工具;交叉編譯時提供這兩個路徑即可照常生成並安裝完整字典(字典屬架構無關資料,宿主生成與目標生成內容一致)。可執行性判斷與 jieba 插件既有的 `OPENCC_CAN_RUN_CPPJIEBA_DICT` 機制共用,上提至 `cmake/OpenCCRunnability.cmake`。 + * 新增 `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 index 143b6a355..99ad2c75c 100644 --- a/cmake/OpenCCRunnability.cmake +++ b/cmake/OpenCCRunnability.cmake @@ -31,11 +31,49 @@ function(opencc_can_run_target_on_host output_var host_arch target_arch) 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 and -# x86 targets on x64 Windows hosts; emulation such as Rosetta 2 (x64 targets -# on Apple Silicon) is deliberately not detected — set the relevant BUILD_* -# options explicitly when relying on it. +# 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() @@ -47,12 +85,27 @@ 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) - 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(_opencc_can_run_target) - set(OPENCC_CAN_RUN_TARGET_TOOLS TRUE) - endif() +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 f0ed2618f..b445a370b 100644 --- a/data/CMakeLists.txt +++ b/data/CMakeLists.txt @@ -6,18 +6,44 @@ find_package(Python3 REQUIRED COMPONENTS Interpreter) 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 with their own libraries, so no - # build-tree copy dependency is needed on Windows. + # 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}") - set(OPENCC_CLI_COMMAND "${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. - set(OPENCC_CLI_COMMAND "$") + # 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() @@ -120,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} + ${OPENCC_CLI_COMMAND} --config ${CMAKE_CURRENT_SOURCE_DIR}/config/t2s.json --dict-dir ${DICT_GENERATED_DIR} ) @@ -186,6 +213,7 @@ foreach(DICT ${DICTS_GENERATED}) ${DICT_WIN32_DEPENDS} ${OPENCC_BIN} ${DICT_${DICT}_GENERATING_INPUT} + VERBATIM ) set_directory_properties( PROPERTIES @@ -221,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 @@ -230,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") @@ -265,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 @@ -274,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 764581973..87d2d2591 100644 --- a/plugins/jieba/CMakeLists.txt +++ b/plugins/jieba/CMakeLists.txt @@ -103,12 +103,19 @@ 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") -# Whether the just-built (integrated mode) or installed (standalone mode) -# opencc_dict can actually run on this host: dictionary generation executes -# it at build time. The helper is shared with the top-level data build; a +# 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. -if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/../../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() @@ -135,6 +142,30 @@ else() set(${output_var} "${_can_run}" PARENT_SCOPE) endfunction() + 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() + + 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}") @@ -151,22 +182,39 @@ else() 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 BUILD_TOOLS) + 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 BUILD_TOOLS=ON or provide host tools.") + "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). @@ -179,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 @@ -214,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) @@ -299,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 311d18afd..7fe6ba2cd 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -263,7 +263,7 @@ endif() # Subdir -if(BUILD_TOOLS) +if(OPENCC_BUILD_TOOLS) add_subdirectory(tools) # opencc_dict in the build tree needs libopencc next to it to run on From 6ec42763c1d80ad386bde4bc704d04d74b98a167 Mon Sep 17 00:00:00 2001 From: Frank Lin Date: Thu, 29 Oct 2026 12:14:38 -0700 Subject: [PATCH 3/3] Skip the system TCLAP check when the command line tools are not built TCLAP is only consumed by src/tools, so a library-only build with OPENCC_BUILD_TOOLS=OFF and USE_SYSTEM_TCLAP=ON should not fail on a host without the TCLAP headers. --- CMakeLists.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a3d65c3e..4d590a29b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -474,7 +474,9 @@ else() endif() endif() -if(USE_SYSTEM_TCLAP) +if(USE_SYSTEM_TCLAP AND OPENCC_BUILD_TOOLS) + # TCLAP is only used by the command line tools in src/tools, so a + # library-only build (OPENCC_BUILD_TOOLS=OFF) does not need it at all. # Locate the system TCLAP headers. TCLAP >= 1.4 installs a CMake package # (TCLAPConfig.cmake, target TCLAP::TCLAP); TCLAP 1.2.x only ships a # pkg-config file. Fall back to a plain header search so that a bare header