mirror of
https://github.com/Jguer/yay.git
synced 2026-07-16 18:58:36 -04:00
fix(lua): change search paths to resolve lua modules in config dir (#2879)
change search paths to not resolve lua modules in cwd but in the config dir
This commit is contained in:
40
doc/lua.md
40
doc/lua.md
@@ -2,6 +2,8 @@
|
||||
|
||||
yay can optionally load a Lua configuration file, `init.lua`. `init.lua` overlays whatever is in `config.json`, and any command-line flag you pass still wins over `init.lua`.
|
||||
|
||||
yay uses the [Lua 5.1 interpreter](https://www.lua.org/manual/5.1/) to run `init.lua`. The Lua standard library is available.
|
||||
|
||||
## Location
|
||||
|
||||
`init.lua` is looked up, in order:
|
||||
@@ -67,6 +69,44 @@ yay.log.error("policy check failed")
|
||||
message and does not stop execution; use `yay.abort("message")` for controlled
|
||||
hook stops.
|
||||
|
||||
## Requiring modules with `require()`
|
||||
|
||||
<p class="api-since">Available from yay v13.0.1</p>
|
||||
|
||||
`init.lua` can pull in other Lua files with the standard `require()`
|
||||
function. yay prepends the directory that contains `init.lua` to
|
||||
`package.path`, so modules resolve relative to your yay config directory
|
||||
rather than the directory you run yay from.
|
||||
|
||||
Given this layout:
|
||||
|
||||
```
|
||||
$XDG_CONFIG_HOME/yay/
|
||||
init.lua
|
||||
hooks/
|
||||
maintainer_change.lua
|
||||
```
|
||||
|
||||
`init.lua` can do:
|
||||
|
||||
```lua
|
||||
require("hooks.maintainer_change")
|
||||
```
|
||||
|
||||
`require("name")` looks up, in order:
|
||||
|
||||
1. `<config_dir>/name.lua`
|
||||
2. `<config_dir>/name/init.lua`
|
||||
|
||||
Dotted module names map onto the filesystem, so
|
||||
`require("hooks.maintainer_change")` loads
|
||||
`<config_dir>/hooks/maintainer_change.lua`. This lets you split hooks and
|
||||
helpers across multiple files and keep `init.lua` small.
|
||||
|
||||
Required modules run in the same Lua state as `init.lua`, so anything they
|
||||
register through `yay.create_autocmd` or assign to `yay.opt` takes effect just
|
||||
as if it were written inline.
|
||||
|
||||
## Upgrade selection hooks
|
||||
|
||||
<p class="api-since">Available from yay v13.0.0</p>
|
||||
|
||||
@@ -3,6 +3,7 @@ package lua
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Jguer/yay/v13/pkg/text"
|
||||
)
|
||||
@@ -15,6 +16,7 @@ func Load(logger *text.Logger, path string, cfg any) (*Engine, error) {
|
||||
}
|
||||
|
||||
engine := NewWithLogger(luaLogger)
|
||||
engine.SetSearchDir(filepath.Dir(path))
|
||||
|
||||
if err := engine.L.DoFile(path); err != nil {
|
||||
engine.Close()
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
func writeLuaFile(t *testing.T, body string) string {
|
||||
@@ -75,3 +76,80 @@ func TestLoadReturnsLiveEngine(t *testing.T) {
|
||||
require.Equal(t, "/tmp/yay", cfg.BuildDir)
|
||||
require.True(t, engine.HasAutocmd(EventAURPreInstall))
|
||||
}
|
||||
|
||||
func TestLoadIntoResolvesRequireRelativeToConfigDir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.Mkdir(filepath.Join(dir, "hooks"), 0o755))
|
||||
|
||||
hookBody := []byte(`
|
||||
yay.create_autocmd("AURPreInstall", {
|
||||
desc = "from required module",
|
||||
callback = function() end,
|
||||
})
|
||||
`)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "hooks", "maintainer_change.lua"), hookBody, 0o600))
|
||||
|
||||
initBody := []byte(`
|
||||
require("hooks.maintainer_change")
|
||||
yay.opt.build_dir = "/tmp/yay"
|
||||
`)
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "init.lua"), initBody, 0o600))
|
||||
|
||||
cfg := &testConfig{}
|
||||
|
||||
// Run from a different working directory to prove require resolves
|
||||
// relative to the config dir, not the process CWD.
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = os.Chdir(wd) })
|
||||
require.NoError(t, os.Chdir(t.TempDir()))
|
||||
|
||||
err = LoadInto(nil, filepath.Join(dir, "init.lua"), cfg)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "/tmp/yay", cfg.BuildDir)
|
||||
}
|
||||
|
||||
func TestLoadDoesNotResolveRequireFromCWD(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// A module that only exists in the process CWD must NOT be resolvable,
|
||||
// since the CWD is not the location of init.lua.
|
||||
cwd := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(cwd, "stray.lua"), []byte(`return {}`), 0o600))
|
||||
|
||||
wd, err := os.Getwd()
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = os.Chdir(wd) })
|
||||
require.NoError(t, os.Chdir(cwd))
|
||||
|
||||
path := writeLuaFile(t, `require("stray")`)
|
||||
|
||||
cfg := &testConfig{}
|
||||
err = LoadInto(nil, path, cfg)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "stray")
|
||||
}
|
||||
|
||||
func TestSetSearchDirBuildsPackagePath(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
engine := New()
|
||||
t.Cleanup(engine.Close)
|
||||
|
||||
pkg := engine.L.GetGlobal("package")
|
||||
original := string(engine.L.GetField(pkg, "path").(lua.LString))
|
||||
|
||||
const dir = "/etc/yay"
|
||||
engine.SetSearchDir(dir)
|
||||
|
||||
got := string(engine.L.GetField(pkg, "path").(lua.LString))
|
||||
|
||||
// The produced path is the init.lua directory patterns followed by the
|
||||
// absolute system entries — never the default "./?.lua" CWD entry.
|
||||
expected := filepath.Join(dir, "?.lua") + ";" +
|
||||
filepath.Join(dir, "?", "init.lua") + ";" +
|
||||
absolutePatterns(original)
|
||||
require.Equal(t, expected, got)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package lua
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/Jguer/yay/v13/pkg/text"
|
||||
|
||||
@@ -107,6 +109,37 @@ func (e *Engine) optTable() (*lua.LTable, bool) {
|
||||
return optTbl, ok
|
||||
}
|
||||
|
||||
// SetSearchDir makes require() resolve modules relative to dir (e.g.
|
||||
// require("hooks.maintainer_change")) and drops the default "./?.lua" entry so
|
||||
// require() searches only dir and the absolute system paths, never an unrelated
|
||||
// CWD. When dir is the CWD, the prepended patterns already cover it.
|
||||
func (e *Engine) SetSearchDir(dir string) {
|
||||
pkg := e.L.GetGlobal("package")
|
||||
|
||||
current, ok := e.L.GetField(pkg, "path").(lua.LString)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
pattern := filepath.Join(dir, "?.lua") + ";" + filepath.Join(dir, "?", "init.lua")
|
||||
e.L.SetField(pkg, "path", lua.LString(pattern+";"+absolutePatterns(string(current))))
|
||||
}
|
||||
|
||||
// absolutePatterns keeps only the absolute entries of a package.path string,
|
||||
// dropping CWD-relative ones such as "./?.lua".
|
||||
func absolutePatterns(path string) string {
|
||||
segments := strings.Split(path, ";")
|
||||
kept := segments[:0]
|
||||
|
||||
for _, seg := range segments {
|
||||
if filepath.IsAbs(seg) {
|
||||
kept = append(kept, seg)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(kept, ";")
|
||||
}
|
||||
|
||||
func luaKeyForField(field *reflect.StructField) string {
|
||||
name := field.Tag.Get("lua")
|
||||
if name != "" && name != "-" {
|
||||
|
||||
Reference in New Issue
Block a user