Add rime-ice-installer with offline assets and no sudo when its packages are present
This commit is contained in:
1 parent
69c8193e2a
commit
5e0ff0df95
3 files changed
+385
No files matched your search
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"source": "local",
|
||||||
|
"sync": false,
|
||||||
|
"origin": {
|
||||||
|
"aur": "rime-ice-installer",
|
||||||
|
"commit": "08b3592fd79845148efde23768e5f1cfbda8bcd2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Maintainer: Andy Stewart <lazycat.manatee@gmail.com>
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
Reference in new issue
Block a user