Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 0 additions & 5 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
[submodule "third_party/luarocks"]
path = cli/rocks/third_party/luarocks
url = https://github.com/tarantool/luarocks.git
branch = luarocks-3.9.2-tarantool
ignore = dirty
[submodule "cli/aeon/protos"]
path = cli/aeon/protos
url = https://github.com/tarantool/aeon-api-protos.git
Expand Down
2 changes: 2 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ linters:
# cli/manifest/version derives SemVer with go-version so its
# pre-release ordering matches the dependency resolver.
- "github.com/hashicorp/go-version"
- "github.com/tarantool/tt/lib/luarocks"
test:
files:
- "$test"
Expand All @@ -102,3 +103,4 @@ linters:
- "github.com/hashicorp/go-version"
- "github.com/stretchr/testify"
- "github.com/tarantool/tt/cli/manifest"
- "github.com/tarantool/tt/lib/luarocks"
6 changes: 6 additions & 0 deletions .lichen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ allow:
- "BSD-2-Clause"
- "MPL-2.0"
- "CC-BY-SA-4.0"
# ISC: a permissive, OSI/FSF-approved license equivalent to MIT / 2-clause
# BSD. It has no copyleft — the only obligation is to preserve the copyright
# notice, and it imposes nothing on our own code. It enters the tree via
# go-git's transitive dependency github.com/emirpasic/gods, which ships a few
# ISC-licensed files alongside its BSD-2-Clause code.
- "ISC"
override:
- path: "github.com/robfig/config"
licenses: ["MPL-2.0"]
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ repos:
types: [go]
name: "Go: formatting sources"
stages: [pre-commit, manual]
exclude: \.pb\.go$
exclude: (\.pb\.go$|^lib/luarocks/)
entry: bash -c "GOFUMPT_SPLIT_LONG_LINES=on gofumpt $@"
args: [-e, -w, -extra]
additional_dependencies:
Expand All @@ -78,7 +78,7 @@ repos:
- id: golines
name: "Go: shorten long lines"
stages: [pre-commit, manual]
exclude: \.pb\.go$
exclude: (\.pb\.go$|^lib/luarocks/)
args: [--max-len=100, --tab-len=4, --no-reformat-tags]

- repo: https://github.com/golangci/golangci-lint
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ for machine-readable output.
Tarantool JSON Schema instead of the in-tree enumerated path validator. The new validator
is stricter (rejects unknown top-level / scope properties) and produces error messages in
the upstream `<path> [code] <message>` format.
- `tt rocks` now runs on the pure-Go go-luarocks engine (`lib/luarocks`) instead of the
bundled LuaRocks git submodule. Behavior is unchanged; the `cli/rocks/third_party/luarocks`
submodule is removed.

### Fixed

Expand Down
103 changes: 103 additions & 0 deletions cli/manifest/rocks/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package rocks

import (
"fmt"
"log/slog"
"net/url"
"path/filepath"

luarocks "github.com/tarantool/tt/lib/luarocks"
"github.com/tarantool/tt/lib/luarocks/client"
)

// TarantoolInfo carries the Tarantool facts the adapter needs to build a
// luarocks.Config. The caller fills it from cmdcontext (the resolved binary
// path and cached version) and the install prefix (tt's GetTarantoolPrefix);
// tests fill it directly. The library never parses `tarantool --version`
// itself, so these come from one source of truth on the tt side.
type TarantoolInfo struct {
// Executable is the path to the tarantool binary.
Executable string
// Prefix is the Tarantool install prefix, e.g. "/usr".
Prefix string
// Version is the "x.y.z" string used in diagnostic / golden headers.
Version string
}

// IncludeDir returns the Tarantool C-header directory under the prefix
// (<prefix>/include/tarantool), where the builtin/c backends find the headers.
func (info TarantoolInfo) IncludeDir() string {
return filepath.Join(info.Prefix, "include", "tarantool")
}

