#!/bin/sh # wetype.sh - WeChat input method (WeType) on Ubuntu via Fcitx 5, for hub.skillnav.dev # # Purpose : Tencent ships no Linux build of WeType. The community project # yu1745/wetype-ime-linux runs the Android WeType engine (under # QEMU user mode on x86_64, natively on aarch64) behind a Fcitx 5 # plugin. This script switches the desktop to Fcitx 5, installs # that project's AppImage, and keeps a copy of the WeType APK so a # later reinstall still works if Tencent pulls that version. # Usage : curl -fsSL https://hub.skillnav.dev/wetype.sh | sh -s -- # subcommands: fcitx | install | check | all # Idempotent: yes. Packages are installed only when missing, ~/.xinputrc is # only rewritten when it does not already select fcitx5, and the # engine install is skipped when the engine is already present. # Dry run : HUB_DRY_RUN=1 prints each command instead of running it. # HUB_DRY_RUN=1 sh wetype.sh all # # Environment: # WETYPE_APPIMAGE= use an AppImage you already downloaded instead of # fetching the latest release from GitHub. # WETYPE_APK= install from a saved WeType 3.5.4 APK instead of # letting the installer download it from Tencent. # The backup this script makes is meant for this. # WETYPE_REINSTALL=1 run the engine installer even if it is installed. # HUB_FAKE_LINUX=1 skip the Linux/apt guard (testing with dry run). # HUB_LIB_ONLY=1 define the functions but do not run main. # # Unofficial: wetype-ime-linux is not affiliated with Tencent. Its installer # downloads the WeType APK (~214 MB) from Tencent's own servers; neither this # script nor this site hosts any Tencent file. # # Installs and configures only; never uninstalls, never deletes a file. To # remove the engine later run the AppImage's own `uninstall` subcommand. # Contains no secrets and no internal hostnames; safe to serve publicly. set -eu # --------------------------------------------------------------------------- HUB_BASE="${HUB_BASE:-https://hub.skillnav.dev}" HUB_DRY_RUN="${HUB_DRY_RUN:-0}" HUB_FAKE_LINUX="${HUB_FAKE_LINUX:-0}" WETYPE_APPIMAGE="${WETYPE_APPIMAGE:-}" WETYPE_APK="${WETYPE_APK:-}" WETYPE_REINSTALL="${WETYPE_REINSTALL:-0}" GH_REPO="yu1745/wetype-ime-linux" # Paths written by the upstream installer for a per-user (~/.local) install. WT_ENGINE="${HOME}/.local/lib/wetype-ime" WT_APK_CACHE="${XDG_CACHE_HOME:-${HOME}/.cache}/wetype-ime" # Paths owned by this script. WT_DL="${XDG_CACHE_HOME:-${HOME}/.cache}/hub-wetype" WT_BACKUP="${XDG_DATA_HOME:-${HOME}/.local/share}/wetype-ime-backup" FCITX_PKGS="fcitx5 fcitx5-config-qt fcitx5-frontend-gtk3 fcitx5-frontend-gtk4 fcitx5-frontend-qt5 im-config" XINPUTRC="${HOME}/.xinputrc" FCITX_PROFILE="${XDG_CONFIG_HOME:-${HOME}/.config}/fcitx5/profile" # --------------------------------------------------------------------------- log() { printf '==> %s\n' "$*"; } ok() { printf 'ok %s\n' "$*"; } note() { printf ' %s\n' "$*"; } warn() { printf '!! %s\n' "$*" >&2; } dry() { printf 'DRY %s\n' "$*"; } # run - execute it, or print it when HUB_DRY_RUN=1. run() { if [ "$HUB_DRY_RUN" = "1" ]; then dry "$*" else log "$*" sh -c "$*" fi } HUB_CHANGED=0 HUB_PENDING="" changed() { HUB_CHANGED=$((HUB_CHANGED + 1)); } pending() { HUB_PENDING="${HUB_PENDING} $1"; } # --- guards ---------------------------------------------------------------- require_linux() { [ "$HUB_FAKE_LINUX" = "1" ] && return 0 if [ "$(uname -s)" != "Linux" ]; then warn "wetype.sh only runs on Linux. This machine reports $(uname -s)." note "On macOS and Windows install WeType from https://z.weixin.qq.com/" exit 1 fi if ! command -v apt-get >/dev/null 2>&1; then warn "apt-get was not found. wetype.sh targets Debian and Ubuntu only." note "On other distributions follow https://github.com/${GH_REPO}" exit 1 fi return 0 } # The engine installs into the invoking user's ~/.local. Run as root it would # land in /root and never be seen by the desktop session. require_user() { [ "$HUB_FAKE_LINUX" = "1" ] && return 0 if [ "$(id -u)" -eq 0 ]; then warn "run wetype.sh as your normal desktop user, not root or sudo." note "It calls sudo itself for the apt steps." exit 1 fi return 0 } # --- small helpers --------------------------------------------------------- # ver_ge - true when version a >= version b. ver_ge() { [ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -n 1)" = "$2" ]; } pkg_installed() { dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q 'install ok installed' } # missing_pkgs "" - print the ones that are not installed. missing_pkgs() { out="" # shellcheck disable=SC2086 # word splitting of the package list is intended set -- $1 for p in "$@"; do pkg_installed "$p" || out="${out} ${p}" done printf '%s' "${out# }" } apt_install() { # shellcheck disable=SC2086 # word splitting of the package list is intended set -- $1 [ "$#" -eq 0 ] && return 0 run "sudo apt-get update -qq && sudo apt-get install -y $*" changed } detect_arch() { case "$(uname -m)" in x86_64|amd64) printf 'x86_64' ;; aarch64|arm64) printf 'aarch64' ;; *) printf 'unknown' ;; esac } glibc_version() { v=$(getconf GNU_LIBC_VERSION 2>/dev/null | awk '{print $2}' || true) [ -n "$v" ] || v=$(ldd --version 2>/dev/null | head -n 1 | grep -oE '[0-9]+\.[0-9]+$' || true) printf '%s' "${v:-0}" } # Upstream version of the fcitx5 package, e.g. 5.1.7 from 5.1.7-1build1. fcitx_version() { v=$(dpkg-query -W -f='${Version}' fcitx5 2>/dev/null || true) v=${v#*:} printf '%s' "${v%%-*}" } xinputrc_is_fcitx5() { [ -f "$XINPUTRC" ] && grep -q 'fcitx5' "$XINPUTRC"; } engine_installed() { [ -d "$WT_ENGINE" ] || [ -d /usr/lib/wetype-ime ]; } profile_has_wetype() { [ -f "$FCITX_PROFILE" ] && grep -qi 'wetype' "$FCITX_PROFILE"; } appimage_name() { printf 'WeTypeIME-Engine-%s.AppImage' "$(detect_arch)"; } # Asset URL of the latest release, read from the GitHub API. Falls back to the # stable /releases/latest/download/ redirect when the API is rate limited. latest_appimage_url() { name=$(appimage_name) url=$(curl -fsSL --connect-timeout 10 -m 20 "https://api.github.com/repos/${GH_REPO}/releases/latest" 2>/dev/null \ | grep -oE "\"browser_download_url\"[[:space:]]*:[[:space:]]*\"[^\"]*/${name}\"" \ | head -n 1 | sed -e 's/.*"\(https[^"]*\)"$/\1/' || true) [ -n "$url" ] || url="https://github.com/${GH_REPO}/releases/latest/download/${name}" printf '%s' "$url" } # --- fcitx ----------------------------------------------------------------- cmd_fcitx() { log "fcitx - Fcitx 5 packages and the session input method" missing=$(missing_pkgs "$FCITX_PKGS") if [ -z "$missing" ]; then ok "Fcitx 5 packages already installed ($(fcitx_version))" else note "missing: ${missing}" apt_install "$missing" fi # im-config -n rewrites ~/.xinputrc, which is the whole point; keep a copy # of a pre-existing one that selected something else. if xinputrc_is_fcitx5; then ok "${XINPUTRC} already selects fcitx5" else if [ -f "$XINPUTRC" ] && [ ! -f "${XINPUTRC}.hub-bak" ]; then run "cp -p '${XINPUTRC}' '${XINPUTRC}.hub-bak'" fi run "im-config -n fcitx5" changed pending "log-out-and-back-in" fi return 0 } # --- install --------------------------------------------------------------- backup_apk() { for apk in "$WT_APK_CACHE"/*.apk; do [ -f "$apk" ] || continue dest="${WT_BACKUP}/$(basename "$apk")" if [ -f "$dest" ]; then ok "APK backup already at ${dest}" else run "mkdir -p '${WT_BACKUP}' && cp -p '${apk}' '${dest}'" note "reinstall offline later with: WETYPE_APK='${dest}'" changed fi done return 0 } cmd_install() { log "install - the wetype-ime-linux engine and Fcitx 5 plugin" arch=$(detect_arch) if [ "$arch" = "unknown" ]; then warn "unsupported architecture $(uname -m). Upstream publishes x86_64 and aarch64 only." pending "unsupported-arch" return 0 fi ok "architecture: ${arch}" glibc=$(glibc_version) if ! ver_ge "$glibc" "2.38"; then warn "glibc ${glibc} is older than 2.38 (Ubuntu 22.04 or earlier)." note "The prebuilt Fcitx 5 plugin needs glibc >= 2.38 and Fcitx 5.1.x." note "Either upgrade to Ubuntu 24.04, or build the plugin from source:" note " https://github.com/${GH_REPO} (fcitx5-wetype/build.sh)" pending "glibc-too-old" return 0 fi ok "glibc ${glibc}" fv=$(fcitx_version) if [ -z "$fv" ] && [ "$HUB_DRY_RUN" = "1" ]; then note "fcitx5 not installed yet; dry run assumes the fcitx step installs 5.1.x" fv="5.1.x" fi if [ -z "$fv" ]; then warn "fcitx5 is not installed. Run the fcitx step first:" note " curl -fsSL ${HUB_BASE}/wetype.sh | sh -s -- fcitx" pending "fcitx5-missing" return 0 fi case "$fv" in 5.1.*) ok "fcitx5 ${fv}" ;; *) warn "fcitx5 ${fv} is not 5.1.x; the prebuilt plugin links libFcitx5Core.so.7." note "Build the plugin from source instead: https://github.com/${GH_REPO}" pending "fcitx5-version" return 0 ;; esac deps="python3 unzip curl" [ "$arch" = "aarch64" ] && deps="${deps} patchelf" missing=$(missing_pkgs "$deps") if [ -z "$missing" ]; then ok "dependencies already installed" else apt_install "$missing" fi if engine_installed && [ "$WETYPE_REINSTALL" != "1" ]; then ok "engine already installed (WETYPE_REINSTALL=1 to run the installer again)" backup_apk return 0 fi # Locate or download the AppImage. if [ -n "$WETYPE_APPIMAGE" ]; then ai=$WETYPE_APPIMAGE if [ "$HUB_DRY_RUN" != "1" ] && [ ! -f "$ai" ]; then warn "WETYPE_APPIMAGE points at a file that does not exist: ${ai}" exit 1 fi ok "using AppImage from WETYPE_APPIMAGE: ${ai}" else ai="${WT_DL}/$(appimage_name)" if [ -f "$ai" ]; then ok "AppImage already downloaded: ${ai}" else [ "$HUB_DRY_RUN" = "1" ] && dry "curl -fsSL https://api.github.com/repos/${GH_REPO}/releases/latest # find $(appimage_name)" url=$(latest_appimage_url) # No total timeout: a slow link is not a failure. Bound the connect # phase, retry a dropped transfer, and only rename a complete file. run "mkdir -p '${WT_DL}' && curl -fL --progress-bar --connect-timeout 20 --retry 2 '${url}' -o '${ai}.part' && mv '${ai}.part' '${ai}'" fi fi run "chmod +x '${ai}'" apk_arg="" if [ -n "$WETYPE_APK" ]; then if [ "$HUB_DRY_RUN" != "1" ] && [ ! -f "$WETYPE_APK" ]; then warn "WETYPE_APK points at a file that does not exist: ${WETYPE_APK}" exit 1 fi apk_arg=" --apk '${WETYPE_APK}'" ok "using APK from WETYPE_APK: ${WETYPE_APK}" else note "first install downloads the WeType APK (~214 MB) from Tencent" fi # Extract-and-run avoids depending on FUSE (libfuse2 is not on a stock # 24.04). stdin is the pipe this script came from, so give the AppImage # /dev/null rather than letting it read the rest of this script. run "APPIMAGE_EXTRACT_AND_RUN=1 '${ai}' install${apk_arg} /dev/null 2>&1; then run "fcitx5 -r -d >/dev/null 2>&1" fi profile_has_wetype || pending "add-微信拼音-in-fcitx5-configtool" return 0 } # --- check ----------------------------------------------------------------- CHECK_FAILS=0 CHECK_ROWS="" check_row() { CHECK_ROWS="${CHECK_ROWS}$1|$2|$3 " [ "$1" = "FAIL" ] && CHECK_FAILS=$((CHECK_FAILS + 1)) return 0 } cmd_check() { log "check - health of the install" if [ "$HUB_DRY_RUN" = "1" ]; then dry "uname -m; getconf GNU_LIBC_VERSION; dpkg-query -W fcitx5" dry "grep fcitx5 ${XINPUTRC}; pgrep -x fcitx5" dry "test -d ${WT_ENGINE}; ls ${WT_BACKUP}/*.apk; grep -i wetype ${FCITX_PROFILE}" dry "APPIMAGE_EXTRACT_AND_RUN=1 demo nihao" note "dry run: nothing was probed, so there is no PASS/FAIL table" return 0 fi arch=$(detect_arch) if [ "$arch" != "unknown" ]; then check_row PASS "architecture" "$arch" else check_row FAIL "architecture" "$(uname -m) is not supported upstream"; fi glibc=$(glibc_version) if ver_ge "$glibc" "2.38"; then check_row PASS "glibc" "$glibc" else check_row FAIL "glibc" "${glibc} < 2.38, prebuilt plugin will not load"; fi fv=$(fcitx_version) case "$fv" in 5.1.*) check_row PASS "fcitx5" "$fv" ;; "") check_row FAIL "fcitx5" "not installed" ;; *) check_row FAIL "fcitx5" "${fv}, prebuilt plugin needs 5.1.x" ;; esac if xinputrc_is_fcitx5; then check_row PASS "session IM" "${XINPUTRC} selects fcitx5" else check_row FAIL "session IM" "${XINPUTRC} does not select fcitx5 (run fcitx)"; fi if pgrep -x fcitx5 >/dev/null 2>&1; then check_row PASS "fcitx5 running" "yes" else check_row FAIL "fcitx5 running" "no - log out and back in"; fi if engine_installed; then check_row PASS "engine" "installed" else check_row FAIL "engine" "not installed (run install)"; fi if ls "$WT_BACKUP"/*.apk >/dev/null 2>&1; then check_row PASS "APK backup" "$WT_BACKUP" else check_row SKIP "APK backup" "none yet"; fi if profile_has_wetype; then check_row PASS "fcitx5 profile" "WeType is in the input method list" else check_row FAIL "fcitx5 profile" "add 微信拼音 in fcitx5-configtool"; fi ai="${WETYPE_APPIMAGE:-${WT_DL}/$(appimage_name)}" if [ -x "$ai" ] && engine_installed; then out=$(APPIMAGE_EXTRACT_AND_RUN=1 timeout 60 "$ai" demo nihao /dev/null | head -n 3 | tr '\n' ' ' || true) if [ -n "$out" ]; then check_row PASS "engine demo" "nihao -> ${out}" else check_row FAIL "engine demo" "no candidates, see /tmp/wetype-harness.log"; fi else check_row SKIP "engine demo" "no AppImage at ${ai}" fi printf '\n' printf '%s' "$CHECK_ROWS" | awk -F'|' '{ printf "%-5s %-16s %s\n", $1, $2, $3 }' printf '\n' if [ "$CHECK_FAILS" -gt 0 ]; then warn "${CHECK_FAILS} check(s) failed" return 1 fi ok "all checks passed" return 0 } # --- driver ---------------------------------------------------------------- usage() { cat < Subcommands fcitx install Fcitx 5 and its GTK/Qt frontends, select it with im-config install download the wetype-ime-linux AppImage for this architecture and install the engine into ~/.local, then back up the WeType APK check PASS/FAIL/SKIP report, exits 1 if anything failed all fcitx, then install, then check Environment WETYPE_APPIMAGE= use a local AppImage instead of the latest release WETYPE_APK= install from a saved APK (offline reinstall) WETYPE_REINSTALL=1 run the engine installer even if already installed HUB_DRY_RUN=1 print commands instead of running them Requires Ubuntu 24.04+ (glibc >= 2.38, Fcitx 5.1.x) for the prebuilt plugin. Unofficial: upstream is https://github.com/${GH_REPO}, not Tencent. USAGE } summary() { printf '\n' log "Summary" if [ "$HUB_DRY_RUN" = "1" ]; then note "Dry run - nothing was installed or modified." elif [ "$HUB_CHANGED" -eq 0 ]; then note "Everything was already in place. No changes made." else note "${HUB_CHANGED} step(s) made changes." fi [ -n "$HUB_PENDING" ] && note "Unfinished:${HUB_PENDING}" note "Then open fcitx5-configtool, add 微信拼音, switch with Ctrl+Space." note "Cheatsheet: ${HUB_BASE}/commands/linux/" return 0 } main() { if [ "$#" -lt 1 ]; then usage exit 1 fi subcmd=$1 case "$subcmd" in -h|--help|help) usage; exit 0 ;; esac require_linux require_user log "hub wetype bootstrap - ${subcmd}" [ "$HUB_DRY_RUN" = "1" ] && note "DRY RUN: commands are printed, not executed." printf '\n' rc=0 case "$subcmd" in fcitx) cmd_fcitx ;; install) cmd_install ;; check) cmd_check || rc=1 ;; all) cmd_fcitx; printf '\n' cmd_install; printf '\n' cmd_check || true ;; *) warn "unknown subcommand: ${subcmd}" printf '\n' usage exit 1 ;; esac summary return "$rc" } [ "${HUB_LIB_ONLY:-0}" = "1" ] || main "$@"