Skip to content

Commit 1d52a35

Browse files
committed
cli/manifest/rocks: luarocks adapter over lib/luarocks
Add cli/manifest/rocks, the single point in tt that knows about lib/luarocks. It builds a luarocks.Config from the tt environment, hands out a client factory, and wraps the primitives the resolver, build and registry layers compose. Policy (lock building, namespace layout, CLI surface) stays with the callers; the adapter returns primitives. * BuildConfig maps a TarantoolInfo (executable, prefix, version) plus tree/working-dir to luarocks.Config, defaulting servers to rocks.tarantool.org then luarocks.org and marking rocks.tarantool.org insecure (AS2: upstream luarocks distrusts its certificate). * Ordered multi-server resolve (first-found-wins): query servers in order, first with the rock wins; a dependency's registry field pins a single server. The ordered index satisfies luarocks.RemoteIndex so the resolver (PR4) can consume it directly. Generic rocks from luarocks.org go through the same fetch/build path as Tarantool rocks. * Checksum extracts source.md5 as "md5:<hex>"; absent md5 returns ("", false) so the caller fixes the fallback (content-hash or empty). * Metadata fetches+evals a rock's rockspec; FetchSource fetches the upstream source honoring source.tag/branch. * Search/Download wrap the facade on BackendLua (the native backend does not implement them) so callers need not know the backend. * Flags re-exports build.DeriveFlags bound to the config: one source of per-OS cc flags for both the rock builtin backend and the manifest c / lua-c component backends. Extend the strict cli/manifest .golangci.yml depguard allowlist with github.com/tarantool/tt/lib/luarocks. Tests drive resolve against fake manifest-index servers (httptest) and cover config build, md5 extraction and the per-OS cc flags. Part of TNTP-8230
1 parent dd469ff commit 1d52a35

7 files changed

Lines changed: 798 additions & 0 deletions

File tree

.golangci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ linters:
9090
allow:
9191
- $gostd
9292
- "github.com/pelletier/go-toml/v2"
93+
- "github.com/tarantool/tt/lib/luarocks"
9394
test:
9495
files:
9596
- "$test"
@@ -98,3 +99,4 @@ linters:
9899
- "github.com/pelletier/go-toml/v2"
99100
- "github.com/stretchr/testify"
100101
- "github.com/tarantool/tt/cli/manifest"
102+
- "github.com/tarantool/tt/lib/luarocks"

