From 5e0ff0df95a8bbd3411bd165422e20fc54c62b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 23:32:21 -0400 Subject: [PATCH] Add rime-ice-installer with offline assets and no sudo when its packages are present --- .../rime-ice-installer/.omarchy/package.json | 8 + pkgbuilds/rime-ice-installer/PKGBUILD | 57 ++++ .../rime-ice-installer/omarchy-offline.patch | 320 ++++++++++++++++++ 3 files changed, 385 insertions(+) create mode 100644 pkgbuilds/rime-ice-installer/.omarchy/package.json create mode 100644 pkgbuilds/rime-ice-installer/PKGBUILD create mode 100644 pkgbuilds/rime-ice-installer/omarchy-offline.patch diff --git a/pkgbuilds/rime-ice-installer/.omarchy/package.json b/pkgbuilds/rime-ice-installer/.omarchy/package.json new file mode 100644 index 0000000..a48ba2c --- /dev/null +++ b/pkgbuilds/rime-ice-installer/.omarchy/package.json @@ -0,0 +1,8 @@ +{ + "source": "local", + "sync": false, + "origin": { + "aur": "rime-ice-installer", + "commit": "08b3592fd79845148efde23768e5f1cfbda8bcd2" + } +} diff --git a/pkgbuilds/rime-ice-installer/PKGBUILD b/pkgbuilds/rime-ice-installer/PKGBUILD new file mode 100644 index 0000000..d24de1c --- /dev/null +++ b/pkgbuilds/rime-ice-installer/PKGBUILD @@ -0,0 +1,57 @@ +# Maintainer: Andy Stewart + +pkgname=rime-ice-installer +pkgver=0.1.10 +pkgrel=1 +pkgdesc='TUI installer for Fcitx5, Rime Ice and Wanxiang on Arch Linux' +arch=('x86_64') +url='https://github.com/manateelazycat/rime-ice-installer' +license=('custom:unknown') +depends=('curl' 'dbus' 'dialog' 'git' 'glib2' 'sudo' 'unzip') +makedepends=('go') +source=("$pkgname-$pkgver.tar.gz::$url/archive/refs/tags/v$pkgver.tar.gz" + 'omarchy-offline.patch') +sha256sums=('64008e326ece22d156c0fb634cc784c60a41c48858eb0d949349b41dfd0c5477' + 'f2b9a8ec97796d4432f3461396fa0332901624e70c873187703b85d61478c384') + +_setup_go_env() { + export GOPATH="$srcdir" + export GOMODCACHE="$srcdir/pkg/mod" + export GOFLAGS='-buildmode=pie -trimpath -mod=readonly -modcacherw' +} + +prepare() { + cd "$srcdir/$pkgname-$pkgver" + + patch -Np1 -i "$srcdir/omarchy-offline.patch" + _setup_go_env + + go mod download +} + +build() { + cd "$srcdir/$pkgname-$pkgver" + + _setup_go_env + export CGO_CPPFLAGS="${CPPFLAGS}" + export CGO_CFLAGS="${CFLAGS}" + export CGO_CXXFLAGS="${CXXFLAGS}" + export CGO_LDFLAGS="${LDFLAGS}" + + go build -o "$pkgname" . +} + +check() { + cd "$srcdir/$pkgname-$pkgver" + + _setup_go_env + + go test ./... +} + +package() { + cd "$srcdir/$pkgname-$pkgver" + + install -Dm755 "$pkgname" "$pkgdir/usr/bin/$pkgname" + install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md" +} diff --git a/pkgbuilds/rime-ice-installer/omarchy-offline.patch b/pkgbuilds/rime-ice-installer/omarchy-offline.patch new file mode 100644 index 0000000..eb92e4b --- /dev/null +++ b/pkgbuilds/rime-ice-installer/omarchy-offline.patch @@ -0,0 +1,320 @@ +diff --git a/internal/app/app.go b/internal/app/app.go +index 17e01bf..fd73578 100644 +--- a/internal/app/app.go ++++ b/internal/app/app.go +@@ -84,7 +84,8 @@ func Run(ctx context.Context, cfg config.InstallConfig) error { + return err + } + +- if os.Geteuid() != 0 { ++ missingPackages := fcitx.MissingPackages() ++ if os.Geteuid() != 0 && len(missingPackages) > 0 { + if dialogUI != nil { + if err := validateSudoWithDialog(ctx, runner, dialogUI, detectedEnv.FingerprintAvailable); err != nil { + if errors.Is(err, ui.ErrCancelled) { +@@ -140,7 +141,7 @@ func Run(ctx context.Context, cfg config.InstallConfig) error { + } + + if err := step("安装 Fcitx5 与依赖", func() error { +- return fcitx.InstallPackages(ctx, runner, cfg, detectedEnv) ++ return fcitx.InstallPackages(ctx, runner, missingPackages, detectedEnv) + }); err != nil { + closeGauge(gauge) + return fail(dialogUI, logger, err) +@@ -181,7 +182,7 @@ func Run(ctx context.Context, cfg config.InstallConfig) error { + + if err := step("下载并补丁雾凇拼音", func() error { + var err error +- rimeWorkspace, err = rime.PrepareWorkspace(ctx, downloader, cfg.WorkspaceDir) ++ rimeWorkspace, err = rime.PrepareWorkspace(ctx, downloader, cfg.WorkspaceDir, cfg.AssetDir) + return err + }); err != nil { + closeGauge(gauge) +@@ -191,7 +192,7 @@ func Run(ctx context.Context, cfg config.InstallConfig) error { + if cfg.EnableWanxiang { + if err := step("下载并启用万象语法模型", func() error { + var err error +- modelRelease, err = wanxiang.Integrate(ctx, downloader, cfg.WorkspaceDir, rimeWorkspace.SourceDir) ++ modelRelease, err = wanxiang.Integrate(ctx, downloader, cfg.WorkspaceDir, rimeWorkspace.SourceDir, cfg.AssetDir) + return err + }); err != nil { + closeGauge(gauge) +diff --git a/internal/config/config.go b/internal/config/config.go +index e3554e8..2efc8c2 100644 +--- a/internal/config/config.go ++++ b/internal/config/config.go +@@ -18,6 +18,7 @@ type InstallConfig struct { + DryRun bool + Verbose bool + WorkspaceDir string ++ AssetDir string + } + + type DetectedEnv struct { +diff --git a/internal/fcitx/fcitx.go b/internal/fcitx/fcitx.go +index fba202a..f252ca1 100644 +--- a/internal/fcitx/fcitx.go ++++ b/internal/fcitx/fcitx.go +@@ -34,10 +34,23 @@ const ( + defaultGroupName = "Default" + ) + +-func InstallPackages(ctx context.Context, runner *system.Runner, cfg config.InstallConfig, env config.DetectedEnv) error { +- args := append([]string{"-S", "--needed", "--noconfirm"}, pacmanPackages...) +- if err := runner.RunPrivileged(ctx, "pacman", args...); err != nil { +- return err ++// MissingPackages 返回尚未安装的 pacman 包。 ++func MissingPackages() []string { ++ var missing []string ++ for _, pkg := range pacmanPackages { ++ if exec.Command("pacman", "-Q", pkg).Run() != nil { ++ missing = append(missing, pkg) ++ } ++ } ++ return missing ++} ++ ++func InstallPackages(ctx context.Context, runner *system.Runner, missing []string, env config.DetectedEnv) error { ++ if len(missing) > 0 { ++ args := append([]string{"-S", "--needed", "--noconfirm"}, missing...) ++ if err := runner.RunPrivileged(ctx, "pacman", args...); err != nil { ++ return err ++ } + } + + if !env.HasOctagramPlugin { +diff --git a/internal/rime/asset_dir_test.go b/internal/rime/asset_dir_test.go +new file mode 100644 +index 0000000..9e66346 +--- /dev/null ++++ b/internal/rime/asset_dir_test.go +@@ -0,0 +1,49 @@ ++package rime ++ ++import ( ++ "archive/zip" ++ "context" ++ "os" ++ "path/filepath" ++ "testing" ++) ++ ++func TestPrepareWorkspaceReadsFullZipFromAssetDir(t *testing.T) { ++ assetDir := t.TempDir() ++ archive, err := os.Create(filepath.Join(assetDir, rimeIceAsset)) ++ if err != nil { ++ t.Fatal(err) ++ } ++ writer := zip.NewWriter(archive) ++ entry, err := writer.Create("rime_ice.schema.yaml") ++ if err != nil { ++ t.Fatal(err) ++ } ++ if _, err := entry.Write([]byte("schema:\n schema_id: rime_ice\n")); err != nil { ++ t.Fatal(err) ++ } ++ if err := writer.Close(); err != nil { ++ t.Fatal(err) ++ } ++ archive.Close() ++ ++ workspace := t.TempDir() ++ prepared, err := PrepareWorkspace(context.Background(), nil, workspace, assetDir) ++ if err != nil { ++ t.Fatalf("PrepareWorkspace() error = %v", err) ++ } ++ if prepared.Release.Tag != "local" || prepared.Release.AssetName != rimeIceAsset { ++ t.Fatalf("release = %+v, want local %s", prepared.Release, rimeIceAsset) ++ } ++ for _, name := range []string{"rime_ice.schema.yaml", "default.custom.yaml", "user.yaml"} { ++ if _, err := os.Stat(filepath.Join(prepared.SourceDir, name)); err != nil { ++ t.Fatalf("%s missing from prepared workspace: %v", name, err) ++ } ++ } ++} ++ ++func TestPrepareWorkspaceFailsWithoutLocalFullZip(t *testing.T) { ++ if _, err := PrepareWorkspace(context.Background(), nil, t.TempDir(), t.TempDir()); err == nil { ++ t.Fatal("PrepareWorkspace() succeeded without full.zip in the asset dir") ++ } ++} +diff --git a/internal/rime/rime.go b/internal/rime/rime.go +index 1db520b..45900be 100644 +--- a/internal/rime/rime.go ++++ b/internal/rime/rime.go +@@ -26,20 +26,28 @@ type PreparedWorkspace struct { + Release config.ReleaseInfo + } + +-func PrepareWorkspace(ctx context.Context, client *download.Client, workspace string) (*PreparedWorkspace, error) { +- release, err := client.ReleaseByTag(ctx, rimeIceOwner, rimeIceRepo, rimeIceTag) +- if err != nil { +- return nil, err +- } ++func PrepareWorkspace(ctx context.Context, client *download.Client, workspace, assetDir string) (*PreparedWorkspace, error) { ++ info := config.ReleaseInfo{Owner: rimeIceOwner, Repo: rimeIceRepo, Tag: "local", AssetName: rimeIceAsset} ++ downloadPath := filepath.Join(assetDir, rimeIceAsset) ++ if assetDir == "" { ++ release, err := client.ReleaseByTag(ctx, rimeIceOwner, rimeIceRepo, rimeIceTag) ++ if err != nil { ++ return nil, err ++ } + +- asset, err := download.FindAsset(release, rimeIceAsset) +- if err != nil { +- return nil, err +- } ++ asset, err := download.FindAsset(release, rimeIceAsset) ++ if err != nil { ++ return nil, err ++ } + +- downloadPath := filepath.Join(workspace, "downloads", asset.Name) +- if err := client.DownloadAsset(ctx, asset, downloadPath); err != nil { +- return nil, err ++ downloadPath = filepath.Join(workspace, "downloads", asset.Name) ++ if err := client.DownloadAsset(ctx, asset, downloadPath); err != nil { ++ return nil, err ++ } ++ info.Tag = release.TagName ++ info.AssetName = asset.Name ++ info.Digest = normalizeDigest(asset.Digest) ++ info.URL = asset.BrowserDownloadURL + } + + sourceDir := filepath.Join(workspace, "source", "rime-ice") +@@ -54,17 +62,7 @@ func PrepareWorkspace(ctx context.Context, client *download.Client, workspace st + return nil, err + } + +- return &PreparedWorkspace{ +- SourceDir: sourceDir, +- Release: config.ReleaseInfo{ +- Owner: rimeIceOwner, +- Repo: rimeIceRepo, +- Tag: release.TagName, +- AssetName: asset.Name, +- Digest: normalizeDigest(asset.Digest), +- URL: asset.BrowserDownloadURL, +- }, +- }, nil ++ return &PreparedWorkspace{SourceDir: sourceDir, Release: info}, nil + } + + func MergeDefaultCustomConfig(path string) error { +diff --git a/internal/wanxiang/asset_dir_test.go b/internal/wanxiang/asset_dir_test.go +new file mode 100644 +index 0000000..23554b3 +--- /dev/null ++++ b/internal/wanxiang/asset_dir_test.go +@@ -0,0 +1,33 @@ ++package wanxiang ++ ++import ( ++ "context" ++ "os" ++ "path/filepath" ++ "strings" ++ "testing" ++) ++ ++func TestIntegrateReadsModelFromAssetDir(t *testing.T) { ++ assetDir := t.TempDir() ++ if err := os.WriteFile(filepath.Join(assetDir, modelAsset), []byte("model"), 0o644); err != nil { ++ t.Fatal(err) ++ } ++ ++ sourceDir := t.TempDir() ++ info, err := Integrate(context.Background(), nil, t.TempDir(), sourceDir, assetDir) ++ if err != nil { ++ t.Fatalf("Integrate() error = %v", err) ++ } ++ if info.Tag != "local" || info.AssetName != modelAsset { ++ t.Fatalf("release = %+v, want local %s", info, modelAsset) ++ } ++ model, err := os.ReadFile(filepath.Join(sourceDir, modelAsset)) ++ if err != nil || string(model) != "model" { ++ t.Fatalf("model copy = %q, %v", model, err) ++ } ++ custom, err := os.ReadFile(filepath.Join(sourceDir, "rime_ice.custom.yaml")) ++ if err != nil || !strings.Contains(string(custom), "wanxiang-lts-zh-hans") { ++ t.Fatalf("custom yaml = %q, %v", custom, err) ++ } ++} +diff --git a/internal/wanxiang/wanxiang.go b/internal/wanxiang/wanxiang.go +index f629977..a0fd266 100644 +--- a/internal/wanxiang/wanxiang.go ++++ b/internal/wanxiang/wanxiang.go +@@ -21,23 +21,31 @@ const ( + modelAsset = "wanxiang-lts-zh-hans.gram" + ) + +-func Integrate(ctx context.Context, client *download.Client, workspace, sourceDir string) (*config.ReleaseInfo, error) { +- release, err := client.ReleaseByTag(ctx, modelOwner, modelRepo, modelTag) +- if err != nil { +- return nil, err +- } ++func Integrate(ctx context.Context, client *download.Client, workspace, sourceDir, assetDir string) (*config.ReleaseInfo, error) { ++ info := &config.ReleaseInfo{Owner: modelOwner, Repo: modelRepo, Tag: "local", AssetName: modelAsset} ++ downloadPath := filepath.Join(assetDir, modelAsset) ++ if assetDir == "" { ++ release, err := client.ReleaseByTag(ctx, modelOwner, modelRepo, modelTag) ++ if err != nil { ++ return nil, err ++ } + +- asset, err := download.FindAsset(release, modelAsset) +- if err != nil { +- return nil, err +- } ++ asset, err := download.FindAsset(release, modelAsset) ++ if err != nil { ++ return nil, err ++ } + +- downloadPath := filepath.Join(workspace, "downloads", asset.Name) +- if err := client.DownloadAsset(ctx, asset, downloadPath); err != nil { +- return nil, err ++ downloadPath = filepath.Join(workspace, "downloads", asset.Name) ++ if err := client.DownloadAsset(ctx, asset, downloadPath); err != nil { ++ return nil, err ++ } ++ info.Tag = release.TagName ++ info.AssetName = asset.Name ++ info.Digest = strings.TrimPrefix(asset.Digest, "sha256:") ++ info.URL = asset.BrowserDownloadURL + } + +- targetModelPath := filepath.Join(sourceDir, asset.Name) ++ targetModelPath := filepath.Join(sourceDir, modelAsset) + modelContent, err := os.ReadFile(downloadPath) + if err != nil { + return nil, fmt.Errorf("读取模型文件失败: %w", err) +@@ -50,14 +58,7 @@ func Integrate(ctx context.Context, client *download.Client, workspace, sourceDi + return nil, err + } + +- return &config.ReleaseInfo{ +- Owner: modelOwner, +- Repo: modelRepo, +- Tag: release.TagName, +- AssetName: asset.Name, +- Digest: strings.TrimPrefix(asset.Digest, "sha256:"), +- URL: asset.BrowserDownloadURL, +- }, nil ++ return info, nil + } + + func MergeCustomConfig(path string) error { +diff --git a/main.go b/main.go +index 50f00f3..15439bd 100644 +--- a/main.go ++++ b/main.go +@@ -19,6 +19,7 @@ func main() { + flag.BoolVar(&cfg.EnableWanxiang, "enable-wanxiang", cfg.EnableWanxiang, "下载并启用万象 LTS 语法模型") + flag.IntVar(&cfg.FontSize, "font-size", cfg.FontSize, "Fcitx5 候选框字体大小(8-48)") + flag.StringVar(&cfg.WorkspaceDir, "workspace-dir", cfg.WorkspaceDir, "工作目录,用于下载缓存和日志") ++ flag.StringVar(&cfg.AssetDir, "asset-dir", "", "从此目录读取 full.zip 和万象模型,不访问 GitHub") + flag.Parse() + + if err := app.Run(context.Background(), cfg); err != nil {