// ConfigOptions tune BuildConfig beyond the Tarantool facts.
type ConfigOptions struct {
// Tree is the rocks tree root to read from and install into (e.g.
// <app>/.rocks). Required.
Tree string
// WorkingDir is the base directory for relative paths and build staging.
// Required; the library never reads the process cwd.
WorkingDir string
// Servers is the ordered rock-server list; nil falls back to
// DefaultServers().
Servers []string
// Logger receives structured operation logs; nil disables logging.
Logger *slog.Logger
}

// BuildConfig maps the tt environment to a luarocks.Config. Servers default to
// DefaultServers(); rocks.tarantool.org is marked insecure whenever it is in
// the list (upstream luarocks distrusts its certificate).
func BuildConfig(info TarantoolInfo, opts ConfigOptions) luarocks.Config {
servers := opts.Servers
if servers == nil {
servers = DefaultServers()
}

return luarocks.Config{
Tree: opts.Tree,
WorkingDir: opts.WorkingDir,
Servers: servers,
InsecureServers: insecureHosts(servers),
Logger: opts.Logger,
Rockspec: luarocks.RockspecConfig{Env: nil},
Tarantool: luarocks.TarantoolConfig{
Executable: info.Executable,
Prefix: info.Prefix,
IncludeDir: info.IncludeDir(),
Version: info.Version,
},
}
}

// Client builds a lib/luarocks client for the bound config. backend selects the
// engine: pass client.BackendLua for operations the native backend does not
// implement (search/download), client.BackendNative otherwise.
func (a *Adapter) Client(backend client.Backend) (*client.Rocks, error) {
rocksClient, err := client.New(a.cfg, client.WithBackend(backend))
if err != nil {
return nil, fmt.Errorf("rocks: new client: %w", err)
}

return rocksClient, nil
}

// insecureHosts returns the subset of server hosts whose TLS certificate
// the engine should not verify. Only rocks.tarantool.org qualifies today;
// unparseable entries are skipped.
func insecureHosts(servers []string) []string {
var hosts []string

for _, server := range servers {
parsed, err := url.Parse(server)
if err != nil {
continue
}

if parsed.Host == insecureHost {
hosts = append(hosts, parsed.Host)
}
}

return hosts
}
163 changes: 163 additions & 0 deletions cli/manifest/rocks/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package rocks_test

import (
"os"
"path/filepath"
"runtime"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/tarantool/tt/cli/manifest/rocks"
luarocks "github.com/tarantool/tt/lib/luarocks"
"github.com/tarantool/tt/lib/luarocks/build"
"github.com/tarantool/tt/lib/luarocks/client"
"github.com/tarantool/tt/lib/luarocks/rockspec"
)

// sampleTarantool is the Tarantool info the config tests build a config from.
func sampleTarantool() rocks.TarantoolInfo {
return rocks.TarantoolInfo{
Executable: "/opt/tt/bin/tarantool",
Prefix: "/opt/tt",
Version: "3.1.0",
}
}

func TestDefaultServers(t *testing.T) {
t.Parallel()

assert.Equal(t, []string{rocks.ServerTarantool, rocks.ServerLuaRocks}, rocks.DefaultServers())
}

func TestBuildConfigDefaults(t *testing.T) {
t.Parallel()

cfg := rocks.BuildConfig(sampleTarantool(), rocks.ConfigOptions{
Tree: "/app/.rocks",
WorkingDir: "/app",
Servers: nil,
Logger: nil,
})

assert.Equal(t, "/app/.rocks", cfg.Tree)
assert.Equal(t, "/app", cfg.WorkingDir)
assert.Equal(t, rocks.DefaultServers(), cfg.Servers)
// rocks.tarantool.org is in the default list, so it must be marked insecure.
assert.Equal(t, []string{"rocks.tarantool.org"}, cfg.InsecureServers)

assert.Equal(t, "/opt/tt/bin/tarantool", cfg.Tarantool.Executable)
assert.Equal(t, "/opt/tt", cfg.Tarantool.Prefix)
assert.Equal(t, "3.1.0", cfg.Tarantool.Version)
assert.Equal(t, "/opt/tt/include/tarantool", cfg.Tarantool.IncludeDir)
}