cli/manifest/rocks/config.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package rocks
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"net/url"
7+
"path/filepath"
8+
9+
luarocks "github.com/tarantool/tt/lib/luarocks"
10+
"github.com/tarantool/tt/lib/luarocks/client"
11+
)
12+
13+
// TarantoolInfo carries the Tarantool facts the adapter needs to build a
14+
// luarocks.Config. The caller fills it from cmdcontext (the resolved binary
15+
// path and cached version) and the install prefix (tt's GetTarantoolPrefix);
16+
// tests fill it directly. The library never parses `tarantool --version`
17+
// itself, so these come from one source of truth on the tt side.
18+
type TarantoolInfo struct {
19+
// Executable is the path to the tarantool binary.
20+
Executable string
21+
// Prefix is the Tarantool install prefix, e.g. "/usr".
22+
Prefix string
23+
// Version is the "x.y.z" string used in diagnostic / golden headers.
24+
Version string
25+
}
26+
27+
// IncludeDir returns the Tarantool C-header directory under the prefix
28+
// (<prefix>/include/tarantool), where the builtin/c backends find the headers.
29+
func (info TarantoolInfo) IncludeDir() string {
30+
return filepath.Join(info.Prefix, "include", "tarantool")
31+
}
32+
33+
// ConfigOptions tune BuildConfig beyond the Tarantool facts.
34+
type ConfigOptions struct {
35+
// Tree is the rocks tree root to read from and install into (e.g.
36+
// <app>/.rocks). Required.
37+
Tree string
38+
// WorkingDir is the base directory for relative paths and build staging.
39+
// Required; the library never reads the process cwd.
40+
WorkingDir string
41+
// Servers is the ordered rock-server list; nil falls back to
42+
// DefaultServers().
43+
Servers []string
44+
// Logger receives structured operation logs; nil disables logging.
45+
Logger *slog.Logger
46+
}
47+
48+
// BuildConfig maps the tt environment to a luarocks.Config. Servers default to
49+
// DefaultServers(); rocks.tarantool.org is marked insecure whenever it is in
50+
// the list (AS2: upstream luarocks distrusts its certificate).
51+
func BuildConfig(info TarantoolInfo, opts ConfigOptions) luarocks.Config {
52+
servers := opts.Servers
53+
if servers == nil {
54+
servers = DefaultServers()
55+
}
56+
57+
return luarocks.Config{
58+
Tree: opts.Tree,
59+
WorkingDir: opts.WorkingDir,
60+
Servers: servers,
61+
InsecureServers: insecureHosts(servers),
62+
Logger: opts.Logger,
63+
Rockspec: luarocks.RockspecConfig{Env: nil},
64+
Tarantool: luarocks.TarantoolConfig{
65+
Executable: info.Executable,
66+
Prefix: info.Prefix,
67+
IncludeDir: info.IncludeDir(),
68+
Version: info.Version,
69+
},
70+
}
71+
}
72+
73+
// Client builds a go-luarocks client for the bound config. backend selects the
74+
// engine: pass client.BackendLua for operations the native backend does not
75+
// implement (search/download), client.BackendNative otherwise.
76+
func (a *Adapter) Client(backend client.Backend) (*client.Rocks, error) {
77+
rocksClient, err := client.New(a.cfg, client.WithBackend(backend))
78+
if err != nil {
79+
return nil, fmt.Errorf("rocks: new client: %w", err)
80+
}
81+
82+
return rocksClient, nil
83+
}
84+
85+
// insecureHosts returns the subset of server hosts whose TLS certificate
86+
// go-luarocks should not verify. Only rocks.tarantool.org qualifies today;
87+
// unparseable entries are skipped.
88+
func insecureHosts(servers []string) []string {
89+
var hosts []string
90+
91+
for _, server := range servers {
92+
parsed, err := url.Parse(server)
93+
if err != nil {
94+
continue
95+
}
96+
97+
if parsed.Host == insecureHost {
98+
hosts = append(hosts, parsed.Host)
99+
}
100+
}
101+
102+
return hosts
103+
}

