1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-07 22:18:30 -04:00
Files
DankMaterialShell/core/internal/plugins/manager_lockfile.go
T
Artrix b7ec57a534 feat(plugins): add portable plugin lockfile (#2976)
* feat(plugins): add portable plugin lockfile

* plugins/lockfile: allow ssh remotes

---------

Co-authored-by: bbedward <bbedward@gmail.com>
2026-08-07 10:55:57 -04:00

300 lines
8.3 KiB
Go

package plugins
import (
"fmt"
"path/filepath"
"strings"
"github.com/spf13/afero"
)
func (m *Manager) lockStore() *LockStore {
lockPath := m.lockPath
if lockPath == "" {
lockPath = filepath.Join(filepath.Dir(m.pluginsDir), "plugins.lock.json")
}
return NewLockStore(m.fs, lockPath)
}
func (m *Manager) GetLockfilePath() string {
return m.lockStore().Path()
}
func (m *Manager) ensureLockfile() (PluginLockfile, error) {
store := m.lockStore()
exists, err := store.Exists()
if err != nil {
return PluginLockfile{}, fmt.Errorf("failed to check plugin lockfile: %w", err)
}
if exists {
return store.Load()
}
lock, _, err := m.SnapshotLockfile()
if err != nil {
return PluginLockfile{}, err
}
if err := store.Write(lock); err != nil {
return PluginLockfile{}, err
}
return lock, nil
}
func (m *Manager) SnapshotLockfile() (PluginLockfile, []string, error) {
lock := NewPluginLockfile()
warnings := []string{}
exists, err := afero.DirExists(m.fs, m.pluginsDir)
if err != nil {
return PluginLockfile{}, nil, fmt.Errorf("failed to check plugins directory: %w", err)
}
if !exists {
return lock, warnings, nil
}
entries, err := afero.ReadDir(m.fs, m.pluginsDir)
if err != nil {
return PluginLockfile{}, nil, fmt.Errorf("failed to read plugins directory: %w", err)
}
for _, entry := range entries {
name := entry.Name()
if name == ".repos" || strings.HasSuffix(name, ".meta") {
continue
}
pluginPath := filepath.Join(m.pluginsDir, name)
manifest := m.getPluginManifest(pluginPath)
if manifest == nil || manifest.ID == "" {
continue
}
locked, repoPath, err := m.lockedPluginFromInstall(pluginPath)
if err != nil {
warnings = append(warnings, fmt.Sprintf("%s: %v", manifest.ID, err))
continue
}
commit, err := m.gitClient.CurrentRevision(repoPath)
if err != nil {
warnings = append(warnings, fmt.Sprintf("%s: cannot read git revision: %v", manifest.ID, err))
continue
}
locked.Commit = commit
lock.SetRepositoryCommit(locked.Repo, commit)
lock.Plugins[manifest.ID] = locked
}
if err := lock.Validate(); err != nil {
return PluginLockfile{}, warnings, err
}
return lock, warnings, nil
}
func (m *Manager) lockedPluginFromInstall(pluginPath string) (LockedPlugin, string, error) {
metaPath := pluginPath + ".meta"
if metaExists, _ := afero.Exists(m.fs, metaPath); metaExists {
data, err := afero.ReadFile(m.fs, metaPath)
if err != nil {
return LockedPlugin{}, "", fmt.Errorf("failed to read legacy metadata: %w", err)
}
metadata := parseLegacyPluginMetadata(string(data))
repo := metadata["repo"]
path := metadata["path"]
repoDir := metadata["repodir"]
if repo == "" || repoDir == "" {
return LockedPlugin{}, "", fmt.Errorf("legacy metadata is incomplete")
}
if err := validatePluginRepo(repo); err != nil {
return LockedPlugin{}, "", err
}
if !isSafePluginPathComponent(repoDir) {
return LockedPlugin{}, "", fmt.Errorf("legacy repository directory is invalid")
}
if err := validatePluginRepoPath(path); err != nil {
return LockedPlugin{}, "", err
}
return LockedPlugin{Repo: repo, Path: path}, filepath.Join(m.pluginsDir, ".repos", repoDir), nil
}
repo, err := m.gitClient.OriginURL(pluginPath)
if err != nil {
return LockedPlugin{}, "", fmt.Errorf("plugin is not a managed git checkout")
}
if err := validatePluginRepo(repo); err != nil {
return LockedPlugin{}, "", err
}
return LockedPlugin{Repo: repo}, pluginPath, nil
}
func parseLegacyPluginMetadata(data string) map[string]string {
metadata := make(map[string]string)
for _, line := range strings.Split(data, "\n") {
key, value, ok := strings.Cut(line, "=")
if ok {
metadata[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
}
return metadata
}
func (m *Manager) recordInstalledPlugin(plugin Plugin, repoPath string) error {
lock, err := m.ensureLockfile()
if err != nil {
return err
}
commit, err := m.gitClient.CurrentRevision(repoPath)
if err != nil {
return fmt.Errorf("failed to read installed plugin revision: %w", err)
}
lock.SetRepositoryCommit(plugin.Repo, commit)
lock.Plugins[plugin.ID] = LockedPlugin{
Repo: plugin.Repo,
Path: plugin.Path,
Commit: commit,
}
return m.lockStore().Write(lock)
}
func (m *Manager) WriteCurrentLockfile(outputPath string) ([]string, error) {
lock, warnings, err := m.SnapshotLockfile()
if err != nil {
return warnings, err
}
if err := m.lockStore().Write(lock); err != nil {
return warnings, err
}
if outputPath != "" && outputPath != m.GetLockfilePath() {
if err := NewLockStore(m.fs, outputPath).Write(lock); err != nil {
return warnings, err
}
}
return warnings, nil
}
func (m *Manager) RestoreFromLockfile(sourcePath string, prune bool) error {
if sourcePath == "" {
sourcePath = m.GetLockfilePath()
}
sourceStore := NewLockStore(m.fs, sourcePath)
exists, err := sourceStore.Exists()
if err != nil {
return fmt.Errorf("failed to check plugin lockfile: %w", err)
}
if !exists {
return fmt.Errorf("plugin lockfile not found: %s", sourcePath)
}
target, err := sourceStore.Load()
if err != nil {
return err
}
current, err := m.ensureLockfile()
if err != nil {
return err
}
for _, id := range target.IDs() {
wanted := target.Plugins[id]
installedPath, err := m.findInstalledPath(id)
if err != nil {
return err
}
if installedPath != "" {
existing, managed := current.Plugins[id]
if !managed {
return fmt.Errorf("plugin %q is already installed but is not managed by the lockfile", id)
}
if existing.Repo != wanted.Repo || existing.Path != wanted.Path {
if err := m.UninstallByIDOrName(id); err != nil {
return err
}
installedPath = ""
}
}
plugin := Plugin{ID: id, Name: id, Repo: wanted.Repo, Path: wanted.Path}
newlyInstalled := installedPath == ""
if installedPath == "" {
if err := m.Install(plugin); err != nil {
return fmt.Errorf("failed to restore plugin %q: %w", id, err)
}
}
repoPath := m.repositoryPath(id, wanted)
previousCommit, err := m.gitClient.CurrentRevision(repoPath)
if err != nil {
if newlyInstalled {
m.UninstallByIDOrName(id) //nolint:errcheck
}
return fmt.Errorf("failed to read current revision for plugin %q: %w", id, err)
}
if err := m.gitClient.CheckoutRevision(repoPath, wanted.Commit); err != nil {
if newlyInstalled {
if rollbackErr := m.UninstallByIDOrName(id); rollbackErr != nil {
return fmt.Errorf("failed to restore plugin %q at %s: %w (also failed to remove the partial install: %v)", id, wanted.Commit, err, rollbackErr)
}
}
return fmt.Errorf("failed to restore plugin %q at %s: %w", id, wanted.Commit, err)
}
rollback := func(restoreErr error) error {
if newlyInstalled {
if rollbackErr := m.UninstallByIDOrName(id); rollbackErr != nil {
return fmt.Errorf("%w (also failed to remove the partial install: %v)", restoreErr, rollbackErr)
}
return restoreErr
}
if rollbackErr := m.gitClient.CheckoutRevision(repoPath, previousCommit); rollbackErr != nil {
return fmt.Errorf("%w (also failed to restore revision %s: %v)", restoreErr, previousCommit, rollbackErr)
}
return restoreErr
}
pluginPath := installedPath
if pluginPath == "" {
pluginPath = filepath.Join(m.pluginsDir, id)
}
if actualID := m.getPluginID(pluginPath); actualID != id {
return rollback(fmt.Errorf("restored plugin %q has manifest id %q", id, actualID))
}
if err := m.recordInstalledPlugin(plugin, repoPath); err != nil {
return rollback(err)
}
current, err = m.lockStore().Load()
if err != nil {
return err
}
}
if prune {
for _, id := range current.IDs() {
if _, wanted := target.Plugins[id]; wanted {
continue
}
if err := m.UninstallByIDOrName(id); err != nil {
return fmt.Errorf("failed to prune plugin %q: %w", id, err)
}
}
return m.lockStore().Write(target)
}
merged, err := m.lockStore().Load()
if err != nil {
return err
}
for id, plugin := range target.Plugins {
merged.SetRepositoryCommit(plugin.Repo, plugin.Commit)
merged.Plugins[id] = plugin
}
return m.lockStore().Write(merged)
}
func (m *Manager) repositoryPath(pluginID string, plugin LockedPlugin) string {
if plugin.Path != "" {
return filepath.Join(m.pluginsDir, ".repos", m.getRepoName(plugin.Repo))
}
if installedPath, err := m.findInDir(m.pluginsDir, pluginID); err == nil && installedPath != "" {
return installedPath
}
return filepath.Join(m.pluginsDir, pluginID)
}