func TestBuildConfigCustomServersNoInsecure(t *testing.T) {
t.Parallel()

servers := []string{"http://127.0.0.1:8080/", "https://luarocks.org/"}
cfg := rocks.BuildConfig(sampleTarantool(), rocks.ConfigOptions{
Tree: "/app/.rocks",
WorkingDir: "/app",
Servers: servers,
Logger: nil,
})

assert.Equal(t, servers, cfg.Servers)
// No rocks.tarantool.org among the servers, so none are marked insecure.
assert.Empty(t, cfg.InsecureServers)
}

func TestClientBackends(t *testing.T) {
t.Parallel()

adapter := rocks.New(rocks.BuildConfig(sampleTarantool(), rocks.ConfigOptions{
Tree: "/app/.rocks",
WorkingDir: "/app",
Servers: nil,
Logger: nil,
}))

for _, backend := range []client.Backend{client.BackendNative, client.BackendLua} {
rocksClient, err := adapter.Client(backend)
require.NoError(t, err)
assert.NotNil(t, rocksClient)
}
}

func TestFlagsMatchDeriveFlags(t *testing.T) {
t.Parallel()

cfg := rocks.BuildConfig(sampleTarantool(), rocks.ConfigOptions{
Tree: "/app/.rocks",
WorkingDir: "/app",
Servers: nil,
Logger: nil,
})
adapter := rocks.New(cfg)

flags := adapter.Flags()

// One source of flags: the adapter must return exactly what the library
// derives for the same config.
assert.Equal(t, build.DeriveFlags(cfg), flags)

// Host-independent invariants.
assert.Contains(t, flags.CFLAGS, "-fPIC")
assert.Contains(t, flags.CFLAGS, "-I/opt/tt/include/tarantool")
assert.Equal(t, ".so", flags.Ext)
assert.NotEmpty(t, flags.LIBFLAG)

// Per-OS shared-link flag.
switch runtime.GOOS {
case "darwin":
assert.Equal(t,
[]string{"-bundle", "-undefined", "dynamic_lookup", "-all_load"}, flags.LIBFLAG)
default:
assert.Equal(t, []string{"-shared"}, flags.LIBFLAG)
}
}

func TestChecksum(t *testing.T) {
t.Parallel()

const md5 = "d41d8cd98f00b204e9800998ecf8427e"

withMD5 := evalRockspec(t, `package = "x"
version = "1.0-1"
source = { url = "https://example.com/x-1.0.tar.gz", md5 = "`+md5+`" }
`)

got, ok := rocks.Checksum(withMD5)
assert.True(t, ok)
assert.Equal(t, "md5:"+md5, got)

withoutMD5 := evalRockspec(t, `package = "x"
version = "1.0-1"
source = { url = "https://example.com/x-1.0.tar.gz" }
`)

got, ok = rocks.Checksum(withoutMD5)
assert.False(t, ok)
assert.Empty(t, got)

// A nil spec carries no checksum.
got, ok = rocks.Checksum(nil)
assert.False(t, ok)
assert.Empty(t, got)
}

// evalRockspec writes body to a temp .rockspec and evaluates it into a typed
// rockspec, failing the test on error.
func evalRockspec(t *testing.T, body string) *luarocks.Rockspec {
t.Helper()

path := filepath.Join(t.TempDir(), "x-1.0-1.rockspec")
require.NoError(t, os.WriteFile(path, []byte(body), 0o600))

spec, err := rockspec.Eval(path, luarocks.RockspecConfig{Env: nil})
require.NoError(t, err)

return spec
}
Loading
Loading