cli/manifest/rocks/config_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package rocks_test
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"runtime"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
12+
"github.com/tarantool/tt/cli/manifest/rocks"
13+
luarocks "github.com/tarantool/tt/lib/luarocks"
14+
"github.com/tarantool/tt/lib/luarocks/build"
15+
"github.com/tarantool/tt/lib/luarocks/client"
16+
"github.com/tarantool/tt/lib/luarocks/rockspec"
17+
)
18+
19+
// sampleTarantool is the Tarantool info the config tests build a config from.
20+
func sampleTarantool() rocks.TarantoolInfo {
21+
return rocks.TarantoolInfo{
22+
Executable: "/opt/tt/bin/tarantool",
23+
Prefix: "/opt/tt",
24+
Version: "3.1.0",
25+
}
26+
}
27+
28+
func TestDefaultServers(t *testing.T) {
29+
t.Parallel()
30+
31+
assert.Equal(t, []string{rocks.ServerTarantool, rocks.ServerLuaRocks}, rocks.DefaultServers())
32+
}
33+
34+
func TestBuildConfigDefaults(t *testing.T) {
35+
t.Parallel()
36+
37+
cfg := rocks.BuildConfig(sampleTarantool(), rocks.ConfigOptions{
38+
Tree: "/app/.rocks",
39+
WorkingDir: "/app",
40+
Servers: nil,
41+
Logger: nil,
42+
})
43+
44+
assert.Equal(t, "/app/.rocks", cfg.Tree)
45+
assert.Equal(t, "/app", cfg.WorkingDir)
46+
assert.Equal(t, rocks.DefaultServers(), cfg.Servers)
47+
// rocks.tarantool.org is in the default list, so it must be marked insecure.
48+
assert.Equal(t, []string{"rocks.tarantool.org"}, cfg.InsecureServers)
49+
50+
assert.Equal(t, "/opt/tt/bin/tarantool", cfg.Tarantool.Executable)
51+
assert.Equal(t, "/opt/tt", cfg.Tarantool.Prefix)
52+
assert.Equal(t, "3.1.0", cfg.Tarantool.Version)
53+
assert.Equal(t, "/opt/tt/include/tarantool", cfg.Tarantool.IncludeDir)
54+
}
55+
56+
func TestBuildConfigCustomServersNoInsecure(t *testing.T) {
57+
t.Parallel()
58+
59+
servers := []string{"http://127.0.0.1:8080/", "https://luarocks.org/"}
60+
cfg := rocks.BuildConfig(sampleTarantool(), rocks.ConfigOptions{
61+
Tree: "/app/.rocks",
62+
WorkingDir: "/app",
63+
Servers: servers,
64+
Logger: nil,
65+
})
66+
67+
assert.Equal(t, servers, cfg.Servers)
68+
// No rocks.tarantool.org among the servers, so none are marked insecure.
69+
assert.Empty(t, cfg.InsecureServers)
70+
}
71+
72+
func TestClientBackends(t *testing.T) {
73+
t.Parallel()
74+
75+
adapter := rocks.New(rocks.BuildConfig(sampleTarantool(), rocks.ConfigOptions{
76+
Tree: "/app/.rocks",
77+
WorkingDir: "/app",
78+
Servers: nil,
79+
Logger: nil,
80+
}))
81+
82+
for _, backend := range []client.Backend{client.BackendNative, client.BackendLua} {
83+
rocksClient, err := adapter.Client(backend)
84+
require.NoError(t, err)
85+
assert.NotNil(t, rocksClient)
86+
}
87+
}
88+
89+
func TestFlagsMatchDeriveFlags(t *testing.T) {
90+
t.Parallel()
91+
92+
cfg := rocks.BuildConfig(sampleTarantool(), rocks.ConfigOptions{
93+
Tree: "/app/.rocks",
94+
WorkingDir: "/app",
95+
Servers: nil,
96+
Logger: nil,
97+
})
98+
adapter := rocks.New(cfg)
99+
100+
flags := adapter.Flags()
101+
102+
// One source of flags: the adapter must return exactly what the library
103+
// derives for the same config.
104+
assert.Equal(t, build.DeriveFlags(cfg), flags)
105+
106+
// Host-independent invariants.
107+
assert.Contains(t, flags.CFLAGS, "-fPIC")
108+
assert.Contains(t, flags.CFLAGS, "-I/opt/tt/include/tarantool")
109+
assert.Equal(t, ".so", flags.Ext)
110+
assert.NotEmpty(t, flags.LIBFLAG)
111+
112+
// Per-OS shared-link flag.
113+
switch runtime.GOOS {
114+
case "darwin":
115+
assert.Equal(t,
116+
[]string{"-bundle", "-undefined", "dynamic_lookup", "-all_load"}, flags.LIBFLAG)
117+
default:
118+
assert.Equal(t, []string{"-shared"}, flags.LIBFLAG)
119+
}
120+
}
121+
122+
func TestChecksum(t *testing.T) {
123+
t.Parallel()
124+
125+
const md5 = "d41d8cd98f00b204e9800998ecf8427e"
126+
127+
withMD5 := evalRockspec(t, `package = "x"
128+
version = "1.0-1"
129+
source = { url = "https://example.com/x-1.0.tar.gz", md5 = "`+md5+`" }
130+
`)
131+
132+
got, ok := rocks.Checksum(withMD5)
133+
assert.True(t, ok)
134+
assert.Equal(t, "md5:"+md5, got)
135+
136+
withoutMD5 := evalRockspec(t, `package = "x"
137+
version = "1.0-1"
138+
source = { url = "https://example.com/x-1.0.tar.gz" }
139+
`)
140+
141+
got, ok = rocks.Checksum(withoutMD5)
142+
assert.False(t, ok)
143+
assert.Empty(t, got)
144+
145+
// A nil spec carries no checksum.
146+
got, ok = rocks.Checksum(nil)
147+
assert.False(t, ok)
148+
assert.Empty(t, got)
149+
}
150+
151+
// evalRockspec writes body to a temp .rockspec and evaluates it into a typed
152+
// rockspec, failing the test on error.
153+
func evalRockspec(t *testing.T, body string) *luarocks.Rockspec {
154+
t.Helper()
155+
156+
path := filepath.Join(t.TempDir(), "x-1.0-1.rockspec")
157+
require.NoError(t, os.WriteFile(path, []byte(body), 0o600))
158+
159+
spec, err := rockspec.Eval(path, luarocks.RockspecConfig{Env: nil})
160+
require.NoError(t, err)
161+
162+
return spec
163+
}

0 commit comments

Comments
 (0)