321 lines
11 KiB
Diff
321 lines
11 KiB
Diff
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 {
|