diff --git a/.gitmodules b/.gitmodules index df6fbb139..ae524ae95 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/.golangci.yml b/.golangci.yml index 61b041f3e..89ac4ace6 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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" @@ -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" diff --git a/.lichen.yaml b/.lichen.yaml index 5fcdff1aa..0427226df 100644 --- a/.lichen.yaml +++ b/.lichen.yaml @@ -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"] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8bdb348b5..da73a9b5c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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: @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e4891524..63998eed9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ` [code] ` 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 diff --git a/cli/manifest/rocks/config.go b/cli/manifest/rocks/config.go new file mode 100644 index 000000000..7fb24a974 --- /dev/null +++ b/cli/manifest/rocks/config.go @@ -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 +// (/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. + // /.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 +} diff --git a/cli/manifest/rocks/config_test.go b/cli/manifest/rocks/config_test.go new file mode 100644 index 000000000..dcba88ee2 --- /dev/null +++ b/cli/manifest/rocks/config_test.go @@ -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 +} diff --git a/cli/manifest/rocks/registry.go b/cli/manifest/rocks/registry.go new file mode 100644 index 000000000..2e0cf9c16 --- /dev/null +++ b/cli/manifest/rocks/registry.go @@ -0,0 +1,127 @@ +package rocks + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + + luarocks "github.com/tarantool/tt/lib/luarocks" + "github.com/tarantool/tt/lib/luarocks/client" + "github.com/tarantool/tt/lib/luarocks/fetch" + "github.com/tarantool/tt/lib/luarocks/rockspec" +) + +// Metadata fetches a resolved rock's rockspec and evaluates it into a typed +// Rockspec, with the runtime platforms merged in. The result carries +// source.md5, which Checksum turns into the lock checksum. A temporary working +// directory is used and removed; the spec is fully materialized before return. +func (a *Adapter) Metadata(ctx context.Context, rock ResolvedRock) (*luarocks.Rockspec, error) { + dir, err := os.MkdirTemp("", "tt-rocks-meta-") + if err != nil { + return nil, fmt.Errorf("rocks: temp dir: %w", err) + } + + defer func() { _ = os.RemoveAll(dir) }() + + unpacked, err := fetch.FetchWith(ctx, rock.URL, dir, fetch.Options{ + InsecureServers: a.cfg.InsecureServers, + UserAgent: "", + Tag: "", + Branch: "", + }) + if err != nil { + return nil, fmt.Errorf("rocks: fetch %s: %w", rock.URL, err) + } + + specPath, err := findRockspec(unpacked) + if err != nil { + return nil, err + } + + spec, err := rockspec.Eval(specPath, a.cfg.Rockspec) + if err != nil { + return nil, fmt.Errorf("rocks: eval %s: %w", specPath, err) + } + + rockspec.MergePlatforms(spec, rockspec.RuntimePlatforms()) + + return spec, nil +} + +// FetchSource downloads and unpacks a rock's upstream source (the rockspec +// source.url) into destDir, honoring source.tag / source.branch and the +// adapter's insecure-server list. It returns the unpacked working-tree path. +// Generic rocks from luarocks.org go through the same path as Tarantool rocks. +func (a *Adapter) FetchSource( + ctx context.Context, spec *luarocks.Rockspec, destDir string, +) (string, error) { + unpacked, err := fetch.FetchWith(ctx, spec.Source.URL, destDir, fetch.Options{ + InsecureServers: a.cfg.InsecureServers, + UserAgent: "", + Tag: spec.Source.Tag, + Branch: spec.Source.Branch, + }) + if err != nil { + return "", fmt.Errorf("rocks: fetch source %s: %w", spec.Source.URL, err) + } + + return unpacked, nil +} + +// Search queries the configured registries for rocks matching pattern. It runs +// on the lua backend (the native backend does not implement search); the caller +// need not know which backend is used. +func (a *Adapter) Search( + ctx context.Context, pattern string, opts client.SearchOpts, +) ([]client.SearchResult, error) { + rocksClient, err := a.Client(client.BackendLua) + if err != nil { + return nil, err + } + + results, err := rocksClient.Search(ctx, pattern, opts) + if err != nil { + return nil, fmt.Errorf("rocks: search %q: %w", pattern, err) + } + + return results, nil +} + +// Download fetches a rock file for name into the working directory and returns +// its path. Like Search it runs on the lua backend behind the wrapper. +func (a *Adapter) Download( + ctx context.Context, name string, opts client.DownloadOpts, +) (string, error) { + rocksClient, err := a.Client(client.BackendLua) + if err != nil { + return "", err + } + + path, err := rocksClient.Download(ctx, name, opts) + if err != nil { + return "", fmt.Errorf("rocks: download %q: %w", name, err) + } + + return path, nil +} + +// findRockspec returns the single top-level *.rockspec in dir. It does not +// recurse: a bare .rockspec download is one top-level file, and a .src.rock +// carries its rockspec at the archive root, while the bundled source tree may +// ship unrelated rockspecs deeper down. +func findRockspec(dir string) (string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return "", fmt.Errorf("rocks: read %s: %w", dir, err) + } + + for _, entry := range entries { + if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".rockspec") { + return filepath.Join(dir, entry.Name()), nil + } + } + + return "", fmt.Errorf("rocks: %w: %s", errNoRockspec, dir) +} diff --git a/cli/manifest/rocks/resolve.go b/cli/manifest/rocks/resolve.go new file mode 100644 index 000000000..f9aecf94d --- /dev/null +++ b/cli/manifest/rocks/resolve.go @@ -0,0 +1,158 @@ +package rocks + +import ( + "context" + "fmt" + + luarocks "github.com/tarantool/tt/lib/luarocks" + "github.com/tarantool/tt/lib/luarocks/deps" + "github.com/tarantool/tt/lib/luarocks/remote" +) + +// ResolvedRock is a rock chosen for a dependency: the name, the version picked +// from the winning server, and the URL of its resource on that server. +type ResolvedRock struct { + // Name is the rock name. + Name string + // Version is the chosen version. + Version luarocks.Version + // URL is the .rock / .rockspec resource on the originating server. + URL string +} + +// orderedIndex queries a list of single-server indexes in order and returns +// the first server's results for a name (first-found-wins), instead of +// aggregating across all servers the way a multi-server HTTPRemoteIndex would. +// A server that errors (e.g. unreachable) is skipped; the last error is +// surfaced only when no server yields a rock. +type orderedIndex struct { + indexes []luarocks.RemoteIndex +} + +// newOrderedIndex builds an ordered index over the given per-server indexes. +func newOrderedIndex(indexes ...luarocks.RemoteIndex) *orderedIndex { + return &orderedIndex{indexes: indexes} +} + +// httpIndexes builds one HTTPRemoteIndex per server so they can be queried in +// order, each carrying the shared insecure-server list. +func httpIndexes(servers, insecure []string) []luarocks.RemoteIndex { + out := make([]luarocks.RemoteIndex, 0, len(servers)) + + for _, server := range servers { + out = append(out, &remote.HTTPRemoteIndex{ + Servers: []string{server}, + InsecureServers: insecure, + UserAgent: "", + LuaVersion: "", + }) + } + + return out +} + +// Query asks each server in order and returns the first non-empty result, +// satisfying luarocks.RemoteIndex so the resolver can consume it too. +func (o *orderedIndex) Query( + ctx context.Context, name string, +) ([]luarocks.VersionedRock, error) { + var lastErr error + + for _, index := range o.indexes { + found, err := index.Query(ctx, name) + if err != nil { + lastErr = err + + continue + } + + if len(found) > 0 { + return found, nil + } + } + + if lastErr != nil { + return nil, fmt.Errorf("query %q across servers: %w", name, lastErr) + } + + return nil, nil +} + +// Resolve finds the newest version of name that satisfies constraintExpr, +// querying servers in order (first-found-wins). A non-empty registry overrides +// the server list with that single server. An empty constraintExpr matches any +// version. +func (a *Adapter) Resolve( + ctx context.Context, name, constraintExpr, registry string, +) (ResolvedRock, error) { + index := a.index + if registry != "" { + index = newOrderedIndex(httpIndexes([]string{registry}, a.cfg.InsecureServers)...) + } + + return resolveWith(ctx, index, name, constraintExpr) +} + +// resolveWith is the index-agnostic core of Resolve, separated so tests can +// drive it with a fake index. +func resolveWith( + ctx context.Context, index luarocks.RemoteIndex, name, constraintExpr string, +) (ResolvedRock, error) { + candidates, err := index.Query(ctx, name) + if err != nil { + return ResolvedRock{}, fmt.Errorf("rocks: resolve %q: %w", name, err) + } + + if len(candidates) == 0 { + return ResolvedRock{}, fmt.Errorf("%q: %w", name, ErrNotFound) + } + + constraints, err := deps.ParseConstraints(constraintExpr) + if err != nil { + return ResolvedRock{}, fmt.Errorf("rocks: parse constraints %q: %w", constraintExpr, err) + } + + best, ok := pickNewest(candidates, constraints) + if !ok { + return ResolvedRock{}, fmt.Errorf("%q %q: %w", name, constraintExpr, ErrNoMatch) + } + + return ResolvedRock{Name: best.Name, Version: best.Version, URL: best.URL}, nil +} + +// pickNewest returns the highest-versioned candidate satisfying every +// constraint. An empty constraint list matches all. ok is false when nothing +// matches. +func pickNewest( + candidates []luarocks.VersionedRock, constraints []luarocks.VersionConstraint, +) (luarocks.VersionedRock, bool) { + var best luarocks.VersionedRock + + found := false + + for _, candidate := range candidates { + if !deps.Match(candidate.Version, constraints) { + continue + } + + if !found || deps.Compare(candidate.Version, best.Version) > 0 { + best = candidate + found = true + } + } + + return best, found +} + +// Checksum renders the source checksum for a resolved rock's rockspec as +// "md5:", taken from source.md5 (what LuaRocks publishes). ok is false +// when the rock publishes no md5; the caller decides the fallback (a +// content-hash of the materialized tree, or an empty checksum). The adapter +// only reports what the rock carried. +func Checksum(spec *luarocks.Rockspec) (string, bool) { + if spec == nil || spec.Source.MD5 == "" { + return "", false + } + + return "md5:" + spec.Source.MD5, true +} diff --git a/cli/manifest/rocks/resolve_test.go b/cli/manifest/rocks/resolve_test.go new file mode 100644 index 000000000..b225e99d6 --- /dev/null +++ b/cli/manifest/rocks/resolve_test.go @@ -0,0 +1,164 @@ +package rocks_test + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "sync/atomic" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tarantool/tt/cli/manifest/rocks" +) + +// manifestServer starts a fake rock server that serves a single JSON manifest +// (manifest-5.1.json) built from repo, the (name → version → arch) shape +// HTTPRemoteIndex consumes. hits counts every request the server receives, so +// tests can assert which servers were consulted. +func manifestServer(t *testing.T, body string, hits *int32) *httptest.Server { + t.Helper() + + handler := func(writer http.ResponseWriter, request *http.Request) { + atomic.AddInt32(hits, 1) + + if strings.HasSuffix(request.URL.Path, "/manifest-5.1.json") { + _, _ = writer.Write([]byte(body)) + + return + } + + http.NotFound(writer, request) + } + + server := httptest.NewServer(http.HandlerFunc(handler)) + t.Cleanup(server.Close) + + return server +} + +// repoJSON renders a one-rock manifest body for name at the given versions, +// each advertised as a rockspec arch. +func repoJSON(name string, versions ...string) string { + entries := make([]string, 0, len(versions)) + for _, version := range versions { + entries = append(entries, `"`+version+`":[{"arch":"rockspec"}]`) + } + + return `{"repository":{"` + name + `":{` + strings.Join(entries, ",") + `}}}` +} + +// newAdapter builds an adapter whose ordered server list is exactly servers. +func newAdapter(servers ...string) *rocks.Adapter { + return rocks.New(rocks.BuildConfig(rocks.TarantoolInfo{ + Executable: "tarantool", + Prefix: "/usr", + Version: "3.1.0", + }, rocks.ConfigOptions{ + Tree: "/app/.rocks", + WorkingDir: "/app", + Servers: servers, + Logger: nil, + })) +} + +func TestResolveFirstServerWins(t *testing.T) { + t.Parallel() + + var hits1, hits2 int32 + + first := manifestServer(t, repoJSON("metrics", "1.0.0-1"), &hits1) + second := manifestServer(t, repoJSON("metrics", "2.0.0-1"), &hits2) + + adapter := newAdapter(first.URL, second.URL) + + resolved, err := adapter.Resolve(context.Background(), "metrics", "", "") + require.NoError(t, err) + + assert.True(t, strings.HasPrefix(resolved.URL, first.URL), + "want %s under %s", resolved.URL, first.URL) + assert.Equal(t, "1.0.0-1", resolved.Version.Raw) + // First server had the rock, so the second is never consulted. + assert.Zero(t, atomic.LoadInt32(&hits2)) +} + +func TestResolveSecondServerWhenFirstMissing(t *testing.T) { + t.Parallel() + + var hits1, hits2 int32 + + first := manifestServer(t, repoJSON("other", "1.0.0-1"), &hits1) + second := manifestServer(t, repoJSON("metrics", "2.0.0-1"), &hits2) + + adapter := newAdapter(first.URL, second.URL) + + resolved, err := adapter.Resolve(context.Background(), "metrics", "", "") + require.NoError(t, err) + + assert.True(t, strings.HasPrefix(resolved.URL, second.URL)) + assert.Equal(t, "2.0.0-1", resolved.Version.Raw) + // First server was consulted and lacked the rock, so it was queried. + assert.Positive(t, atomic.LoadInt32(&hits1)) +} + +func TestResolveRegistryOverride(t *testing.T) { + t.Parallel() + + var hits1, hits2 int32 + + first := manifestServer(t, repoJSON("metrics", "1.0.0-1"), &hits1) + second := manifestServer(t, repoJSON("metrics", "2.0.0-1"), &hits2) + + adapter := newAdapter(first.URL, second.URL) + + // The registry override pins the second server; the default list is ignored. + resolved, err := adapter.Resolve(context.Background(), "metrics", "", second.URL) + require.NoError(t, err) + + assert.True(t, strings.HasPrefix(resolved.URL, second.URL)) + assert.Equal(t, "2.0.0-1", resolved.Version.Raw) + assert.Zero(t, atomic.LoadInt32(&hits1), "registry override must not query the default servers") +} + +func TestResolveConstraintPicksNewestMatch(t *testing.T) { + t.Parallel() + + var hits int32 + + server := manifestServer(t, repoJSON("metrics", "1.0.0-1", "2.0.0-1"), &hits) + + adapter := newAdapter(server.URL) + + resolved, err := adapter.Resolve(context.Background(), "metrics", "<2.0.0", "") + require.NoError(t, err) + + assert.Equal(t, "1.0.0-1", resolved.Version.Raw) +} + +func TestResolveNotFound(t *testing.T) { + t.Parallel() + + var hits int32 + + server := manifestServer(t, repoJSON("other", "1.0.0-1"), &hits) + + adapter := newAdapter(server.URL) + + _, err := adapter.Resolve(context.Background(), "metrics", "", "") + assert.ErrorIs(t, err, rocks.ErrNotFound) +} + +func TestResolveNoMatch(t *testing.T) { + t.Parallel() + + var hits int32 + + server := manifestServer(t, repoJSON("metrics", "1.0.0-1"), &hits) + + adapter := newAdapter(server.URL) + + _, err := adapter.Resolve(context.Background(), "metrics", ">=2.0.0", "") + assert.ErrorIs(t, err, rocks.ErrNoMatch) +} diff --git a/cli/manifest/rocks/rocks.go b/cli/manifest/rocks/rocks.go new file mode 100644 index 000000000..4ff36e096 --- /dev/null +++ b/cli/manifest/rocks/rocks.go @@ -0,0 +1,81 @@ +// Package rocks is the single point in tt that knows about the lib/luarocks +// engine (github.com/tarantool/tt/lib/luarocks). It builds a luarocks.Config +// from the tt environment and the manifest platform, hands out a client +// factory, and wraps the primitives the resolver, build and registry layers +// compose: ordered multi-server resolve, rock metadata and source checksum, +// source fetch, and registry search/download. It also re-exports the +// compile/link flag derivation so the manifest c / lua-c build backends share +// one source of per-OS flags with the rock builtin backend. +// +// The package returns primitives; policy lives in the callers. Resolving a +// lock, laying components out by namespace, and the search/download CLI +// surface are built by other packages on top of these calls. +package rocks + +import ( + "errors" + + luarocks "github.com/tarantool/tt/lib/luarocks" + "github.com/tarantool/tt/lib/luarocks/build" +) + +// Default rock servers, queried in order (first-found-wins) unless a +// dependency pins its own registry. rocks.tarantool.org carries the Tarantool +// rocks; luarocks.org carries generic ones (inspect, luasocket, …). +const ( + // ServerTarantool is the default primary rock server. + ServerTarantool = "https://rocks.tarantool.org/" + // ServerLuaRocks is the default fallback rock server for generic rocks. + ServerLuaRocks = "https://luarocks.org/" + + // insecureHost is the one server whose TLS certificate the engine does not + // verify: upstream luarocks distrusts rocks.tarantool.org's cert. + insecureHost = "rocks.tarantool.org" +) + +var ( + // ErrNotFound reports that no queried server has the requested rock. + ErrNotFound = errors.New("rock not found on any server") + // ErrNoMatch reports that a rock exists but no version satisfies the + // requested constraints. + ErrNoMatch = errors.New("no version satisfies the constraints") + // errNoRockspec reports that a fetched rock artifact carries no rockspec. + errNoRockspec = errors.New("no rockspec in fetched artifact") +) + +// DefaultServers returns the ordered default server list: Tarantool rocks +// first, then generic luarocks.org. +func DefaultServers() []string { + return []string{ServerTarantool, ServerLuaRocks} +} + +// Adapter wraps a single luarocks.Config and exposes the rock primitives bound +// to it. Construct it with New; every method works against the same config, so +// the cc flags a rock is built with match the flags a component is built with. +type Adapter struct { + cfg luarocks.Config + index luarocks.RemoteIndex +} + +// New builds an Adapter around cfg. The ordered multi-server index is derived +// from cfg.Servers / cfg.InsecureServers and reused across Resolve calls that +// do not pin a registry. +func New(cfg luarocks.Config) *Adapter { + return &Adapter{ + cfg: cfg, + index: newOrderedIndex(httpIndexes(cfg.Servers, cfg.InsecureServers)...), + } +} + +// Config returns the luarocks.Config the Adapter is bound to. +func (a *Adapter) Config() luarocks.Config { + return a.cfg +} + +// Flags derives the compile/link toolchain flags (-fPIC, -I, the +// per-OS shared-link flag, the .so extension) for the bound config. It is the +// one source of flags for both the rock builtin backend and the manifest +// c / lua-c component backends, so per-OS logic is not duplicated. +func (a *Adapter) Flags() build.Flags { + return build.DeriveFlags(a.cfg) +} diff --git a/cli/rocks/extra/macosx.lua b/cli/rocks/extra/macosx.lua deleted file mode 100644 index 0532254c6..000000000 --- a/cli/rocks/extra/macosx.lua +++ /dev/null @@ -1,7 +0,0 @@ --- It is a workaround for https://github.com/tarantool/tt/issues/640 --- Probably won't be needed after these patches will go into release: --- https://github.com/yuin/gopher-lua/pull/456 --- https://github.com/yuin/gopher-lua/pull/458 --- Original macosx.lua is not needed for any luarocks operation. -local macosx = {} -return macosx diff --git a/cli/rocks/rocks.go b/cli/rocks/rocks.go index b0472a094..e7cf9fb92 100644 --- a/cli/rocks/rocks.go +++ b/cli/rocks/rocks.go @@ -1,9 +1,9 @@ package rocks import ( + "context" "embed" "fmt" - "io/fs" "os" "os/exec" "path/filepath" @@ -14,13 +14,10 @@ import ( "github.com/tarantool/tt/cli/cmdcontext" "github.com/tarantool/tt/cli/config" "github.com/tarantool/tt/cli/util" - lua "github.com/yuin/gopher-lua" + luarocks "github.com/tarantool/tt/lib/luarocks" + "github.com/tarantool/tt/lib/luarocks/client" ) -//go:embed extra/* -//go:embed third_party/luarocks/src/* -var luarocks embed.FS - //go:embed completions/* var EmbedCompletions embed.FS @@ -120,18 +117,12 @@ func GetTarantoolPrefix(cli *cmdcontext.CliCtx, cliOpts *config.CliOpts) (string return prefixDir, nil } -// getwdWrapperForLua is getwd call wrapper. -func getwdWrapperForLua(L *lua.LState) int { - dir, _ := os.Getwd() - L.Push(lua.LString(dir)) - return 1 -} - -// Execute LuaRocks command. All args will be processed by LuaRocks. +// Exec runs a LuaRocks command through the embedded LuaRocks engine +// (lib/luarocks). All args are passed verbatim to the upstream LuaRocks CLI +// dispatcher, which parses them and prints its own output. This is the +// transitional `tt rocks` escape-hatch; the new manifest commands go through +// cli/manifest/rocks instead. func Exec(cmdCtx *cmdcontext.CmdCtx, cliOpts *config.CliOpts, args []string) error { - var cmd string - var rocks_cmd string - cliOpts.Repo.Rocks = getRocksRepoPath(cliOpts.Repo.Rocks) var err error @@ -139,14 +130,6 @@ func Exec(cmdCtx *cmdcontext.CmdCtx, cliOpts *config.CliOpts, args []string) err return err } - for idx, arg := range args { - if (len(args))-1 == idx { - cmd += fmt.Sprintf("'%s'", arg) - } else { - cmd += fmt.Sprintf("'%s', ", arg) - } - } - version, err := cmdCtx.Cli.TarantoolCli.GetVersion() if err != nil { return err @@ -160,139 +143,39 @@ func Exec(cmdCtx *cmdcontext.CmdCtx, cliOpts *config.CliOpts, args []string) err return err } + workingDir, err := os.Getwd() + if err != nil { + return err + } + + // LuaRocks builtin/cmake rocks (and tt's own cmake hand-off in + // build/local.go) read these at compile time via extra/hardcoded.lua's + // contract. The library never sets process env; only this shim does, + // mirroring the previous embedded-luarocks behavior. os.Setenv("TT_CLI_TARANTOOL_VERSION", version.Str) os.Setenv("TT_CLI_TARANTOOL_PREFIX", tarantoolPrefixDir) os.Setenv("TT_CLI_TARANTOOL_INCLUDE", tarantoolIncludeDir) os.Setenv("TARANTOOL_DIR", filepath.Dir(tarantoolIncludeDir)) os.Setenv("TT_CLI_TARANTOOL_PATH", filepath.Dir(cmdCtx.Cli.TarantoolCli.Executable)) - if len(args) == 0 { - rocks_cmd = fmt.Sprintf("t=require('extra.wrapper').exec('%s')", os.Args[0]) - } else { - rocks_cmd = fmt.Sprintf("t=require('extra.wrapper').exec('%s', %s)", - os.Args[0], cmd) + cfg := luarocks.Config{ + Tree: filepath.Join(workingDir, ".rocks"), + WorkingDir: workingDir, + Tarantool: luarocks.TarantoolConfig{ + Executable: cmdCtx.Cli.TarantoolCli.Executable, + Prefix: tarantoolPrefixDir, + IncludeDir: tarantoolIncludeDir, + Version: version.Str, + }, } - extra_path := "extra/" - rocks_path := "third_party/luarocks/src/" - // spell-checker:ignore manif deplocks sscm - rocks_preload := map[string]string{ - "extra.wrapper": extra_path + "wrapper.lua", - "luarocks.core.hardcoded": extra_path + "hardcoded.lua", - "luarocks.core.util": rocks_path + "luarocks/core/util.lua", - "luarocks.core.persist": rocks_path + "luarocks/core/persist.lua", - "luarocks.core.sysdetect": rocks_path + "luarocks/core/sysdetect.lua", - "luarocks.core.cfg": rocks_path + "luarocks/core/cfg.lua", - "luarocks.core.dir": rocks_path + "luarocks/core/dir.lua", - "luarocks.core.path": rocks_path + "luarocks/core/path.lua", - "luarocks.core.manif": rocks_path + "luarocks/core/manif.lua", - "luarocks.core.vers": rocks_path + "luarocks/core/vers.lua", - "luarocks.util": rocks_path + "luarocks/util.lua", - "luarocks.loader": rocks_path + "luarocks/loader.lua", - "luarocks.dir": rocks_path + "luarocks/dir.lua", - "luarocks.path": rocks_path + "luarocks/path.lua", - "luarocks.fs": rocks_path + "luarocks/fs.lua", - "luarocks.persist": rocks_path + "luarocks/persist.lua", - "luarocks.fun": rocks_path + "luarocks/fun.lua", - "luarocks.tools.patch": rocks_path + "luarocks/tools/patch.lua", - "luarocks.tools.zip": rocks_path + "luarocks/tools/zip.lua", - "luarocks.tools.tar": rocks_path + "luarocks/tools/tar.lua", - "luarocks.fs.unix": rocks_path + "luarocks/fs/unix.lua", - "luarocks.fs.macosx": extra_path + "macosx.lua", - "luarocks.fs.unix.tools": rocks_path + "luarocks/fs/unix/tools.lua", - "luarocks.fs.lua": rocks_path + "luarocks/fs/lua.lua", - "luarocks.fs.tools": rocks_path + "luarocks/fs/tools.lua", - "luarocks.queries": rocks_path + "luarocks/queries.lua", - "luarocks.type_check": rocks_path + "luarocks/type_check.lua", - "luarocks.type.rockspec": rocks_path + "luarocks/type/rockspec.lua", - "luarocks.rockspecs": rocks_path + "luarocks/rockspecs.lua", - "luarocks.signing": rocks_path + "luarocks/signing.lua", - "luarocks.fetch": rocks_path + "luarocks/fetch.lua", - "luarocks.type.manifest": rocks_path + "luarocks/type/manifest.lua", - "luarocks.manif": rocks_path + "luarocks/manif.lua", - "luarocks.build.builtin": rocks_path + "luarocks/build/builtin.lua", - "luarocks.deps": rocks_path + "luarocks/deps.lua", - "luarocks.deplocks": rocks_path + "luarocks/deplocks.lua", - "luarocks.cmd": rocks_path + "luarocks/cmd.lua", - "luarocks.argparse": rocks_path + "luarocks/argparse.lua", - "luarocks.test.busted": rocks_path + "luarocks/test/busted.lua", - "luarocks.test.command": rocks_path + "luarocks/test/command.lua", - "luarocks.results": rocks_path + "luarocks/results.lua", - "luarocks.search": rocks_path + "luarocks/search.lua", - "luarocks.repos": rocks_path + "luarocks/repos.lua", - "luarocks.cmd.show": rocks_path + "luarocks/cmd/show.lua", - "luarocks.cmd.path": rocks_path + "luarocks/cmd/path.lua", - "luarocks.cmd.write_rockspec": rocks_path + "luarocks/cmd/write_rockspec.lua", - "luarocks.manif.writer": rocks_path + "luarocks/manif/writer.lua", - "luarocks.remove": rocks_path + "luarocks/remove.lua", - "luarocks.pack": rocks_path + "luarocks/pack.lua", - "luarocks.build": rocks_path + "luarocks/build.lua", - "luarocks.cmd.make": rocks_path + "luarocks/cmd/make.lua", - "luarocks.cmd.build": rocks_path + "luarocks/cmd/build.lua", - "luarocks.cmd.install": rocks_path + "luarocks/cmd/install.lua", - "luarocks.cmd.list": rocks_path + "luarocks/cmd/list.lua", - "luarocks.download": rocks_path + "luarocks/download.lua", - "luarocks.cmd.download": rocks_path + "luarocks/cmd/download.lua", - "luarocks.cmd.search": rocks_path + "luarocks/cmd/search.lua", - "luarocks.cmd.pack": rocks_path + "luarocks/cmd/pack.lua", - "luarocks.cmd.new_version": rocks_path + "luarocks/cmd/new_version.lua", - "luarocks.cmd.purge": rocks_path + "luarocks/cmd/purge.lua", - "luarocks.cmd.init": rocks_path + "luarocks/cmd/init.lua", - "luarocks.cmd.lint": rocks_path + "luarocks/cmd/lint.lua", - "luarocks.test": rocks_path + "luarocks/test.lua", - "luarocks.cmd.test": rocks_path + "luarocks/cmd/test.lua", - "luarocks.cmd.which": rocks_path + "luarocks/cmd/which.lua", - "luarocks.cmd.remove": rocks_path + "luarocks/cmd/remove.lua", - "luarocks.upload.multipart": rocks_path + "luarocks/upload/multipart.lua", - "luarocks.upload.api": rocks_path + "luarocks/upload/api.lua", - "luarocks.cmd.upload": rocks_path + "luarocks/cmd/upload.lua", - "luarocks.cmd.doc": rocks_path + "luarocks/cmd/doc.lua", - "luarocks.cmd.unpack": rocks_path + "luarocks/cmd/unpack.lua", - "luarocks.cmd.config": rocks_path + "luarocks/cmd/config.lua", - "luarocks.require": rocks_path + "luarocks/require.lua", - "luarocks.build.cmake": rocks_path + "luarocks/build/cmake.lua", - "luarocks.build.make": rocks_path + "luarocks/build/make.lua", - "luarocks.build.command": rocks_path + "luarocks/build/command.lua", - "luarocks.fetch.cvs": rocks_path + "luarocks/fetch/cvs.lua", - "luarocks.fetch.svn": rocks_path + "luarocks/fetch/svn.lua", - "luarocks.fetch.sscm": rocks_path + "luarocks/fetch/sscm.lua", - "luarocks.fetch.git": rocks_path + "luarocks/fetch/git.lua", - "luarocks.fetch.git_file": rocks_path + "luarocks/fetch/git_file.lua", - "luarocks.fetch.git_http": rocks_path + "luarocks/fetch/git_http.lua", - "luarocks.fetch.git_https": rocks_path + "luarocks/fetch/git_https.lua", - "luarocks.fetch.git_ssh": rocks_path + "luarocks/fetch/git_ssh.lua", - "luarocks.fetch.hg": rocks_path + "luarocks/fetch/hg.lua", - "luarocks.fetch.hg_http": rocks_path + "luarocks/fetch/hg_http.lua", - "luarocks.fetch.hg_https": rocks_path + "luarocks/fetch/hg_https.lua", - "luarocks.fetch.hg_ssh": rocks_path + "luarocks/fetch/hg_ssh.lua", - "luarocks.admin.cache": rocks_path + "luarocks/admin/cache.lua", - "luarocks.admin.cmd.refresh_cache": rocks_path + "luarocks/admin/cmd/refresh_cache.lua", - "luarocks.admin.index": rocks_path + "luarocks/admin/index.lua", - "luarocks.admin.cmd.add": rocks_path + "luarocks/admin/cmd/add.lua", - "luarocks.admin.cmd.remove": rocks_path + "luarocks/admin/cmd/remove.lua", - "luarocks.admin.cmd.make_manifest": rocks_path + "luarocks/admin/cmd/make_manifest.lua", - } - - L := lua.NewState() - defer L.Close() - L.SetGlobal("tt_getwd", L.NewFunction(getwdWrapperForLua)) - preload := L.GetField(L.GetField(L.Get(lua.EnvironIndex), "package"), "preload") - - for modName, path := range rocks_preload { - ctx, err := fs.ReadFile(luarocks, path) - if err != nil { - return err - } - mod, err := L.LoadString(string(ctx)) - if err != nil { - return err - } - L.SetField(preload, modName, mod) - } - - if err := L.DoString(rocks_cmd); err != nil { + rocksClient, err := client.New(cfg, client.WithBackend(client.BackendLua)) + if err != nil { return err } - return nil + // Match the historical progname: LuaRocks prints " rocks" (and + // " rocks admin") in its usage, the wrapper appending " admin" for + // the admin sub-CLI. + return rocksClient.Exec(context.Background(), os.Args[0]+" rocks", args) } diff --git a/cli/rocks/third_party/luarocks b/cli/rocks/third_party/luarocks deleted file mode 160000 index f13f4ad36..000000000 --- a/cli/rocks/third_party/luarocks +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f13f4ad36cc565f5f918c68c11c035bb2f0b6e6e diff --git a/go.mod b/go.mod index 96b069ccd..6aebd4d90 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/avast/retry-go v3.0.0+incompatible github.com/briandowns/spinner v1.23.2 github.com/fatih/color v1.18.0 + github.com/go-git/go-git/v5 v5.19.1 github.com/google/uuid v1.6.0 github.com/hashicorp/go-version v1.8.0 github.com/jedib0t/go-pretty/v6 v6.7.8 @@ -37,13 +38,13 @@ require ( github.com/tarantool/tt/lib/dial v0.0.0-00010101000000-000000000000 github.com/tarantool/tt/lib/integrity v0.0.0 github.com/vmihailenco/msgpack/v5 v5.4.1 - github.com/yuin/gopher-lua v1.1.1 + github.com/yuin/gopher-lua v1.1.2 go.etcd.io/etcd/client/pkg/v3 v3.6.11 go.etcd.io/etcd/client/v3 v3.6.11 - golang.org/x/crypto v0.49.0 - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa - golang.org/x/sys v0.42.0 - golang.org/x/term v0.41.0 + golang.org/x/crypto v0.50.0 + golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f + golang.org/x/sys v0.43.0 + golang.org/x/term v0.42.0 google.golang.org/grpc v1.80.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v2 v2.4.0 @@ -51,32 +52,40 @@ require ( ) require ( + dario.cat/mergo v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect github.com/alecthomas/repr v0.5.2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chzyer/readline v1.5.1 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudflare/circl v1.6.3 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/emirpasic/gods v1.18.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.9.0 // indirect github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/goccy/go-yaml v1.19.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/go-cmp v0.7.0 // indirect @@ -85,12 +94,15 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect github.com/kaptinlin/go-i18n v0.2.2 // indirect github.com/kaptinlin/jsonpointer v0.4.8 // indirect github.com/kaptinlin/jsonschema v0.6.7 // indirect github.com/kaptinlin/messageformat-go v0.4.7 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-pointer v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.20 // indirect @@ -104,14 +116,17 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/otiai10/mint v1.6.3 // indirect + github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/term v1.2.0-beta.2 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/procfs v0.19.2 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect github.com/soheilhy/cmux v0.1.5 // indirect github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect github.com/spf13/pflag v1.0.10 // indirect @@ -120,6 +135,7 @@ require ( github.com/tarantool/go-option v1.1.0 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect go.etcd.io/bbolt v1.4.3 // indirect go.etcd.io/etcd/api/v3 v3.6.11 // indirect @@ -140,7 +156,7 @@ require ( go.uber.org/zap v1.27.1 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.52.0 // indirect + golang.org/x/net v0.53.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.14.0 // indirect @@ -148,6 +164,7 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 50315429a..366e50188 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,14 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/adam-hanna/arrayOperations v0.2.6 h1:QZC99xC8MgUawXnav7bFMejs/dm7YySnDpMx3oZzz2Y= github.com/adam-hanna/arrayOperations v0.2.6/go.mod h1:iIzkSjP91FnE66cUFNAjjUJVmjyAwCH0SXnWsx2nbdk= github.com/alecthomas/participle/v2 v2.0.0-alpha4 h1:/mx1xDva4aUDplaYOydXT7dKf1MPmkwy/igdhPCCDyw= @@ -11,11 +16,15 @@ github.com/alecthomas/participle/v2 v2.0.0-alpha4/go.mod h1:Z1zPLDbcGsVsBYsThKXY github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs= github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0= github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA= github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE= github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= @@ -39,6 +48,8 @@ github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= +github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= @@ -54,6 +65,8 @@ github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -66,6 +79,10 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= @@ -75,6 +92,16 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA= +github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00= +github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -91,6 +118,8 @@ github.com/gojuno/minimock/v3 v3.4.7 h1:vhE5zpniyPDRT0DXd5s3DbtZJVlcbmC5k80izYtj github.com/gojuno/minimock/v3 v3.4.7/go.mod h1:QxJk4mdPrVyYUmEZGc2yD2NONpqM/j4dWhsy9twjFHg= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= @@ -116,6 +145,8 @@ github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jedib0t/go-pretty/v6 v6.7.8 h1:BVYrDy5DPBA3Qn9ICT+PokP9cvCv1KaHv2i+Hc8sr5o= github.com/jedib0t/go-pretty/v6 v6.7.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -130,11 +161,16 @@ github.com/kaptinlin/jsonschema v0.6.7 h1:ytzDGyvSMhcijnyRsr+CyRbwocp4qdeAWREESP github.com/kaptinlin/jsonschema v0.6.7/go.mod h1:EbhSbdxZ4QjzIORdMWOrRXJeCHrLTJqXDA8JzNaeFc8= github.com/kaptinlin/messageformat-go v0.4.7 h1:HQ/OvFUSU7+fAHWkZnP2ug9y+A/ZyTE8j33jfWr8O3Q= github.com/kaptinlin/messageformat-go v0.4.7/go.mod h1:DusKpv8CIybczGvwIVn3j13hbR3psr5mOwhFudkiq1c= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -198,6 +234,8 @@ github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= +github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -208,6 +246,8 @@ github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs= github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU= +github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -229,11 +269,16 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= @@ -290,12 +335,14 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= -github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA= +github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/etcd/api/v3 v3.6.11 h1:XFGTgrJ8nak3kB4NgMG8t7NT+lEeuuvKQAqUHKVgkWQ= @@ -346,10 +393,11 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= +golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -358,9 +406,10 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -381,18 +430,21 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -428,6 +480,8 @@ gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/golangci-lint.yml b/golangci-lint.yml index c513cbda2..1c68d83b4 100644 --- a/golangci-lint.yml +++ b/golangci-lint.yml @@ -3,6 +3,10 @@ run: build-tags: - go_tarantool_ssl_disable - tt_ssl_disable + # Vendored go-luarocks sources: part of the tt module but not held to tt's + # lint rules for now. + skip-dirs: + - lib/luarocks linters: disable-all: true diff --git a/golint-precommit-ci.yml b/golint-precommit-ci.yml index e44d91a0d..ddb5cf713 100644 --- a/golint-precommit-ci.yml +++ b/golint-precommit-ci.yml @@ -60,6 +60,9 @@ linters: - third_party$ - builtin$ - examples$ + # Vendored go-luarocks sources: part of the tt module but not held to + # tt's lint rules for now. + - lib/luarocks formatters: exclusions: diff --git a/golint-precommit.yml b/golint-precommit.yml index 2548ec198..506ff1cd3 100644 --- a/golint-precommit.yml +++ b/golint-precommit.yml @@ -94,6 +94,9 @@ linters: - builtin$ - examples$ - magefile.go + # Vendored go-luarocks sources: part of the tt module but not held to + # tt's lint rules for now. + - lib/luarocks formatters: exclusions: diff --git a/lib/luarocks/build/builtin.go b/lib/luarocks/build/builtin.go new file mode 100644 index 000000000..afe5cde31 --- /dev/null +++ b/lib/luarocks/build/builtin.go @@ -0,0 +1,315 @@ +package build + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +const ( + // dirPerm is the mode used when creating destination directories. + dirPerm os.FileMode = 0o750 + // exePerm is the mode applied to installed executable artifacts. + exePerm os.FileMode = 0o750 + // dirOwnerRWX is OR-ed into a copied directory's mode so the owner can + // always traverse/write the recreated tree. + dirOwnerRWX os.FileMode = 0o700 + // initialArgsCap pre-sizes the cc argument slice to avoid early regrows. + initialArgsCap = 16 +) + +// runBuiltin implements the `builtin` build backend. +// +// It iterates spec.Build.Modules and dispatches per module shape: +// +// - Path != "" and ends in ".lua" → copy file (no compile) +// - Path != "" and ends in ".c" → single-source C compile +// - Sources non-empty (table form) → multi-source C compile with the +// per-module incdirs/libdirs/libraries/defines applied +// +// After modules, spec.Build.Install.{Lua,Lib,Bin,Conf} entries are copied +// into the matching subdirectory under destDir (lua/lib/bin/conf). The +// downstream tree.Deploy step is responsible for moving these to their +// final deploy paths; this backend only writes them under destDir. +// +// spec.Build.CopyDirectories entries are copied recursively from srcDir +// to destDir/ verbatim. +// +// If ANY module needs a C compile and cfg.Tarantool.IncludeDir is empty, +// the function returns ErrMissingTarantoolHeaders before invoking cc. +// +// destDir is the rock's staging tree root (the "build" subdir the facade +// hands us). Files live at: +// +// destDir/lua///.lua for ["a.b.c"] = ".../c.lua" +// destDir/lib///.so for compiled C modules +// destDir/lua|lib|bin|conf/ for build.install.* +// destDir//... for build.copy_directories +func runBuiltin(ctx context.Context, spec *rocks.Rockspec, srcDir, destDir string, cfg rocks.Config) error { + flags := DeriveFlags(cfg) + + // Stable iteration so error reports are deterministic. + names := make([]string, 0, len(spec.Build.Modules)) + for n := range spec.Build.Modules { + names = append(names, n) + } + + sort.Strings(names) + + // Pre-flight: any C compile needs headers. + for _, n := range names { + m := spec.Build.Modules[n] + if needsCC(m) && cfg.Tarantool.IncludeDir == "" { + return fmt.Errorf("build: module %q: %w", n, rocks.ErrMissingTarantoolHeaders) + } + } + + for _, n := range names { + m := spec.Build.Modules[n] + + switch { + case m.Path != "" && strings.HasSuffix(m.Path, ".lua"): + err := installLuaModule(srcDir, destDir, n, m.Path) + if err != nil { + return fmt.Errorf("build: module %q: %w", n, err) + } + case m.Path != "" && strings.HasSuffix(m.Path, ".c"): + err := compileModule(ctx, srcDir, destDir, n, + rocks.Module{Sources: []string{m.Path}}, flags, cfg) + if err != nil { + return fmt.Errorf("build: module %q: %w", n, err) + } + case len(m.Sources) > 0: + err := compileModule(ctx, srcDir, destDir, n, m, flags, cfg) + if err != nil { + return fmt.Errorf("build: module %q: %w", n, err) + } + default: + return fmt.Errorf("build: module %q: empty entry (no path, no sources)", n) + } + } + + err := installBuildInstall(srcDir, destDir, spec.Build.Install) + if err != nil { + return fmt.Errorf("build: install: %w", err) + } + + for _, d := range spec.Build.CopyDirectories { + err := copyDir(filepath.Join(srcDir, d), filepath.Join(destDir, d)) + if err != nil { + return fmt.Errorf("build: copy_directories %q: %w", d, err) + } + } + + return nil +} + +// needsCC reports whether the module entry compiles a C source. +func needsCC(m rocks.Module) bool { + if m.Path != "" && strings.HasSuffix(m.Path, ".c") { + return true + } + + return len(m.Sources) > 0 +} + +// moduleSlashPath converts a dotted module name to the slashed +// destination subpath. "foo.bar.baz" → "foo/bar/baz". +func moduleSlashPath(name string) string { + return strings.ReplaceAll(name, ".", "/") +} + +// installLuaModule copies a single .lua module file from srcDir/path into +// destDir/lua/.lua. +func installLuaModule(srcDir, destDir, name, path string) error { + dst := filepath.Join(destDir, "lua", moduleSlashPath(name)+".lua") + + return copyFile(filepath.Join(srcDir, path), dst) +} + +// compileModule builds a single .so via one cc invocation: +// +// $CC $CFLAGS [-Iincdir...] [-Ddefine...] $LIBFLAG \ +// -o destDir/lib/.so \ +// [-Llibdir...] [-llibname...] $LDFLAGS +// +// All sources are passed in a single cc call (do not reimplement +// build infrastructure; rely on cc to handle multiple .c inputs). +func compileModule(ctx context.Context, srcDir, destDir, name string, m rocks.Module, flags Flags, cfg rocks.Config) error { + out := filepath.Join(destDir, "lib", moduleSlashPath(name)+flags.Ext) + + err := os.MkdirAll(filepath.Dir(out), dirPerm) + if err != nil { + return err + } + + args := make([]string, 0, initialArgsCap) + + args = append(args, flags.CFLAGS...) + + for _, d := range m.Defines { + args = append(args, "-D"+d) + } + + for _, inc := range m.Incdirs { + args = append(args, "-I"+inc) + } + + args = append(args, flags.LIBFLAG...) + + args = append(args, "-o", out) + + for _, s := range m.Sources { + args = append(args, filepath.Join(srcDir, s)) + } + + for _, l := range m.Libdirs { + args = append(args, "-L"+l) + } + + for _, lib := range m.Libraries { + args = append(args, "-l"+lib) + } + + args = append(args, flags.LDFLAGS...) + + return runCmd(ctx, flags.CC, args, srcDir, buildEnv(cfg)) +} + +// installBuildInstall processes the four sub-maps of build.install. Each +// map is destination-name → source-path-relative-to-srcDir. The +// destination layout under destDir is: +// +// install.lua → destDir/lua/.lua +// install.lib → destDir/lib/. (mode 0o750) +// install.bin → destDir/bin/ (mode 0o750) +// install.conf → destDir/conf/ +// +// tree.Deploy may later re-copy these into the deploy tree; that +// duplication is acceptable for now and the install dirs win since deploy +// runs after build. +func installBuildInstall(srcDir, destDir string, bi rocks.BuildInstall) error { + type sub struct { + name string + entries map[string]string + subdir string + isModule bool + isExe bool + extension string // forced extension, e.g. ".so"; empty = key as-given + } + + subs := []sub{ + {name: "install.lua", entries: bi.Lua, subdir: "lua", isModule: true, extension: ".lua"}, + {name: "install.lib", entries: bi.Lib, subdir: "lib", isModule: true, isExe: true, extension: ".so"}, + {name: "install.bin", entries: bi.Bin, subdir: "bin", isExe: true}, + {name: "install.conf", entries: bi.Conf, subdir: "conf"}, + } + for _, s := range subs { + keys := make([]string, 0, len(s.entries)) + for k := range s.entries { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, k := range keys { + src := filepath.Join(srcDir, s.entries[k]) + + var rel string + if s.isModule { + rel = moduleSlashPath(k) + s.extension + } else { + rel = k + } + + dst := filepath.Join(destDir, s.subdir, rel) + + err := copyFile(src, dst) + if err != nil { + return fmt.Errorf("%s[%q]: %w", s.name, k, err) + } + + if s.isExe { + err := os.Chmod(dst, exePerm) //nolint:gosec // installed binaries/libraries must be executable + if err != nil { + return fmt.Errorf("%s[%q]: chmod: %w", s.name, k, err) + } + } + } + } + + return nil +} + +// copyFile copies a file preserving the source file's regular-file mode +// bits (lower 9 bits). Parent directories of dst are created with dirPerm +// (0o750). +func copyFile(src, dst string) error { + if err := os.MkdirAll(filepath.Dir(dst), dirPerm); err != nil { + return err + } + + in, err := os.Open(src) //nolint:gosec // src is a rockspec-derived build path, internally controlled + if err != nil { + return err + } + + defer func() { _ = in.Close() }() + + st, err := in.Stat() + if err != nil { + return err + } + + mode := st.Mode().Perm() + if mode == 0 { + mode = 0o644 + } + + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, mode) //nolint:gosec // dst is a rockspec-derived build path, internally controlled + if err != nil { + return err + } + + if _, err := io.Copy(out, in); err != nil { + _ = out.Close() + + return err + } + + return out.Close() +} + +// copyDir recursively copies src into dst. Symlinks are not specially +// handled — they are dereferenced (matches upstream luarocks behavior for +// copy_directories on unix). +func copyDir(src, dst string) error { + return filepath.Walk(src, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + rel, relErr := filepath.Rel(src, p) + if relErr != nil { + return relErr + } + + target := filepath.Join(dst, rel) + if info.IsDir() { + return os.MkdirAll(target, info.Mode().Perm()|dirOwnerRWX) + } + + if info.Mode()&os.ModeSymlink != 0 { + // Resolve to real file and copy its contents. + return copyFile(p, target) + } + + return copyFile(p, target) + }) +} diff --git a/lib/luarocks/build/cmake.go b/lib/luarocks/build/cmake.go new file mode 100644 index 000000000..f95e6b66e --- /dev/null +++ b/lib/luarocks/build/cmake.go @@ -0,0 +1,59 @@ +package build + +import ( + "context" + "sort" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// runCMake implements the `cmake` build backend. +// +// Sequence (pure pass-through — luarocks itself does NOT auto-inject +// LUA_INCLUDE_DIR, CMAKE_INSTALL_PREFIX, CMAKE_MODULE_LINKER_FLAGS, etc.; +// the rockspec is responsible for supplying them via build.variables): +// +// 1. cmake . -D= ... (configure) +// 2. cmake --build . (build) +// 3. cmake --install . --prefix destDir +// +// The working directory is srcDir. Builds happen in-tree, matching +// upstream's `-H. -Bbuild.luarocks` shape closely enough for our purposes +// (upstream uses `build.luarocks` as a subdir; we let cmake default to +// in-tree which gives the same outputs on the install step). +// +// The install --prefix argument is the one piece of plumbing we DO inject: +// the facade needs the rock's output rooted at destDir, and cmake's +// --install --prefix is the cleanest way to achieve that without +// post-hoc moving. This matches the spirit of upstream which expects +// CMAKE_INSTALL_PREFIX to be set to $(PREFIX); --prefix on --install +// overrides it. +func runCMake(ctx context.Context, spec *rocks.Rockspec, srcDir, destDir string, cfg rocks.Config) error { + env := buildEnv(cfg) + + keys := make([]string, 0, len(spec.Build.Variables)) + for k := range spec.Build.Variables { + keys = append(keys, k) + } + + sort.Strings(keys) + + configure := make([]string, 0, 1+len(keys)) + configure = append(configure, ".") + + for _, k := range keys { + configure = append(configure, "-D"+k+"="+spec.Build.Variables[k]) + } + + err := runCmd(ctx, "cmake", configure, srcDir, env) + if err != nil { + return err + } + + err = runCmd(ctx, "cmake", []string{"--build", "."}, srcDir, env) + if err != nil { + return err + } + + return runCmd(ctx, "cmake", []string{"--install", ".", "--prefix", destDir}, srcDir, env) +} diff --git a/lib/luarocks/build/command.go b/lib/luarocks/build/command.go new file mode 100644 index 000000000..21b2cbc9b --- /dev/null +++ b/lib/luarocks/build/command.go @@ -0,0 +1,39 @@ +package build + +import ( + "context" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// runCommand implements the `command` build backend. +// +// Each of spec.Build.BuildCommand and spec.Build.InstallCommand, when +// non-empty, is executed verbatim via `sh -c ` from srcDir with the +// buildEnv(cfg) overlay applied to the inherited process environment. +// +// Either may be empty — the corresponding phase is then a no-op. +// +// This is intentionally minimal: upstream luarocks injects only CC into +// the env (cfg.variables.CC), but for Tarantool builds we expose the full +// canonical set so rockspec commands can `${LUA_INCDIR}` etc. just like +// other backends. +func runCommand(ctx context.Context, spec *rocks.Rockspec, srcDir, _ string, cfg rocks.Config) error { + env := buildEnv(cfg) + + if spec.Build.BuildCommand != "" { + err := runCmd(ctx, "sh", []string{"-c", spec.Build.BuildCommand}, srcDir, env) + if err != nil { + return err + } + } + + if spec.Build.InstallCommand != "" { + err := runCmd(ctx, "sh", []string{"-c", spec.Build.InstallCommand}, srcDir, env) + if err != nil { + return err + } + } + + return nil +} diff --git a/lib/luarocks/build/dispatch.go b/lib/luarocks/build/dispatch.go new file mode 100644 index 000000000..92243e667 --- /dev/null +++ b/lib/luarocks/build/dispatch.go @@ -0,0 +1,43 @@ +package build + +import ( + "context" + "fmt" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// RunBackend dispatches to the build backend selected by spec.Build.Type. +// +// Supported types: +// +// - "" → builtin (upstream default for format ≥ 3.0) +// - "builtin" → builtin +// - "cmake" → cmake +// - "make" → make +// - "command" → command +// - "none" → no-op (returns nil immediately) +// +// Any other value yields ErrUnsupportedRockspecFeature wrapped with the +// observed type string (typed errors callers can branch on). +// +// srcDir is the unpacked source tree the rock will be built against. +// destDir is the staging root the backend writes outputs into. The facade +// is responsible for orchestrating srcDir (post-fetch) and destDir +// (the `/.../build/` working area handed to tree.Deploy). +func RunBackend(ctx context.Context, spec *rocks.Rockspec, srcDir, destDir string, cfg rocks.Config) error { + switch spec.Build.Type { + case "", "builtin": + return runBuiltin(ctx, spec, srcDir, destDir, cfg) + case "cmake": + return runCMake(ctx, spec, srcDir, destDir, cfg) + case "make": + return runMake(ctx, spec, srcDir, destDir, cfg) + case "command": + return runCommand(ctx, spec, srcDir, destDir, cfg) + case "none": + return nil + default: + return fmt.Errorf("build: type %q: %w", spec.Build.Type, rocks.ErrUnsupportedRockspecFeature) + } +} diff --git a/lib/luarocks/build/flags.go b/lib/luarocks/build/flags.go new file mode 100644 index 000000000..67bd2accd --- /dev/null +++ b/lib/luarocks/build/flags.go @@ -0,0 +1,119 @@ +// Package build implements the four supported rockspec build backends — +// builtin, cmake, make, command — plus the none no-op. RunBackend is the +// single public entry point used by the facade. +// +// The package never sets process environment variables. Every +// subprocess invocation builds its env via cmd.Env, layering on top of +// os.Environ() with the five canonical TARANTOOL_DIR / LUA_* vars and any +// rockspec-supplied K=V pairs. +// +// All subprocesses receive ctx via exec.CommandContext. Output +// shared-library extension is `.so` on both linux and macOS (upstream +// luarocks sets `lib_extension = "so"` for unix unconditionally, +// even on macOS where the linker emits a -bundle). +package build + +import ( + "os" + "path/filepath" + "runtime" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// Flags is the resolved compile/link toolchain configuration for a single +// build invocation. The fields are derived from rocks.Config plus the host +// GOOS; the build backends consume them when constructing cc / ld command +// lines. +// +// CFLAGS always contains "-fPIC" on unix and "-I" iff +// cfg.Tarantool.IncludeDir is set. CFLAGS does NOT inject "-llua" or +// "-lluajit"; for Tarantool the Lua symbols are resolved at dlopen against +// the running tarantool executable. +// +// LIBFLAG carries the shared-library link flag list: +// +// - linux: ["-shared"] +// - macOS: ["-bundle", "-undefined", "dynamic_lookup", "-all_load"] +// +// Ext is always ".so" on unix (per upstream cfg.lib_extension = "so"). +type Flags struct { + // CC is the C compiler to invoke (the CC env var, else the platform default). + CC string + // CFLAGS are the compile flags (see the type doc for how they are built). + CFLAGS []string + // LDFLAGS is the extra-link-flags slot consumed on the cc line. DeriveFlags + // does not populate it (it is currently always empty); it exists so callers + // and future config can supply additional link flags without a signature + // change. + LDFLAGS []string + // LIBFLAG is the per-OS shared-library link flag list (see the type doc). + LIBFLAG []string + // LuaIncDir is the Lua/Tarantool include dir (empty when none is configured). + LuaIncDir string + // LuaLibDir is the Lua/Tarantool library dir, when configured. + LuaLibDir string + // LuaBinDir is the Lua/Tarantool bin dir, when configured. + LuaBinDir string + // Ext is the compiled-module extension — always ".so" on unix. + Ext string +} + +// DeriveFlags resolves toolchain Flags for the running host. The result is +// pure data — no env writes, no subprocesses — so it is safe to call from +// tests on any platform. +// +// CC is taken from the CC environment variable if set, otherwise the +// platform default ("cc"). DeriveFlags itself reads CC via os.Getenv but +// never writes process env. +// +// If cfg.Tarantool.IncludeDir is empty the returned CFLAGS omits the +// "-I" entry and LuaIncDir is empty. Backends that require headers detect +// this case themselves and return ErrMissingTarantoolHeaders. +func DeriveFlags(cfg rocks.Config) Flags { + return deriveFlagsFor(cfg, runtime.GOOS) +} + +// deriveFlagsFor is the GOOS-injected helper that backs DeriveFlags. Tests +// invoke it directly to cover both "linux" and "darwin" without depending +// on the host runtime.GOOS. +func deriveFlagsFor(cfg rocks.Config, goos string) Flags { + cc := os.Getenv("CC") + if cc == "" { + cc = "cc" + } + + f := Flags{ + CC: cc, + LuaIncDir: cfg.Tarantool.IncludeDir, + Ext: ".so", + } + + // CFLAGS: always -fPIC on unix; -I only if we have one. + f.CFLAGS = append(f.CFLAGS, "-fPIC") + if cfg.Tarantool.IncludeDir != "" { + f.CFLAGS = append(f.CFLAGS, "-I"+cfg.Tarantool.IncludeDir) + } + + switch goos { + case "darwin": + // -bundle (NOT -dynamiclib) so the loader resolves symbols against + // the host executable at dlopen time. No -Wl,-rpath on macOS — + // upstream luarocks defaults gcc_rpath=false there. + f.LIBFLAG = []string{"-bundle", "-undefined", "dynamic_lookup", "-all_load"} + default: + // Treat anything non-darwin as linux/unix for our purposes; this + // package is unix-only and the dispatcher does not gate on + // GOOS otherwise. + f.LIBFLAG = []string{"-shared"} + // gcc_rpath: hardcoded false for now (no Config flag exposed). + // When we add the toggle, append "-Wl,-rpath=" + LuaLibDir here. + } + + if cfg.Tarantool.Prefix != "" { + f.LuaLibDir = filepath.Join(cfg.Tarantool.Prefix, "lib") + f.LuaBinDir = filepath.Join(cfg.Tarantool.Prefix, "bin") + } + + return f +} diff --git a/lib/luarocks/build/make.go b/lib/luarocks/build/make.go new file mode 100644 index 000000000..21e6ee44c --- /dev/null +++ b/lib/luarocks/build/make.go @@ -0,0 +1,74 @@ +package build + +import ( + "context" + "maps" + "sort" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// runMake implements the `make` build backend. +// +// Sequence (mirrors upstream luarocks/src/luarocks/build/make.lua): +// +// 1. make [] K=V ... (build phase) +// - target defaults to "" (make's default target) if BuildTarget is unset +// - K=V assignments come from spec.Build.BuildVariables, passed via env +// 2. make (install phase) +// - target defaults to "install" if InstallTarget is unset +// - K=V assignments come from spec.Build.InstallVariables, passed via env +// +// Working directory is srcDir for both phases. The base env is buildEnv(cfg) +// — the five canonical TARANTOOL_DIR/LUA_* vars — overlaid with the +// per-phase build/install variables. The phase variables WIN over buildEnv +// when both define the same key. +// +// We pass variables as env, not as `K=V` argv (upstream make.lua does +// both interchangeably; env is closer to how Makefiles consume them and +// it keeps the argv shape simple and shell-safe). +func runMake(ctx context.Context, spec *rocks.Rockspec, srcDir, _ string, cfg rocks.Config) error { + base := buildEnv(cfg) + + // Build phase. + buildArgs := []string{} + if spec.Build.BuildTarget != "" { + buildArgs = append(buildArgs, spec.Build.BuildTarget) + } + + err := runCmd(ctx, "make", buildArgs, srcDir, overlay(base, spec.Build.BuildVariables)) + if err != nil { + return err + } + + // Install phase. + installTarget := spec.Build.InstallTarget + if installTarget == "" { + installTarget = "install" + } + + installArgs := []string{installTarget} + + return runCmd(ctx, "make", installArgs, srcDir, overlay(base, spec.Build.InstallVariables)) +} + +// overlay returns a copy of base with vars layered on top. base is left +// unchanged. Order is deterministic for tests (vars iterated in sorted +// key order, though map iteration order doesn't affect the final map). +func overlay(base, vars map[string]string) map[string]string { + out := make(map[string]string, len(base)+len(vars)) + maps.Copy(out, base) + + keys := make([]string, 0, len(vars)) + for k := range vars { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, k := range keys { + out[k] = vars[k] + } + + return out +} diff --git a/lib/luarocks/build/subprocess.go b/lib/luarocks/build/subprocess.go new file mode 100644 index 000000000..152dfb54c --- /dev/null +++ b/lib/luarocks/build/subprocess.go @@ -0,0 +1,148 @@ +package build + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "sort" + "strings" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// stderrTailLimit caps how many trailing bytes of a failed command's stderr +// are embedded in the returned error. +const stderrTailLimit = 4096 + +// runCmd shells out to `name args...` with the given working directory and +// extra environment overlay. extraEnv overrides any matching key in +// os.Environ(). Stdout/stderr are captured; on non-zero exit the returned +// error embeds the program name, args, and a tail of stderr. +// +// This is the ONLY entry point in the build/ package that +// constructs an exec.Cmd, and the entire env path goes through extraEnv — +// we never os.Setenv. The context is passed via +// exec.CommandContext so cancellation propagates to the child process +// group. +func runCmd(ctx context.Context, name string, args []string, cwd string, extraEnv map[string]string) error { + cmd := exec.CommandContext(ctx, name, args...) //nolint:gosec // name/args are rockspec-derived build tooling, internally controlled + if cwd != "" { + cmd.Dir = cwd + } + + cmd.Env = mergeEnv(os.Environ(), extraEnv) + + var stdout, stderr bytes.Buffer + + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + tail := strings.TrimSpace(stderr.String()) + if len(tail) > stderrTailLimit { + tail = tail[len(tail)-stderrTailLimit:] + } + + return fmt.Errorf("build: %s %s: %w (stderr: %s)", + name, strings.Join(args, " "), err, tail) + } + + return nil +} + +// mergeEnv produces a final environment slice by starting from base +// (typically os.Environ()) and replacing or appending each key in +// overlay. Determinism of order is not guaranteed by exec — but for tests +// we sort overlay keys so assertions over cmd.Env are stable. +func mergeEnv(base []string, overlay map[string]string) []string { + if len(overlay) == 0 { + out := make([]string, len(base)) + copy(out, base) + + return out + } + // Build an index of base entries by key for in-place override. + idx := make(map[string]int, len(base)) + + out := make([]string, 0, len(base)+len(overlay)) + + for i, e := range base { + k, _, ok := splitEnv(e) + if !ok { + out = append(out, e) + + continue + } + + idx[k] = i + + out = append(out, e) + } + + keys := make([]string, 0, len(overlay)) + for k := range overlay { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, k := range keys { + entry := k + "=" + overlay[k] + if i, ok := idx[k]; ok { + out[i] = entry + + continue + } + + out = append(out, entry) + } + + return out +} + +// splitEnv splits a "KEY=VAL" string. Returns ok=false for entries without +// an "=" (defensive — should not occur in os.Environ output). +func splitEnv(e string) (string, string, bool) { + before, after, ok := strings.Cut(e, "=") + if !ok { + return "", "", false + } + + return before, after, true +} + +// buildEnv produces the canonical {TARANTOOL_DIR, LUA, LUA_INCDIR, +// LUA_LIBDIR, LUA_BINDIR} environment overlay for a subprocess invocation. +// All five vars come from rocks.Config — never from the +// host process env. +// +// Entries with empty values are omitted so the child shell sees them as +// unset rather than as the empty string. +func buildEnv(cfg rocks.Config) map[string]string { + env := map[string]string{} + if cfg.Tarantool.Prefix != "" { + env["TARANTOOL_DIR"] = cfg.Tarantool.Prefix + } + + if cfg.Tarantool.Executable != "" { + env["LUA"] = cfg.Tarantool.Executable + } + + if cfg.Tarantool.IncludeDir != "" { + env["LUA_INCDIR"] = cfg.Tarantool.IncludeDir + } + + f := DeriveFlags(cfg) + if f.LuaLibDir != "" { + env["LUA_LIBDIR"] = f.LuaLibDir + } + + if f.LuaBinDir != "" { + env["LUA_BINDIR"] = f.LuaBinDir + } + + return env +} diff --git a/lib/luarocks/client/client.go b/lib/luarocks/client/client.go new file mode 100644 index 000000000..b59f1033b --- /dev/null +++ b/lib/luarocks/client/client.go @@ -0,0 +1,661 @@ +// Package client implements the Rocks facade — the keystone public API that +// composes the manif, rockspec, fetch, build, tree, deps and remote subsystems +// into LuaRocks operations. The native backend implements eight of them — +// Install, Build, Make, List, Show, Which, Pack, Unpack — and returns +// rocks.ErrNotImplemented for the rest; the lua backend covers the full +// upstream command set. The complete method set is the Engine interface +// (engine.go). +// +// Why this lives in a sub-package rather than at the module root: +// +// - deps/ imports rocks (root) for shared data types (Rockspec, Version, +// VersionConstraint, …). +// - The facade needs to invoke deps.Resolve. +// - A direct rocks → deps import would create a cycle. +// +// The root rocks package retains the data types and interfaces; the +// operational Rocks struct + methods live here in the client package. +// Callers spell it `client.New(cfg)`. +// +// Subsystem references: +// +// - rockspec.Eval / MergePlatforms / RuntimePlatforms / Validate +// - fetch.Fetch / FetchWith +// - build.RunBackend +// - tree.Open / tree.Tree.Deploy / tree.Tree.Which +// - manif.FileStore (default ManifestStore) +// - deps.Resolve +// - remote.HTTPRemoteIndex (default RemoteIndex) +package client + +import ( + "archive/zip" + "context" + "errors" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "slices" + "strings" + + rocks "github.com/tarantool/tt/lib/luarocks" + "github.com/tarantool/tt/lib/luarocks/deps" + "github.com/tarantool/tt/lib/luarocks/manif" + "github.com/tarantool/tt/lib/luarocks/remote" + "github.com/tarantool/tt/lib/luarocks/rockspec" + "github.com/tarantool/tt/lib/luarocks/tree" +) + +// Rocks is the public facade. Construct via New(Config). All operations +// take a context and read configuration from r.cfg — no hidden global +// state, no os.Getwd / os.Chdir, no os.Setenv. +// +// Write operations delegate to r.engine, which is selected once at New() +// time per r.backend and is final for the lifetime of the facade. +// Read operations (List, Show, Which, ReadTreeManifest) are served directly +// from r.store regardless of backend. +type Rocks struct { + cfg rocks.Config + store rocks.ManifestStore + index rocks.RemoteIndex + logger *slog.Logger + backend Backend + engine Engine +} + +// InstallOpts tunes Install. +type InstallOpts struct { + // Version, if set, narrows the candidate set to those matching this + // constraint (parsed via deps.ParseConstraints). Empty means "any + // version satisfying the rockspec's transitive constraints" — i.e. + // the resolver picks the newest. + Version string + + // Servers overrides r.cfg.Servers for this Install. Empty means use + // the facade's configured servers. + Servers []string + + // Deps controls whether transitive dependencies are also installed. + Deps DepsPolicy +} + +// DepsPolicy mirrors upstream's `--deps-mode` flag. +type DepsPolicy int + +const ( + // DepsAll resolves and installs every transitive dependency. + DepsAll DepsPolicy = 0 + // DepsNone installs only the named rock; missing deps cause Install + // to fail. + DepsNone DepsPolicy = 1 + // DepsOnlyNew installs deps that aren't already in the tree. + DepsOnlyNew DepsPolicy = 2 +) + +// BuildOpts tunes Build. +type BuildOpts struct { + // Keep, if true, leaves the staging build directory in place after a + // successful build for debugging. Default removes it. + Keep bool +} + +// MakeOpts tunes Make. +type MakeOpts struct { + // RockspecPath, if non-empty, names the rockspec to build. Empty + // means search r.cfg.WorkingDir for exactly one `*.rockspec`. + RockspecPath string +} + +// PackOpts tunes Pack. +type PackOpts struct { + // SrcOnly, if true, produces a `.src.rock` containing the rockspec + // and original source archive rather than a deployable `.rock`. + SrcOnly bool +} + +// InstalledRock is re-exported from the root package for caller +// convenience (so `client.InstalledRock` and `rocks.InstalledRock` both +// resolve to the same type). +type InstalledRock = rocks.InstalledRock + +// ShowInfo is re-exported from the root package — see InstalledRock. +type ShowInfo = rocks.ShowInfo + +// New constructs a Rocks facade from cfg. Validates the minimum required +// fields and wires up default implementations. +// +// The only error New itself returns is a descriptive error when cfg.Tree is +// empty. Tarantool-header validation is NOT done here: New does not stat +// cfg.Tarantool.IncludeDir; ErrMissingTarantoolHeaders surfaces later from the +// operations that actually need the headers (e.g. building a C-extension rock). +// +// opts apply at construction time. The default backend is BackendNative +// (the zero value); WithBackend overrides it. Backend selection is final +// for the returned *Rocks. The existing New(cfg) call shape is +// preserved by the variadic. +func New(cfg rocks.Config, opts ...Option) (*Rocks, error) { + if cfg.Tree == "" { + return nil, errors.New("rocks: Config.Tree is required") + } + + r := &Rocks{ + cfg: cfg, + store: manif.FileStore{}, + index: &remote.HTTPRemoteIndex{ + Servers: cfg.Servers, + InsecureServers: cfg.InsecureServers, + }, + logger: cfg.Logger, + } + if r.logger == nil { + r.logger = slog.New(slog.NewTextHandler(io.Discard, nil)) + } + + for _, opt := range opts { + opt(r) + } + // Select the engine per r.backend. The native engine shares the same + // cfg/store/index/logger so reads (r.store) and writes (r.engine) see + // one consistent tree. + native := &nativeEngine{ + cfg: r.cfg, + store: r.store, + index: r.index, + logger: r.logger, + } + + switch r.backend { + case BackendLua: + // The gopher-lua backend. Boot is lazy: newLuaEngine does not + // touch the VM here; the LState is created on first write call. The + // native engine is still constructed above (reads use r.store + // regardless of backend) — harmless if its write methods go + // unused for the lua backend. + r.engine = newLuaEngine(r.cfg, r.store, r.logger) + case BackendNative: + r.engine = native + default: + r.engine = native + } + + return r, nil +} + +// Exec runs an arbitrary LuaRocks command line — argv is everything after +// `luarocks` — through the embedded upstream dispatcher, letting LuaRocks print +// its own output to the process stdout/stderr verbatim. It is the escape hatch +// tt's `tt rocks` is built on; programmatic callers should prefer the typed +// methods (Install, Search, …), which capture and shape their output. +// +// progname is the program name LuaRocks prints in its usage/help text (e.g. +// "tt rocks"); use the library's default progname otherwise. +// +// Exec requires BackendLua: the native engine cannot run the full CLI, so on +// any other backend Exec returns rocks.ErrNotImplemented. +func (r *Rocks) Exec(ctx context.Context, progname string, argv []string) error { + le, ok := r.engine.(*luaEngine) + if !ok { + return rocks.ErrNotImplemented + } + + return le.callRaw(progname, argv) +} + +// ReadTreeManifest is the method-style convenience for +// manif.FileStore.ReadTree against r.cfg.Tree. It returns the top-level +// manifest the tree currently advertises (composes the store). +func (r *Rocks) ReadTreeManifest() (*rocks.Manifest, error) { + t, err := tree.Open(r.cfg) + if err != nil { + return nil, err + } + + return r.store.ReadTree(t.RocksDir()) +} + +// Install installs `name` (with optional version constraint in +// opts.Version) into r.cfg.Tree, including transitive deps per +// opts.Deps. The general algorithm: +// +// 1. Query the remote index for `name` candidates. +// 2. Pick the newest version satisfying opts.Version. +// 3. Resolve transitive deps (unless DepsNone). +// 4. For each step in topo order: fetch source, eval rockspec, merge +// platforms, validate, build, deploy, update tree manifest. +// 5. Install the requested rock itself. +// +// Returns ErrUnsupportedRockspecFeature for unrecognized build types +// (bubbled up from build.RunBackend). May also surface +// ErrMissingTarantoolHeaders when a C-extension rock is built. +func (r *Rocks) Install(ctx context.Context, name string, opts InstallOpts) error { + return r.engine.Install(ctx, name, opts) +} + +// Build evaluates the rockspec at specPath, fetches its declared source, +// runs the build backend, and deploys the result into r.cfg.Tree. +// +// Unlike Install, Build does not perform dependency resolution — it +// assumes prerequisites are already present (matching upstream +// `luarocks build`). +func (r *Rocks) Build(ctx context.Context, specPath string, opts BuildOpts) error { + return r.engine.Build(ctx, specPath, opts) +} + +// Make is "build the rockspec found in cwd against the source already +// present in cwd" — the upstream `luarocks make` flow. It is the +// developer-iteration form of Build. +func (r *Rocks) Make(ctx context.Context, opts MakeOpts) error { + return r.engine.Make(ctx, opts) +} + +// List returns every rock currently installed in r.cfg.Tree. +func (r *Rocks) List(ctx context.Context) ([]InstalledRock, error) { + _ = ctx // local read, no I/O cancellation needed + + t, err := tree.Open(r.cfg) + if err != nil { + return nil, err + } + + m, err := r.store.ReadTree(t.RocksDir()) + if err != nil { + if os.IsNotExist(unwrapPathErr(err)) { + return []InstalledRock{}, nil + } + + return nil, err + } + + out := []InstalledRock{} + + for name, versions := range m.Repository { + for ver := range versions { + out = append(out, InstalledRock{Name: name, Version: ver}) + } + } + + return out, nil +} + +// Show returns the summary info for a single rock installed in the tree. +// Returns an error wrapping os.ErrNotExist when the rock is not present. +func (r *Rocks) Show(ctx context.Context, name string) (*ShowInfo, error) { + _ = ctx + + t, err := tree.Open(r.cfg) + if err != nil { + return nil, err + } + + m, err := r.store.ReadTree(t.RocksDir()) + if err != nil { + return nil, err + } + + versions, ok := m.Repository[name] + if !ok { + return nil, fmt.Errorf("rocks.Show: %q not installed: %w", name, os.ErrNotExist) + } + // Pick the lexicographically lowest installed version string, so the + // choice is deterministic across runs regardless of map iteration order. + var picked string + for v := range versions { + if picked == "" || v < picked { + picked = v + } + } + + out := &ShowInfo{Package: name, Version: picked} + + // Load the per-rock manifest to enumerate modules. + rockManifestPath := filepath.Join(t.InstallDir(name, picked), "rock_manifest") + + rm, err := r.store.ReadRock(rockManifestPath) + if err == nil && rm != nil { + for k := range rm.Lua { + out.Modules = append(out.Modules, k) + } + + for k := range rm.Lib { + out.Modules = append(out.Modules, k) + } + } + + // Try to read the rockspec for richer info — non-fatal if missing. + specPath := filepath.Join(t.InstallDir(name, picked), name+"-"+picked+".rockspec") + if spec, err := rockspec.Eval(specPath, r.cfg.Rockspec); err == nil { + out.Summary = spec.Description.Summary + out.License = spec.Description.License + out.Homepage = spec.Description.Homepage + out.Dependencies = spec.Dependencies + } + + return out, nil +} + +// Which resolves a dotted Lua module name to its on-disk file path in +// r.cfg.Tree. Returns (path, true, nil) on hit; ("", false, nil) on miss. +func (r *Rocks) Which(ctx context.Context, module string) (string, bool, error) { + _ = ctx + + t, err := tree.Open(r.cfg) + if err != nil { + return "", false, err + } + + p, ok := t.Which(module) + + return p, ok, nil +} + +// Pack produces a .rock or .src.rock archive for `target` (rock name) in +// r.cfg.WorkingDir and returns its path. +// +// - opts.SrcOnly == false: zip the installed tree at +// /share/tarantool/rocks/// into -.rock. +// - opts.SrcOnly == true: zip the rockspec only into +// -.src.rock. (Source download lives outside the rockspec; +// upstream pulls source per `source.url` and inlines it. We omit that +// until a real packaging need surfaces — fail loud over over-engineering.) +func (r *Rocks) Pack(ctx context.Context, target string, opts PackOpts) (string, error) { + return r.engine.Pack(ctx, target, opts) +} + +// Unpack extracts `archive` (a .rock or .src.rock zip) into destDir. +func (r *Rocks) Unpack(ctx context.Context, archive, destDir string) error { + return r.engine.Unpack(ctx, archive, destDir) +} + +// --- engine-delegated operations not served by the native backend --- +// +// Each method below is a pure one-line delegation to r.engine — no +// backend-aware branching here. BackendNative returns rocks.ErrNotImplemented +// for every one of them; BackendLua runs the corresponding upstream +// LuaRocks command in the embedded VM. + +// Remove uninstalls a rock from r.cfg.Tree (upstream `luarocks remove`). +// BackendNative returns rocks.ErrNotImplemented; BackendLua runs upstream. +func (r *Rocks) Remove(ctx context.Context, name string, opts RemoveOpts) error { + return r.engine.Remove(ctx, name, opts) +} + +// Purge removes all rocks from r.cfg.Tree (upstream `luarocks purge`). +// BackendNative returns rocks.ErrNotImplemented; BackendLua runs upstream. +func (r *Rocks) Purge(ctx context.Context, opts PurgeOpts) error { + return r.engine.Purge(ctx, opts) +} + +// Search queries the configured servers for rocks matching pattern (upstream +// `luarocks search`). BackendNative returns rocks.ErrNotImplemented; BackendLua +// runs upstream. +func (r *Rocks) Search(ctx context.Context, pattern string, opts SearchOpts) ([]SearchResult, error) { + return r.engine.Search(ctx, pattern, opts) +} + +// Download fetches a rock file into r.cfg.WorkingDir and returns its path +// (upstream `luarocks download`). BackendNative returns +// rocks.ErrNotImplemented; BackendLua runs upstream. +func (r *Rocks) Download(ctx context.Context, name string, opts DownloadOpts) (string, error) { + return r.engine.Download(ctx, name, opts) +} + +// Lint checks the syntax of a rockspec (upstream `luarocks lint`). +// BackendNative returns rocks.ErrNotImplemented; BackendLua runs upstream. +func (r *Rocks) Lint(ctx context.Context, specPath string, opts LintOpts) error { + return r.engine.Lint(ctx, specPath, opts) +} + +// NewVersion writes an updated rockspec for a new version and returns its path +// (upstream `luarocks new_version`). BackendNative returns +// rocks.ErrNotImplemented; BackendLua runs upstream. +func (r *Rocks) NewVersion(ctx context.Context, specPath string, opts NewVersionOpts) (string, error) { + return r.engine.NewVersion(ctx, specPath, opts) +} + +// WriteRockspec writes a starter rockspec for sources at url and returns its +// path (upstream `luarocks write_rockspec`). BackendNative returns +// rocks.ErrNotImplemented; BackendLua runs upstream. +func (r *Rocks) WriteRockspec(ctx context.Context, url string, opts WriteRockspecOpts) (string, error) { + return r.engine.WriteRockspec(ctx, url, opts) +} + +// Doc shows or lists documentation for an installed rock (upstream +// `luarocks doc`). BackendNative returns rocks.ErrNotImplemented; BackendLua +// runs upstream. +func (r *Rocks) Doc(ctx context.Context, name string, opts DocOpts) error { + return r.engine.Doc(ctx, name, opts) +} + +// Test runs a rock's test suite (upstream `luarocks test`). BackendNative +// returns rocks.ErrNotImplemented; BackendLua runs upstream. +func (r *Rocks) Test(ctx context.Context, specPath string, opts TestOpts) error { + return r.engine.Test(ctx, specPath, opts) +} + +// Config reads or writes LuaRocks configuration and returns the printed value +// (upstream `luarocks config`). BackendNative returns rocks.ErrNotImplemented; +// BackendLua runs upstream. +func (r *Rocks) Config(ctx context.Context, opts ConfigOpts) (string, error) { + return r.engine.Config(ctx, opts) +} + +// Upload publishes a rockspec to a rocks server (upstream `luarocks upload`). +// BackendNative returns rocks.ErrNotImplemented; BackendLua runs upstream. +func (r *Rocks) Upload(ctx context.Context, specPath string, opts UploadOpts) error { + return r.engine.Upload(ctx, specPath, opts) +} + +// InitProject scaffolds a LuaRocks project in r.cfg.WorkingDir (upstream +// `luarocks init`). BackendNative returns rocks.ErrNotImplemented; BackendLua +// runs upstream. +func (r *Rocks) InitProject(ctx context.Context, opts InitProjectOpts) error { + return r.engine.InitProject(ctx, opts) +} + +// Admin runs a `luarocks admin ` repository-administration command +// (upstream `luarocks admin`). BackendNative returns rocks.ErrNotImplemented; +// BackendLua runs upstream. +func (r *Rocks) Admin(ctx context.Context, subCmd string, args []string, opts AdminOpts) error { + return r.engine.Admin(ctx, subCmd, args, opts) +} + +// --- internal helpers --- + +// matchModulePath reports whether the on-disk slashed path (with extension) +// appears verbatim in the deployed file map, returning the matched key. It is +// an exact lookup; conflict-munged siblings (`_` suffixes) are +// not matched here. +func matchModulePath(deployed map[string]string, want string) (string, bool) { + if _, ok := deployed[want]; ok { + return want, true + } + + return "", false +} + +// appendUnique adds s to xs if not already present. +func appendUnique(xs []string, s string) []string { + if slices.Contains(xs, s) { + return xs + } + + return append(xs, s) +} + +func evalAndPrepare(specPath string, cfg rocks.Config) (*rocks.Rockspec, error) { + spec, err := rockspec.Eval(specPath, cfg.Rockspec) + if err != nil { + return nil, fmt.Errorf("rockspec.Eval %s: %w", specPath, err) + } + + rockspec.MergePlatforms(spec, rockspec.RuntimePlatforms()) + + if err := rockspec.Validate(spec); err != nil { + return nil, fmt.Errorf("rockspec.Validate %s: %w", specPath, err) + } + + return spec, nil +} + +// findRockspecIn locates the rockspec of a fetched registry artifact. It +// scans ONLY the top level of dir: a bare `.rockspec` download is a single +// top-level file, and a `.src.rock` archive carries its rockspec at the +// archive root. We deliberately do NOT recurse — a .src.rock also bundles +// the rock's source tree, which may itself ship a `rockspecs/` directory of +// unrelated rockspec files (e.g. `say`), and recursing would ambiguously +// match those. +func findRockspecIn(dir string) (string, error) { + entries, err := os.ReadDir(dir) + if err != nil { + return "", fmt.Errorf("read %s: %w", dir, err) + } + + var found string + + for _, ent := range entries { + if ent.IsDir() || !strings.HasSuffix(ent.Name(), ".rockspec") { + continue + } + + p := filepath.Join(dir, ent.Name()) + if found != "" { + return "", fmt.Errorf("multiple .rockspec under %s: %s and %s", dir, found, p) + } + + found = p + } + + if found == "" { + return "", fmt.Errorf("no .rockspec under %s", dir) + } + + return found, nil +} + +// pickNewest filters candidates by cs and returns the highest version. +// Mirrors deps.pickNewest (which is unexported); duplicating here keeps +// the client free of a deps-internal symbol dependency. +func pickNewest(candidates []rocks.VersionedRock, cs []rocks.VersionConstraint) (rocks.VersionedRock, bool) { + var best rocks.VersionedRock + + have := false + + for _, c := range candidates { + if !deps.Match(c.Version, cs) { + continue + } + + if !have || deps.Compare(c.Version, best.Version) > 0 { + best = c + have = true + } + } + + return best, have +} + +func unwrapPathErr(err error) error { + type unwrapper interface{ Unwrap() error } + + for err != nil { + pe := &os.PathError{} + if errors.As(err, &pe) { + return pe.Err + } + + u, ok := err.(unwrapper) + if !ok { + return err + } + + err = u.Unwrap() + } + + return nil +} + +// zipDir recursively zips srcDir contents into outPath. Entries are +// stored with paths relative to srcDir. +func zipDir(outPath, srcDir string) error { + f, err := os.Create(outPath) + if err != nil { + return err + } + + defer func() { _ = f.Close() }() + + zw := zip.NewWriter(f) + + walkErr := filepath.Walk(srcDir, func(p string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + rel, err := filepath.Rel(srcDir, p) + if err != nil { + return err + } + + w, err := zw.Create(filepath.ToSlash(rel)) + if err != nil { + return err + } + + in, err := os.Open(p) + if err != nil { + return err + } + + defer func() { _ = in.Close() }() + + _, err = io.Copy(w, in) + + return err + }) + if walkErr != nil { + _ = zw.Close() + + return walkErr + } + + return zw.Close() +} + +func zipSingleFile(outPath, srcPath, entryName string) error { + in, err := os.Open(srcPath) + if err != nil { + return err + } + + defer func() { _ = in.Close() }() + + out, err := os.Create(outPath) + if err != nil { + return err + } + + defer func() { _ = out.Close() }() + + zw := zip.NewWriter(out) + + w, err := zw.Create(entryName) + if err != nil { + _ = zw.Close() + + return err + } + + if _, err := io.Copy(w, in); err != nil { + _ = zw.Close() + + return err + } + + return zw.Close() +} diff --git a/lib/luarocks/client/engine.go b/lib/luarocks/client/engine.go new file mode 100644 index 000000000..8870c56ad --- /dev/null +++ b/lib/luarocks/client/engine.go @@ -0,0 +1,84 @@ +package client + +import ( + "context" +) + +// Engine is the backend contract for every write operation a Rocks facade +// can perform. The public *Rocks write methods are pure delegations to the +// active engine; the engine is selected once at New() time and is +// final for the lifetime of the returned *Rocks. +// +// Engine contains every operation both backends (native, lua) must +// answer for. Where a backend cannot perform an operation, its method +// returns rocks.ErrNotImplemented — never a silent no-op or zero-value +// success. Callers discriminate with errors.Is(err, rocks.ErrNotImplemented). +// +// Read operations (List, Show, Which, ReadTreeManifest) are intentionally +// NOT part of Engine: they are served by the native r.store regardless of +// the selected backend, so they stay on *Rocks directly. +// +//nolint:interfacebloat // Engine deliberately mirrors the full LuaRocks command surface; splitting it would fragment the single backend contract. +type Engine interface { + // The five operations the native backend already implements. + Install(ctx context.Context, name string, opts InstallOpts) error + Build(ctx context.Context, specPath string, opts BuildOpts) error + Make(ctx context.Context, opts MakeOpts) error + Pack(ctx context.Context, target string, opts PackOpts) (string, error) + Unpack(ctx context.Context, archive, destDir string) error + + // The thirteen operations not yet implemented by the native backend. + // Until a backend implements them they return rocks.ErrNotImplemented. + Remove(ctx context.Context, name string, opts RemoveOpts) error + Purge(ctx context.Context, opts PurgeOpts) error + Search(ctx context.Context, pattern string, opts SearchOpts) ([]SearchResult, error) + Download(ctx context.Context, name string, opts DownloadOpts) (string, error) + Lint(ctx context.Context, specPath string, opts LintOpts) error + NewVersion(ctx context.Context, specPath string, opts NewVersionOpts) (string, error) + WriteRockspec(ctx context.Context, url string, opts WriteRockspecOpts) (string, error) + Doc(ctx context.Context, name string, opts DocOpts) error + Test(ctx context.Context, specPath string, opts TestOpts) error + Config(ctx context.Context, opts ConfigOpts) (string, error) + Upload(ctx context.Context, specPath string, opts UploadOpts) error + InitProject(ctx context.Context, opts InitProjectOpts) error + Admin(ctx context.Context, subCmd string, args []string, opts AdminOpts) error +} + +// Backend selects which Engine implementation a Rocks facade uses. The zero +// value is BackendNative — the pure-Go backend that has always served this +// package — so New(cfg) with no options keeps existing behavior. +type Backend int + +const ( + // BackendNative is the pure-Go implementation (nativeEngine). It is the + // default (zero value) and serves all five currently-implemented write + // operations. + BackendNative Backend = iota + // BackendLua selects the gopher-lua backend, which boots an embedded + // LuaRocks VM lazily on the first write call and serves every upstream + // LuaRocks command. + BackendLua +) + +// String renders the Backend for debug logging. +func (b Backend) String() string { + switch b { + case BackendNative: + return "native" + case BackendLua: + return "lua" + default: + return "unknown" + } +} + +// Option configures a Rocks at construction time. Apply via New(cfg, opts...). +type Option func(*Rocks) + +// WithBackend selects the Engine backend for the constructed Rocks. The +// selection is applied at New() time and is final for the returned *Rocks. +func WithBackend(b Backend) Option { + return func(r *Rocks) { + r.backend = b + } +} diff --git a/lib/luarocks/client/lua.go b/lib/luarocks/client/lua.go new file mode 100644 index 000000000..bb02adaec --- /dev/null +++ b/lib/luarocks/client/lua.go @@ -0,0 +1,1417 @@ +package client + +// luaEngine is the gopher-lua backend (BackendLua). It boots the embedded +// Tarantool LuaRocks fork (3.9.2) into a single, long-lived gopher-lua VM and +// dispatches write operations through extra/wrapper.lua's exec() entry point. +// +// Environment-override policy: +// +// The engine installs a custom os.getenv into the VM that consults an +// envOverride map first and falls through to the host process env for any +// other key. The override map serves only the four LuaRocks build-config keys +// consumed by extra/hardcoded.lua — LUAROCKS_PREFIX, LUA_INCDIR, LUA_BINDIR, +// TARANTOOL_DIR. Every other os.getenv read (CC, CFLAGS, LDFLAGS, AR, RANLIB, +// LINK, MT, MAKE, WINDRES, CMAKE_*, PATH, HOME, USER, TMPDIR/TMP/TEMP, XDG_*, +// http_proxy/https_proxy/no_proxy, LUAROCKS_SYSCONFDIR, +// LUAROCKS_CROSS_COMPILING, LUA_PATH_/LUA_CPATH_, and Windows-only vars) falls +// through to the host process env by design — the build toolchain genuinely +// lives there. The engine never mutates the host env: it never calls +// os.Setenv. +// +// Boot is lazy: newLuaEngine does not touch the VM; the LState is created +// and populated on the first call() via bootOnce. The LState is single-threaded: +// concurrent call()s serialize through e.mu. Only Lua that arrived via +// the embedded FS is executed — embedded module bytes via LoadString plus the +// fixed wrapper dispatch string via DoString; no caller-supplied Lua source and +// no loadfile/dofile exposure. + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "regexp" + "runtime" + "strings" + "sync" + + lua "github.com/yuin/gopher-lua" + rocks "github.com/tarantool/tt/lib/luarocks" + luarocksembed "github.com/tarantool/tt/lib/luarocks/internal/luarocks" +) + +// osExitSentinel prefixes the Lua error raised by the engine's os.exit +// replacement so call() can recover the requested exit code from the error +// message produced by DoString. +const osExitSentinel = "go-luarocks os.exit: " + +const ( + // luaConfigFileMode is the permission for the generated LUAROCKS_CONFIG + // file: owner read/write only (it holds no secrets but needs no wider access). + luaConfigFileMode = 0o600 + + // ioOpenModeArgIndex is the 1-based Lua argument position of io.open's mode + // string; ioOpenMinArgsForMode is the argument count at or above which a + // mode string is present (io.open(filename, mode)). + ioOpenMinArgsForMode = 2 + + // printWriteArgCount is the number of values pushed for the io.stdout write + // call in the print shim (the file handle plus the line string). + printWriteArgCount = 2 + + // searchFieldsMin is the minimum tab-separated field count a --porcelain + // search line must have to be a result record (name, version, arch, repo). + searchFieldsMin = 4 + + // decimalBase is the radix used when accumulating the exit-code digits. + decimalBase = 10 + + // globalArgsTreePair counts the two argv entries the mandatory --tree global + // always contributes; per-server entries add two more apiece. + globalArgsTreePair = 2 + argsPerServer = 2 +) + +// luaEngine implements the Engine interface against an embedded LuaRocks VM. +type luaEngine struct { + cfg rocks.Config + store rocks.ManifestStore + logger *slog.Logger + envOverride map[string]string + + lstate *lua.LState + bootOnce sync.Once + bootErr error + mu sync.Mutex + + // configDir is the temp dir holding the generated LUAROCKS_CONFIG file + // (set by writeConfigFile during boot). It must outlive boot since cfg.lua + // is read on first require; a finalizer (set in newLuaEngine) removes it and + // closes the LState when the engine becomes unreachable — best-effort, since + // the engine has no explicit Close in the public API. + configDir string + + // callImpl, when non-nil, replaces callViaState as the dispatch backend. + // It exists solely as a test seam so dispatch tests can assert the exact + // argv a method builds (and feed a canned stdout to data-returning + // methods) without booting the embedded VM. Production code leaves it nil. + callImpl func(argv []string) (string, error) +} + +// newLuaEngine constructs the engine and populates the env-override map from +// cfg.Tarantool. It does NOT boot the LState — boot is lazy on first +// call(). +func newLuaEngine(cfg rocks.Config, store rocks.ManifestStore, logger *slog.Logger) *luaEngine { + envOverride := map[string]string{} + // Only add an entry when the source value is non-empty so the host-env + // fallback can still apply for keys we have no opinion on. + if cfg.Tarantool.Prefix != "" { + envOverride["LUAROCKS_PREFIX"] = cfg.Tarantool.Prefix + envOverride["LUA_BINDIR"] = filepath.Join(cfg.Tarantool.Prefix, "bin") + } + + if cfg.Tarantool.IncludeDir != "" { + envOverride["LUA_INCDIR"] = cfg.Tarantool.IncludeDir + envOverride["TARANTOOL_DIR"] = filepath.Dir(cfg.Tarantool.IncludeDir) + } + + if cfg.Tarantool.Version != "" { + // The tarantool/luarocks fork registers `tarantool` as a provided + // dependency from this env var (util.lua add_provided_versions) — there + // is no `_TARANTOOL` global in the gopher-lua VM, so the env is the only + // source. Without it, rockspecs with `dependencies = {"tarantool >= X"}` + // fail resolution. Mirrors tt's TT_CLI_TARANTOOL_VERSION. + envOverride["TT_CLI_TARANTOOL_VERSION"] = cfg.Tarantool.Version + } + + e := &luaEngine{ + cfg: cfg, + store: store, + logger: logger, + envOverride: envOverride, + } + // Best-effort cleanup of the temp config dir and the cached LState when the + // engine is garbage-collected. The engine has no public Close (adding one + // would burden every caller); a finalizer keeps New(...WithBackend(BackendLua)) + // from leaking a temp dir per instance (e.g. across test runs). + runtime.SetFinalizer(e, func(e *luaEngine) { e.cleanup() }) + + return e +} + +// cleanup removes the generated config dir and closes the cached LState. Safe +// to call when neither was created (empty configDir, nil lstate). +func (e *luaEngine) cleanup() { + if e.configDir != "" { + _ = os.RemoveAll(e.configDir) + } + + if e.lstate != nil { + e.lstate.Close() + } +} + +// luaPreloadMap maps gopher-lua require names to their embedded-FS paths. +// Mirrors tt/cli/rocks/rocks.go's rocks_preload, with the path prefixes adapted +// to this module's embed layout: upstream modules live under "src/src/" (the +// git subtree placed the upstream repo root at internal/luarocks/src/), shims +// under "extra/". macosx maps to the upstream module — we ship no extra shim. +var luaPreloadMap = func() map[string]string { + rocksPath := "src/src/" + extraPath := "extra/" + + return map[string]string{ + "extra.wrapper": extraPath + "wrapper.lua", + "luarocks.core.hardcoded": extraPath + "hardcoded.lua", + "luarocks.core.util": rocksPath + "luarocks/core/util.lua", + "luarocks.core.persist": rocksPath + "luarocks/core/persist.lua", + "luarocks.core.sysdetect": rocksPath + "luarocks/core/sysdetect.lua", + "luarocks.core.cfg": rocksPath + "luarocks/core/cfg.lua", + "luarocks.core.dir": rocksPath + "luarocks/core/dir.lua", + "luarocks.core.path": rocksPath + "luarocks/core/path.lua", + "luarocks.core.manif": rocksPath + "luarocks/core/manif.lua", + "luarocks.core.vers": rocksPath + "luarocks/core/vers.lua", + "luarocks.util": rocksPath + "luarocks/util.lua", + "luarocks.loader": rocksPath + "luarocks/loader.lua", + "luarocks.dir": rocksPath + "luarocks/dir.lua", + "luarocks.path": rocksPath + "luarocks/path.lua", + "luarocks.fs": rocksPath + "luarocks/fs.lua", + "luarocks.persist": rocksPath + "luarocks/persist.lua", + "luarocks.fun": rocksPath + "luarocks/fun.lua", + "luarocks.tools.patch": rocksPath + "luarocks/tools/patch.lua", + "luarocks.tools.zip": rocksPath + "luarocks/tools/zip.lua", + "luarocks.tools.tar": rocksPath + "luarocks/tools/tar.lua", + "luarocks.fs.unix": rocksPath + "luarocks/fs/unix.lua", + // luarocks.fs.macosx is INTENTIONALLY NOT preloaded. Its is_dir/is_file + // (internal/luarocks/.../fs/macosx.lua) probe directory-ness via + // io.open(path.."/.", "r") and inspect the PUC-Lua errno returned as the + // third result (codes 2/13/20/21). gopher-lua's io.open does not surface + // libc errno that way, so macosx.is_dir returns false for EVERY existing + // directory — which breaks fs.check_command_permissions (the tree always + // looks unwritable) and every downstream mkdir/copy. Omitting the module + // makes require("luarocks.fs.macosx") fail in load_platform_fns's pcall, + // so fs.init falls through to the shell-out tools backend + // (luarocks.fs.unix.tools → `test -d`), which works correctly in-VM. + "luarocks.fs.unix.tools": rocksPath + "luarocks/fs/unix/tools.lua", + "luarocks.fs.lua": rocksPath + "luarocks/fs/lua.lua", + "luarocks.fs.tools": rocksPath + "luarocks/fs/tools.lua", + "luarocks.queries": rocksPath + "luarocks/queries.lua", + "luarocks.type_check": rocksPath + "luarocks/type_check.lua", + "luarocks.type.rockspec": rocksPath + "luarocks/type/rockspec.lua", + "luarocks.rockspecs": rocksPath + "luarocks/rockspecs.lua", + "luarocks.signing": rocksPath + "luarocks/signing.lua", + "luarocks.fetch": rocksPath + "luarocks/fetch.lua", + "luarocks.type.manifest": rocksPath + "luarocks/type/manifest.lua", + "luarocks.manif": rocksPath + "luarocks/manif.lua", + "luarocks.build.builtin": rocksPath + "luarocks/build/builtin.lua", + "luarocks.deps": rocksPath + "luarocks/deps.lua", + "luarocks.deplocks": rocksPath + "luarocks/deplocks.lua", + "luarocks.cmd": rocksPath + "luarocks/cmd.lua", + "luarocks.argparse": rocksPath + "luarocks/argparse.lua", + "luarocks.test.busted": rocksPath + "luarocks/test/busted.lua", + "luarocks.test.command": rocksPath + "luarocks/test/command.lua", + "luarocks.results": rocksPath + "luarocks/results.lua", + "luarocks.search": rocksPath + "luarocks/search.lua", + "luarocks.repos": rocksPath + "luarocks/repos.lua", + "luarocks.cmd.show": rocksPath + "luarocks/cmd/show.lua", + "luarocks.cmd.path": rocksPath + "luarocks/cmd/path.lua", + "luarocks.cmd.write_rockspec": rocksPath + "luarocks/cmd/write_rockspec.lua", + "luarocks.manif.writer": rocksPath + "luarocks/manif/writer.lua", + "luarocks.remove": rocksPath + "luarocks/remove.lua", + "luarocks.pack": rocksPath + "luarocks/pack.lua", + "luarocks.build": rocksPath + "luarocks/build.lua", + "luarocks.cmd.make": rocksPath + "luarocks/cmd/make.lua", + "luarocks.cmd.build": rocksPath + "luarocks/cmd/build.lua", + "luarocks.cmd.install": rocksPath + "luarocks/cmd/install.lua", + "luarocks.cmd.list": rocksPath + "luarocks/cmd/list.lua", + "luarocks.download": rocksPath + "luarocks/download.lua", + "luarocks.cmd.download": rocksPath + "luarocks/cmd/download.lua", + "luarocks.cmd.search": rocksPath + "luarocks/cmd/search.lua", + "luarocks.cmd.pack": rocksPath + "luarocks/cmd/pack.lua", + "luarocks.cmd.new_version": rocksPath + "luarocks/cmd/new_version.lua", + "luarocks.cmd.purge": rocksPath + "luarocks/cmd/purge.lua", + "luarocks.cmd.init": rocksPath + "luarocks/cmd/init.lua", + "luarocks.cmd.lint": rocksPath + "luarocks/cmd/lint.lua", + "luarocks.test": rocksPath + "luarocks/test.lua", + "luarocks.cmd.test": rocksPath + "luarocks/cmd/test.lua", + "luarocks.cmd.which": rocksPath + "luarocks/cmd/which.lua", + "luarocks.cmd.remove": rocksPath + "luarocks/cmd/remove.lua", + "luarocks.upload.multipart": rocksPath + "luarocks/upload/multipart.lua", + "luarocks.upload.api": rocksPath + "luarocks/upload/api.lua", + "luarocks.cmd.upload": rocksPath + "luarocks/cmd/upload.lua", + "luarocks.cmd.doc": rocksPath + "luarocks/cmd/doc.lua", + "luarocks.cmd.unpack": rocksPath + "luarocks/cmd/unpack.lua", + "luarocks.cmd.config": rocksPath + "luarocks/cmd/config.lua", + "luarocks.require": rocksPath + "luarocks/require.lua", + "luarocks.build.cmake": rocksPath + "luarocks/build/cmake.lua", + "luarocks.build.make": rocksPath + "luarocks/build/make.lua", + "luarocks.build.command": rocksPath + "luarocks/build/command.lua", + "luarocks.fetch.cvs": rocksPath + "luarocks/fetch/cvs.lua", + "luarocks.fetch.svn": rocksPath + "luarocks/fetch/svn.lua", + "luarocks.fetch.sscm": rocksPath + "luarocks/fetch/sscm.lua", + "luarocks.fetch.git": rocksPath + "luarocks/fetch/git.lua", + "luarocks.fetch.git_file": rocksPath + "luarocks/fetch/git_file.lua", + "luarocks.fetch.git_http": rocksPath + "luarocks/fetch/git_http.lua", + "luarocks.fetch.git_https": rocksPath + "luarocks/fetch/git_https.lua", + "luarocks.fetch.git_ssh": rocksPath + "luarocks/fetch/git_ssh.lua", + "luarocks.fetch.hg": rocksPath + "luarocks/fetch/hg.lua", + "luarocks.fetch.hg_http": rocksPath + "luarocks/fetch/hg_http.lua", + "luarocks.fetch.hg_https": rocksPath + "luarocks/fetch/hg_https.lua", + "luarocks.fetch.hg_ssh": rocksPath + "luarocks/fetch/hg_ssh.lua", + "luarocks.admin.cache": rocksPath + "luarocks/admin/cache.lua", + "luarocks.admin.cmd.refresh_cache": rocksPath + "luarocks/admin/cmd/refresh_cache.lua", + "luarocks.admin.index": rocksPath + "luarocks/admin/index.lua", + "luarocks.admin.cmd.add": rocksPath + "luarocks/admin/cmd/add.lua", + "luarocks.admin.cmd.remove": rocksPath + "luarocks/admin/cmd/remove.lua", + "luarocks.admin.cmd.make_manifest": rocksPath + "luarocks/admin/cmd/make_manifest.lua", + } +}() + +// boot creates the gopher-lua VM, installs the custom os.getenv and the +// glr_getwd global, preloads every embedded module, and caches the LState on +// e.lstate for the engine's lifetime. It is invoked exactly once via +// e.bootOnce.Do. The LState is intentionally NOT closed here — it lives as long +// as the engine. +// +// LState reuse vs tt: tt opens a fresh lua.NewState per command and closes it, +// so each command re-initializes LuaRocks config. We reuse one cached LState to +// amortize the preload cost, which means LuaRocks' module-level state — +// notably core.cfg, guarded by cfg.initialized in the tarantool fork — persists +// across calls. This is safe here because cfg.Tree and cfg.Tarantool are fixed +// for the engine's lifetime, so the cached config stays correct; +// TestLuaEngine_ReusedLState_SequentialMakes is the regression gate. The one +// known caveat is cfg.rocks_servers, which the fork *prepends* per --server, so +// repeated calls passing different InstallOpts.Servers on the SAME engine +// accumulate sources; a caller needing isolated server sets constructs a new +// *Rocks (the same "want isolation → new client" rule). +func (e *luaEngine) boot() error { + // Materialize the LuaRocks config file that aligns the tree layout with the + // native backend (tt-style subdirs). The vendored cfg.lua does NOT + // consume hardcoded.lua's ROCKS_SUBDIR / LUA_MODULES_*_SUBDIR keys (those + // were tt patches); left to its defaults it would write to + // /lib/luarocks/rocks-5.1 and /share/lua/5.1, diverging from the + // native engine's /share/tarantool/rocks etc. We point LUAROCKS_CONFIG + // at a generated file that sets the three path subdirs, which cfg.init + // deep-merges over the defaults. This is the LuaRocks-native override + // mechanism and keeps the no-Setenv invariant intact: LUAROCKS_CONFIG is + // served via the in-VM os.getenv shim (envOverride), never os.Setenv. + err := e.writeConfigFile() + if err != nil { + return err + } + + L := lua.NewState() // full stdlibs; do NOT skip OpenLibs + + // Install the custom os.getenv and glr_getwd BEFORE preloading, since + // hardcoded.lua reads them at require time. + osTbl, ok := L.GetField(L.Get(lua.EnvironIndex), "os").(*lua.LTable) + if !ok { + return errors.New("luaEngine: os table missing from Lua environment") + } + + L.SetField(osTbl, "getenv", L.NewFunction(func(L *lua.LState) int { + key := L.CheckString(1) + if v, ok := e.envOverride[key]; ok { + L.Push(lua.LString(v)) + + return 1 + } + + if v, ok := os.LookupEnv(key); ok { + L.Push(lua.LString(v)) + + return 1 + } + + L.Push(lua.LNil) + + return 1 + })) + + L.SetGlobal("glr_getwd", L.NewFunction(func(L *lua.LState) int { + L.Push(lua.LString(e.cfg.WorkingDir)) + + return 1 + })) + + // Normalize io.open mode strings. LuaRocks' luarocks/fs/lua.lua opens files + // with PUC-Lua-5.1 mode strings of the form "+b" (e.g. "r+b", "w+b", + // "a+b") for fs.is_writable / fs.copy_binary. gopher-lua's io.open accepts + // only the "b+" ordering ("rb+", "wb+", "ab+") and raises "invalid + // option" otherwise, crashing the command (exit 99). Wrap the original + // io.open to rewrite the "+b" tail to "b+" before delegating; every other + // mode passes through untouched. This is a VM-compatibility shim, not a + // behavior change — the resulting file is opened in the identical mode. + ioTbl, ok := L.GetField(L.Get(lua.EnvironIndex), "io").(*lua.LTable) + if !ok { + return errors.New("luaEngine: io table missing from Lua environment") + } + + origOpen, ok := L.GetField(ioTbl, "open").(*lua.LFunction) + if !ok { + return errors.New("luaEngine: io.open missing from Lua environment") + } + + L.SetField(ioTbl, "open", L.NewFunction(func(L *lua.LState) int { + // Collect every argument so we can re-dispatch verbatim, rewriting + // only the mode (arg 2) when it uses the unsupported "+b" ordering. + top := L.GetTop() + + args := make([]lua.LValue, top) + + for i := 1; i <= top; i++ { + args[i-1] = L.Get(i) + } + + if top >= ioOpenMinArgsForMode { + if ls, ok := args[1].(lua.LString); ok { + args[1] = lua.LString(normalizeOpenMode(string(ls))) + } + } + + L.Push(origOpen) + + for _, a := range args { + L.Push(a) + } + // io.open returns (file) on success or (nil, errmsg, errno) on failure; + // propagate all of them so LuaRocks' error handling is unchanged. + L.Call(len(args), lua.MultRet) + + return L.GetTop() - top + })) + + // Intercept os.exit. The vendored LuaRocks CLI terminates every command + // path with os.exit (luarocks/cmd.lua), which gopher-lua maps to Go's + // os.Exit — fatal for our in-process VM (one long-lived LState for the + // engine lifetime). Replace it with a function that raises a Lua error + // carrying the requested exit code (prefixed with osExitSentinel) so the + // surrounding DoString unwinds back into call() instead of killing the + // process. call() maps code 0 to a nil error and any non-zero code to a + // descriptive Go error. + L.SetField(osTbl, "exit", L.NewFunction(func(L *lua.LState) int { + code := L.OptInt(1, 0) + L.RaiseError("%s%d", osExitSentinel, code) + + return 0 + })) + + // Route the base print() through the VM's io.stdout. gopher-lua's stock + // print writes via fmt.Print to the host process stdout, bypassing the + // io.stdout field that redirectIO swaps for an in-memory buffer. A + // handful of LuaRocks commands emit user-facing data with print rather than + // util.printout — notably `config ` (cmd/config.lua print_entry prints + // string values via print). Without this shim that output would escape to + // the real stdout and the captured string callViaState returns would be + // empty, breaking the Config data parse. The shim tab-joins its args and + // appends a newline exactly like PUC-Lua print, then writes to whatever + // io.stdout currently is — so it honors the redirect and the no-Setenv + // invariant (in-VM handle only, never the host stdout or os.Setenv). + L.SetGlobal("print", L.NewFunction(func(L *lua.LState) int { + ioTbl, ok := L.GetField(L.Get(lua.EnvironIndex), "io").(*lua.LTable) + if !ok { + return 0 + } + + out := L.GetField(ioTbl, "stdout") + writeFn := L.GetField(out, "write") + top := L.GetTop() + + parts := make([]string, top) + + for i := 1; i <= top; i++ { + parts[i-1] = lua.LVAsString(L.Get(i)) + } + + line := strings.Join(parts, "\t") + "\n" + + L.Push(writeFn) + L.Push(out) + L.Push(lua.LString(line)) + L.Call(printWriteArgCount, 0) + + return 0 + })) + + preload := L.GetField(L.GetField(L.Get(lua.EnvironIndex), "package"), "preload") + + for modName, path := range luaPreloadMap { + src, err := luarocksembed.FS.ReadFile(path) + if err != nil { + return fmt.Errorf("luaEngine: read embedded %s: %w", path, err) + } + + mod, err := L.LoadString(string(src)) + if err != nil { + return fmt.Errorf("luaEngine: load %s: %w", modName, err) + } + + L.SetField(preload, modName, mod) + } + + e.lstate = L + + return nil +} + +// luaConfigContents builds the LuaRocks config file the engine generates. It +// serves two purposes: +// +// 1. Tree-layout parity with the native backend (tree/paths.go): +// rocks_subdir=/share/tarantool/rocks (RocksDir), +// lua_modules_path=/share/tarantool (DeployLuaDir), +// lib_modules_path=/lib/tarantool (DeployLibDir). These mirror +// hardcoded.lua's ROCKS_SUBDIR / LUA_MODULES_*_SUBDIR, which this vendored +// cfg.lua does not read. cfg.init deep-merges them over defaults. +// +// 2. Working-directory anchoring without host mutation. The shell-out fs +// backend (luarocks.fs.tools) resolves the base directory for every +// `cd && ` it runs from `cfg.variables.PWD` (default "pwd"), +// which would return the host process cwd — wrong for an in-process engine +// whose logical cwd is cfg.WorkingDir. We override PWD to echo WorkingDir, +// so relative build paths (e.g. builtin `cp src/foo.lua`) resolve against +// WorkingDir. This replaces a host os.Chdir (forbidden) with a +// per-command cd into the engine's configured working directory. +// +// workDir is single-quote-escaped for the Lua string literal and shell echo. +func luaConfigContents(workDir string) string { + escaped := strings.ReplaceAll(workDir, `'`, `'\''`) + + return fmt.Sprintf(`-- Generated by go-luarocks luaEngine. Aligns tree layout with the native +-- backend (tree/paths.go) and anchors the shell-out cwd to WorkingDir. Do not +-- edit by hand. +rocks_subdir = "/share/tarantool/rocks" +lua_modules_path = "/share/tarantool" +lib_modules_path = "/lib/tarantool" +variables = { + PWD = "echo '%s'", +} +`, escaped) +} + +// writeConfigFile materializes the generated config to a temp path and records +// LUAROCKS_CONFIG in the env-override map so cfg.init loads it. Content depends +// on cfg.WorkingDir, so it is regenerated per engine. +func (e *luaEngine) writeConfigFile() error { + dir, err := os.MkdirTemp("", "go-luarocks-cfg-") + if err != nil { + return fmt.Errorf("luaEngine: create config dir: %w", err) + } + + e.configDir = dir // tracked for finalizer cleanup + + path := filepath.Join(dir, "config-5.1.lua") + + if err := os.WriteFile(path, []byte(luaConfigContents(e.cfg.WorkingDir)), luaConfigFileMode); err != nil { + return fmt.Errorf("luaEngine: write config file: %w", err) + } + + e.envOverride["LUAROCKS_CONFIG"] = path + + return nil +} + +// dispatch routes argv to the active dispatch backend. callImpl is a test seam +// (nil in production); when nil, the real embedded-VM path callViaState runs. +// It returns the stdout the LuaRocks command printed plus any error. +func (e *luaEngine) dispatch(argv []string) (string, error) { + if e.callImpl != nil { + return e.callImpl(argv) + } + + return e.callViaState(argv) +} + +// call is a thin error-only wrapper over callViaState, retained for the boot +// smoke tests in lua_test.go that predate the unified seam. New code uses +// dispatch/callViaState directly. +func (e *luaEngine) call(argv []string) error { + _, err := e.callViaState(argv) + + return err +} + +// callViaState dispatches argv through extra/wrapper.lua's exec(). It serializes +// access to the single-threaded LState via e.mu, lazily booting on first +// use. progname is fixed to the default value. Each arg is single-quote- +// escaped for embedding in the Lua single-quoted dispatch string. +// +// stdout/stderr capture: before running the command, callViaState +// replaces the Lua VM's io.stdout and io.stderr fields with buffer-backed file +// tables (newWriterFile) pointing at in-memory Go buffers, restoring the +// originals afterward. LuaRocks emits all user-facing text through +// util.printout/printerr, which write to io.stdout/io.stderr, so this captures +// it. After the command finishes, the captured text is drained into e.logger at +// info level (slog.Default() if e.logger is nil); the no-Setenv invariant holds +// — we touch only the in-VM io handles, never the host process stdout/stderr or +// os.Setenv. The +// captured stdout string is returned so data-returning methods (e.g. Pack) can +// parse it. +func (e *luaEngine) callViaState(argv []string) (string, error) { + e.mu.Lock() + defer e.mu.Unlock() + + e.bootOnce.Do(func() { + e.bootErr = e.boot() + }) + + if e.bootErr != nil { + return "", e.bootErr + } + + var stdout, stderr bytes.Buffer + + restore := e.redirectIO(&stdout, &stderr) + defer restore() + + doErr := e.lstate.DoString(dispatchString("go-luarocks", argv)) + + // Drain whatever the command printed into the logger before interpreting + // the result, so even a failing command's diagnostics reach the caller. + restore() + e.drainOutput(argv, stdout.String(), stderr.String()) + out := stdout.String() + + if doErr == nil { + // The wrapper returned normally (no os.exit). Treat as success. + return out, nil + } + // The CLI almost always unwinds via our os.exit replacement, surfacing as + // a Lua error whose message embeds osExitSentinel + the exit code. A code + // of 0 is success; any non-zero code (or a non-sentinel error) is a real + // failure surfaced verbatim. + if code, ok := parseOsExit(doErr.Error()); ok { + if code == 0 { + return out, nil + } + + return out, fmt.Errorf("luaEngine: %v exited with code %d", argv, code) + } + + return out, doErr +} + +// dispatchString builds the Lua one-liner that invokes extra/wrapper.lua's +// exec() with the given program name and the single-quote-escaped argv. It is +// shared by callViaState (capturing, with the default progname) and callRaw +// (passthrough, caller-supplied progname so `tt rocks` can show its own usage). +func dispatchString(progname string, argv []string) string { + parts := make([]string, 0, len(argv)+1) + parts = append(parts, quoteLua(progname)) + + for _, arg := range argv { + parts = append(parts, quoteLua(arg)) + } + + return fmt.Sprintf("t=require('extra.wrapper').exec(%s)", strings.Join(parts, ", ")) +} + +// quoteLua wraps s in single quotes, escaping embedded single quotes, for +// embedding in the dispatch string. +func quoteLua(s string) string { + return "'" + strings.ReplaceAll(s, "'", `\'`) + "'" +} + +// callRaw dispatches argv through extra/wrapper.lua's exec() WITHOUT capturing +// io.stdout/io.stderr, so LuaRocks writes straight to the process's stdout and +// stderr. It backs the public Rocks.Exec escape hatch (tt's `tt rocks`), which +// must reproduce upstream's terminal output verbatim; the capturing +// callViaState exists for programmatic methods that parse the output. +// + +// progname is the program name LuaRocks prints in its own usage/help, letting +// the caller (e.g. tt) show "tt rocks" instead of the default progname. +// +// Like callViaState it serializes on e.mu and lazily boots on first use. +// Exit-code handling matches callViaState: os.exit(0) and a clean return +// are success, any other code is surfaced as an error. +func (e *luaEngine) callRaw(progname string, argv []string) error { + e.mu.Lock() + defer e.mu.Unlock() + + e.bootOnce.Do(func() { + e.bootErr = e.boot() + }) + + if e.bootErr != nil { + return e.bootErr + } + + doErr := e.lstate.DoString(dispatchString(progname, argv)) + if doErr == nil { + return nil + } + + if code, ok := parseOsExit(doErr.Error()); ok { + if code == 0 { + return nil + } + + return fmt.Errorf("luaEngine: %v exited with code %d", argv, code) + } + + return doErr +} + +// redirectIO points the VM's io.stdout and io.stderr at the supplied Go +// buffers by installing buffer-backed file userdata via io.output()/io.errput. +// It returns a function that restores the original handles; the function is +// idempotent so callViaState can both defer it and call it eagerly. Only the +// in-VM io handles are touched. +func (e *luaEngine) redirectIO(stdout, stderr io.Writer) func() { + L := e.lstate + + ioTbl, ok := L.GetField(L.Get(lua.EnvironIndex), "io").(*lua.LTable) + if !ok { + return func() {} + } + + origOut := L.GetField(ioTbl, "stdout") + origErr := L.GetField(ioTbl, "stderr") + + L.SetField(ioTbl, "stdout", newWriterFile(L, stdout)) + L.SetField(ioTbl, "stderr", newWriterFile(L, stderr)) + + restored := false + + return func() { + if restored { + return + } + + restored = true + + L.SetField(ioTbl, "stdout", origOut) + L.SetField(ioTbl, "stderr", origErr) + } +} + +// newWriterFile builds a Lua table that quacks like an io file handle for the +// subset of methods LuaRocks' util.printout/printerr use: write (called as +// f:write(...)) and the no-op flush/close. All bytes go to w. +func newWriterFile(L *lua.LState, w io.Writer) *lua.LTable { //nolint:gocritic // L is the conventional gopher-lua LState receiver name used throughout + tbl := L.NewTable() + write := L.NewFunction(func(L *lua.LState) int { + // arg 1 is the file table (self); the rest are the strings to write. + top := L.GetTop() + for i := 2; i <= top; i++ { + v := L.Get(i) + if v == lua.LNil { + continue + } + + if _, err := io.WriteString(w, lua.LVAsString(v)); err != nil { + L.RaiseError("go-luarocks io capture: %v", err) + } + } + + L.Push(tbl) // io files return self for chaining + + return 1 + }) + noop := L.NewFunction(func(L *lua.LState) int { + L.Push(tbl) + + return 1 + }) + + L.SetField(tbl, "write", write) + L.SetField(tbl, "flush", noop) + L.SetField(tbl, "close", noop) + + return tbl +} + +// drainOutput logs captured stdout/stderr at info level so command diagnostics +// are not lost. e.logger is used when set; otherwise slog.Default(). +func (e *luaEngine) drainOutput(argv []string, stdout, stderr string) { + logger := e.logger + if logger == nil { + logger = slog.Default() + } + + cmd := strings.Join(argv, " ") + if s := strings.TrimRight(stdout, "\n"); s != "" { + logger.Info("luaEngine stdout", "cmd", cmd, "output", s) + } + + if s := strings.TrimRight(stderr, "\n"); s != "" { + logger.Info("luaEngine stderr", "cmd", cmd, "output", s) + } +} + +// packedPathRE matches the line upstream luarocks pack prints on success: +// `Packed: ` (luarocks/pack.lua report_and_sign_local_file). +var packedPathRE = regexp.MustCompile(`(?m)^Packed:\s*(.+?)\s*$`) + +// parsePackPath extracts the produced rock path from pack's captured stdout. +// It returns ("", false) when no `Packed:` line is present so the caller can +// raise a real error rather than fabricating a path (no silent fallback). +func parsePackPath(stdout string) (string, bool) { + m := packedPathRE.FindStringSubmatch(stdout) + if m == nil { + return "", false + } + + return m[1], true +} + +// wrotePathRE matches the line LuaRocks prints after writing a rockspec file: +// - new_version: `Wrote ` (cmd/new_version.lua). +// - write_rockspec: `Wrote template at -- you should now edit ...` +// (cmd/write_rockspec.lua). +// +// Both share the `Wrote ` / `Wrote template at ` prefix; the alternation +// captures the path and stops the write_rockspec variant before its trailing +// " -- you should now edit and finish it." hint. +var wrotePathRE = regexp.MustCompile(`(?m)^Wrote (?:template at )?(.+?)(?: -- .*)?\s*$`) + +// parseWrotePath extracts the written rockspec path from new_version / +// write_rockspec stdout. Returns ("", false) when no `Wrote` line is present so +// the caller raises a real error rather than fabricating a path. +func parseWrotePath(stdout string) (string, bool) { + m := wrotePathRE.FindStringSubmatch(stdout) + if m == nil { + return "", false + } + + return m[1], true +} + +// parseSearchResults parses the --porcelain listing search.print_result_tree +// emits: one tab-separated record per match, +// "\t\t\t\t". Title lines are suppressed +// under --porcelain, so every non-blank line with at least three tab fields is +// a result; lines that do not match that shape are skipped (defensive against +// stray diagnostics that may share the buffer). Name, Version and Server (the +// repo URL, field index 3) are surfaced. +func parseSearchResults(stdout string) []SearchResult { + var out []SearchResult + + for _, line := range strings.Split(stdout, "\n") { + if strings.TrimSpace(line) == "" { + continue + } + + fields := strings.Split(line, "\t") + if len(fields) < searchFieldsMin { + continue + } + + out = append(out, SearchResult{ + Name: fields[0], + Version: fields[1], + Server: fields[3], + }) + } + + return out +} + +// normalizeOpenMode rewrites a PUC-Lua-5.1 file mode string into the form +// gopher-lua's io.open accepts. gopher-lua rejects the "+b" ordering +// (r+b, w+b, a+b) but accepts the equivalent "b+" ordering (rb+, wb+, +// ab+); both denote the same update-binary mode. Any other mode (including +// already-normalized or non-binary modes) is returned unchanged. +func normalizeOpenMode(mode string) string { + switch mode { + case "r+b": + return "rb+" + case "w+b": + return "wb+" + case "a+b": + return "ab+" + default: + return mode + } +} + +// parseOsExit extracts the exit code from a DoString error message produced by +// the engine's os.exit replacement. The gopher-lua error string embeds the +// sentinel somewhere after a position prefix; returns (code, true) on a match. +func parseOsExit(msg string) (int, bool) { + _, after, ok := strings.Cut(msg, osExitSentinel) + if !ok { + return 0, false + } + + rest := after + // The code runs up to the first non-digit (gopher-lua may append a + // stack traceback after the message). + end := 0 + for end < len(rest) && rest[end] >= '0' && rest[end] <= '9' { + end++ + } + + if end == 0 { + return 0, false + } + + code := 0 + for i := range end { + code = code*decimalBase + int(rest[i]-'0') + } + + return code, true +} + +// --- Engine interface implementation --- +// +// The five methods the native backend also serves (Install, Build, Make, Pack, +// Unpack) build the upstream argv and dispatch; the remaining methods map to +// upstream-only commands. + +// globalArgs returns the LuaRocks global options that must precede the +// subcommand name. --tree is ALWAYS emitted so the lua backend installs into +// the SAME tree the native engine uses (backend parity); it reads the engine's +// cfg.Tree. servers, when non-empty, append a --server per entry (the +// global option lives on the main parser, before the subcommand). +func (e *luaEngine) globalArgs(servers []string) []string { + argv := make([]string, 0, globalArgsTreePair+argsPerServer*len(servers)) + argv = append(argv, "--tree", e.cfg.Tree) + + for _, s := range servers { + argv = append(argv, "--server", s) + } + + return argv +} + +// depsModeArg maps a DepsPolicy to upstream's --deps-mode choices +// {all, one, order, none}. DepsAll→"all", DepsNone→"none". DepsOnlyNew has no +// exact upstream equivalent; "order" (use the current tree plus those below it +// on rocks_trees) is the closest match to the "only new" documented intent. +func depsModeArg(p DepsPolicy) string { + switch p { + case DepsAll: + return "all" + case DepsNone: + return "none" + case DepsOnlyNew: + // Closest upstream match to "only new" — see DepsPolicy doc. + return "order" + default: + return "all" + } +} + +func (e *luaEngine) Install(ctx context.Context, name string, opts InstallOpts) error { + argv := e.globalArgs(opts.Servers) + + argv = append(argv, "install", name) + + if opts.Version != "" { + argv = append(argv, opts.Version) + } + + argv = append(argv, "--deps-mode", depsModeArg(opts.Deps)) + _, err := e.dispatch(argv) + + return err +} + +func (e *luaEngine) Build(ctx context.Context, specPath string, opts BuildOpts) error { + argv := e.globalArgs(nil) + + argv = append(argv, "build", specPath) + + if opts.Keep { + argv = append(argv, "--keep") + } + + _, err := e.dispatch(argv) + + return err +} + +func (e *luaEngine) Make(ctx context.Context, opts MakeOpts) error { + argv := e.globalArgs(nil) + argv = append(argv, "make") + // Upstream make searches the current dir (glr_getwd) when no rockspec + // positional is given; only append it when explicitly set. + if opts.RockspecPath != "" { + argv = append(argv, opts.RockspecPath) + } + + _, err := e.dispatch(argv) + + return err +} + +func (e *luaEngine) Pack(ctx context.Context, target string, opts PackOpts) (string, error) { + // opts.SrcOnly: upstream pack has no dedicated flag — it produces a + // .src.rock when target is a rockspec and a binary .rock when target is an + // installed rock name (luarocks/cmd/pack.lua). There is no clean argv + // expression to force src-only for an installed-rock target through this + // path, so SrcOnly is honored implicitly by the target kind. We do NOT + // silently swallow it: when SrcOnly is set against a non-rockspec target it + // simply has no effect here, left for a follow-up rather than guessed. + argv := e.globalArgs(nil) + argv = append(argv, "pack", target) + + out, err := e.dispatch(argv) + if err != nil { + return "", err + } + + path, ok := parsePackPath(out) + if !ok { + return "", fmt.Errorf("luaEngine: pack %s succeeded but no 'Packed:' path line found in output: %q", target, out) + } + + return path, nil +} + +func (e *luaEngine) Unpack(ctx context.Context, archive, destDir string) error { + // Upstream unpack has no destination flag: it creates a directory derived + // from the rock name inside the current working dir (luarocks/cmd/unpack.lua + // run_unpacker → fs.make_dir(dir_name)). The VM's cwd is reported by + // glr_getwd, which returns cfg.WorkingDir. destDir is therefore not + // expressible as an argv flag here; the caller controls the extraction root + // via cfg.WorkingDir. We do not invent a flag or silently drop destDir — + // the parameter's effect is documented as bounded by cfg.WorkingDir. + _ = destDir + argv := e.globalArgs(nil) + argv = append(argv, "unpack", archive) + _, err := e.dispatch(argv) + + return err +} + +// Remove runs `luarocks remove`. It is tree-scoped: the --tree global precedes +// the subcommand. name is the rock; opts.Version narrows to one version. +func (e *luaEngine) Remove(ctx context.Context, name string, opts RemoveOpts) error { + argv := e.globalArgs(nil) + + argv = append(argv, "remove", name) + + if opts.Version != "" { + argv = append(argv, opts.Version) + } + + if opts.Force { + argv = append(argv, "--force") + } + + if opts.ForceFast { + argv = append(argv, "--force-fast") + } + + argv = append(argv, "--deps-mode", depsModeArg(opts.Deps)) + _, err := e.dispatch(argv) + + return err +} + +// Purge runs `luarocks purge` against the engine's tree (the mandatory --tree +// global is always emitted). +func (e *luaEngine) Purge(ctx context.Context, opts PurgeOpts) error { + argv := e.globalArgs(nil) + + argv = append(argv, "purge") + + if opts.OldVersions { + argv = append(argv, "--old-versions") + } + + if opts.Force { + argv = append(argv, "--force") + } + + if opts.ForceFast { + argv = append(argv, "--force-fast") + } + + _, err := e.dispatch(argv) + + return err +} + +// Search runs `luarocks search --porcelain` and parses the tab-separated +// listing into []SearchResult. --porcelain is always passed so the output is +// machine-parseable. Errors loud if the command fails. +func (e *luaEngine) Search(ctx context.Context, pattern string, opts SearchOpts) ([]SearchResult, error) { + argv := e.globalArgs(opts.Servers) + + argv = append(argv, "search", pattern) + + if opts.Version != "" { + argv = append(argv, opts.Version) + } + + if opts.Source { + argv = append(argv, "--source") + } + + if opts.Binary { + argv = append(argv, "--binary") + } + + if opts.All { + argv = append(argv, "--all") + } + + argv = append(argv, "--porcelain") + + out, err := e.dispatch(argv) + if err != nil { + return nil, err + } + // An empty result set is a legitimate outcome (a successful search that + // matched nothing prints no listing lines under --porcelain); return an + // empty slice, not an error. + return parseSearchResults(out), nil +} + +// Download runs `luarocks download` and returns the path of the downloaded +// file. NOTE: the embedded LuaRocks 3.9.2 does not print the saved path — +// cmd/download.lua emits nothing on success and copies the file into the +// current directory (= cfg.WorkingDir via glr_getwd). Rather than fabricate a +// path (no silent fallback), Download diffs the download directory across +// the dispatch and returns the file that appeared; on ambiguity (overwrite or +// >1 new file) it returns the containing directory. Success returns a nil error. +func (e *luaEngine) Download(ctx context.Context, name string, opts DownloadOpts) (string, error) { + argv := e.globalArgs(opts.Servers) + + argv = append(argv, "download", name) + + if opts.Version != "" { + argv = append(argv, opts.Version) + } + + if opts.All { + argv = append(argv, "--all") + } + + if opts.Source { + argv = append(argv, "--source") + } + + if opts.Rockspec { + argv = append(argv, "--rockspec") + } + + if opts.Arch != "" { + argv = append(argv, "--arch", opts.Arch) + } + // The embedded LuaRocks 3.9.2 (download.lua) prints no saved path, so we can't + // parse one. Instead, snapshot the download directory (fs.current_dir() == + // cfg.WorkingDir) before and after and report the file that appeared. This + // is a real path, never fabricated; on success the error is always nil. + before := dirFiles(e.cfg.WorkingDir) + + if _, err := e.dispatch(argv); err != nil { + return "", err + } + + var added []string + + for name := range dirFiles(e.cfg.WorkingDir) { + if !before[name] { + added = append(added, name) + } + } + + if len(added) == 1 { + return filepath.Join(e.cfg.WorkingDir, added[0]), nil + } + // Zero new files (an existing file was overwritten on re-download) or more + // than one (ambiguous): the exact filename can't be uniquely identified. + // Return the directory the rock was downloaded into — honest, not fabricated. + return e.cfg.WorkingDir, nil +} + +// dirFiles returns the set of regular-file names directly in dir. A missing or +// empty dir yields an empty set (no error) — used by Download to diff the +// download directory across a dispatch. +func dirFiles(dir string) map[string]bool { + out := map[string]bool{} + if dir == "" { + return out + } + + entries, err := os.ReadDir(dir) + if err != nil { + return out + } + + for _, ent := range entries { + if ent.Type().IsRegular() { + out[ent.Name()] = true + } + } + + return out +} + +// Lint runs `luarocks lint `. lint takes only the rockspec +// positional. Non-zero exit (syntax error) surfaces as a real error. +func (e *luaEngine) Lint(ctx context.Context, specPath string, opts LintOpts) error { + _ = opts // lint has no tunable flags + argv := []string{"lint", specPath} + _, err := e.dispatch(argv) + + return err +} + +// NewVersion runs `luarocks new_version ` and returns the path of the +// written rockspec parsed from the `Wrote ` line. Fails loud if that line +// is absent. +func (e *luaEngine) NewVersion(ctx context.Context, specPath string, opts NewVersionOpts) (string, error) { + argv := []string{"new_version", specPath} + if opts.NewVersion != "" { + argv = append(argv, opts.NewVersion) + } + + if opts.NewURL != "" { + argv = append(argv, opts.NewURL) + } + + if opts.Dir != "" { + argv = append(argv, "--dir", opts.Dir) + } + + if opts.Tag != "" { + argv = append(argv, "--tag", opts.Tag) + } + + out, err := e.dispatch(argv) + if err != nil { + return "", err + } + + path, ok := parseWrotePath(out) + if !ok { + return "", fmt.Errorf("luaEngine: new_version %s succeeded but no 'Wrote' path line found in output: %q", specPath, out) + } + + return path, nil +} + +// WriteRockspec runs `luarocks write_rockspec` and returns the path of the +// written rockspec parsed from the `Wrote template at ` line. Fails loud +// if that line is absent. The wrapper command name uses an underscore. +func (e *luaEngine) WriteRockspec(ctx context.Context, url string, opts WriteRockspecOpts) (string, error) { + argv := []string{"write_rockspec"} + // Positionals are name, version, location in that order; upstream infers + // missing leading ones. Emit name/version only when set, then the url + // (location) last. + if opts.Name != "" { + argv = append(argv, opts.Name) + } + + if opts.Version != "" { + argv = append(argv, opts.Version) + } + + if url != "" { + argv = append(argv, url) + } + + if opts.Output != "" { + argv = append(argv, "--output", opts.Output) + } + + if opts.License != "" { + argv = append(argv, "--license", opts.License) + } + + if opts.Summary != "" { + argv = append(argv, "--summary", opts.Summary) + } + + if opts.Detailed != "" { + argv = append(argv, "--detailed", opts.Detailed) + } + + if opts.Homepage != "" { + argv = append(argv, "--homepage", opts.Homepage) + } + + if opts.LuaVersions != "" { + argv = append(argv, "--lua-versions", opts.LuaVersions) + } + + if opts.RockspecFormat != "" { + argv = append(argv, "--rockspec-format", opts.RockspecFormat) + } + + if opts.Tag != "" { + argv = append(argv, "--tag", opts.Tag) + } + + if opts.Lib != "" { + argv = append(argv, "--lib", opts.Lib) + } + + out, err := e.dispatch(argv) + if err != nil { + return "", err + } + + path, ok := parseWrotePath(out) + if !ok { + return "", fmt.Errorf("luaEngine: write_rockspec %s succeeded but no 'Wrote' path line found in output: %q", url, out) + } + + return path, nil +} + +// Doc runs `luarocks doc`. It is tree-scoped (it resolves an installed rock), +// so the --tree global precedes the subcommand. +func (e *luaEngine) Doc(ctx context.Context, name string, opts DocOpts) error { + argv := e.globalArgs(nil) + + argv = append(argv, "doc", name) + + if opts.Version != "" { + argv = append(argv, opts.Version) + } + + if opts.Home { + argv = append(argv, "--home") + } + + if opts.List { + argv = append(argv, "--list") + } + + _, err := e.dispatch(argv) + + return err +} + +// Test runs `luarocks test `. It is tree-scoped (test installs deps +// into the tree), so the --tree global precedes the subcommand. opts.Args are +// passed through as the trailing suite arguments. +func (e *luaEngine) Test(ctx context.Context, specPath string, opts TestOpts) error { + argv := e.globalArgs(nil) + + argv = append(argv, "test", specPath) + + if opts.Prepare { + argv = append(argv, "--prepare") + } + + if opts.TestType != "" { + argv = append(argv, "--test-type", opts.TestType) + } + + argv = append(argv, opts.Args...) + _, err := e.dispatch(argv) + + return err +} + +// Config runs `luarocks config` and returns the captured stdout (the printed +// value for a key, or the whole config when no key is given). When the command +// writes (Value or Unset set) there is no value to print and the trimmed empty +// output is returned. Fails loud on command error. +func (e *luaEngine) Config(ctx context.Context, opts ConfigOpts) (string, error) { + argv := []string{"config"} + if opts.Key != "" { + argv = append(argv, opts.Key) + } + + if opts.Value != "" { + argv = append(argv, opts.Value) + } + + if opts.Unset { + argv = append(argv, "--unset") + } + + if opts.Scope != "" { + argv = append(argv, "--scope", opts.Scope) + } + + if opts.JSON { + argv = append(argv, "--json") + } + + out, err := e.dispatch(argv) + if err != nil { + return "", err + } + + return strings.TrimRight(out, "\n"), nil +} + +// Upload runs `luarocks upload `. Void: success is an exit-0 dispatch. +func (e *luaEngine) Upload(ctx context.Context, specPath string, opts UploadOpts) error { + argv := []string{"upload", specPath} + if opts.SrcRock != "" { + argv = append(argv, opts.SrcRock) + } + + if opts.SkipPack { + argv = append(argv, "--skip-pack") + } + + if opts.APIKey != "" { + argv = append(argv, "--api-key", opts.APIKey) + } + + if opts.TempKey != "" { + argv = append(argv, "--temp-key", opts.TempKey) + } + + if opts.Force { + argv = append(argv, "--force") + } + + if opts.Sign { + argv = append(argv, "--sign") + } + + _, err := e.dispatch(argv) + + return err +} + +// InitProject runs `luarocks init` in cfg.WorkingDir. Void. +func (e *luaEngine) InitProject(ctx context.Context, opts InitProjectOpts) error { + argv := []string{"init"} + if opts.Name != "" { + argv = append(argv, opts.Name) + } + + if opts.Version != "" { + argv = append(argv, opts.Version) + } + + if opts.Reset { + argv = append(argv, "--reset") + } + + _, err := e.dispatch(argv) + + return err +} + +// Admin runs `luarocks admin `. The wrapper.lua exec() +// dispatches a leading "admin" arg to the admin command table, so argv begins +// with "admin" followed by the subcommand and its verbatim args. Admin +// commands are server-scoped, not tree-scoped, so no --tree global is emitted; +// opts.Server appends --server when set. Void. +func (e *luaEngine) Admin(ctx context.Context, subCmd string, args []string, opts AdminOpts) error { + argv := []string{"admin", subCmd} + + argv = append(argv, args...) + + if opts.Server != "" { + argv = append(argv, "--server", opts.Server) + } + + if opts.Force { + argv = append(argv, "--force") + } + + _, err := e.dispatch(argv) + + return err +} diff --git a/lib/luarocks/client/native.go b/lib/luarocks/client/native.go new file mode 100644 index 000000000..8c4b47c27 --- /dev/null +++ b/lib/luarocks/client/native.go @@ -0,0 +1,609 @@ +package client + +import ( + "archive/zip" + "context" + "errors" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "strings" + + rocks "github.com/tarantool/tt/lib/luarocks" + "github.com/tarantool/tt/lib/luarocks/build" + "github.com/tarantool/tt/lib/luarocks/deps" + "github.com/tarantool/tt/lib/luarocks/fetch" + "github.com/tarantool/tt/lib/luarocks/remote" + "github.com/tarantool/tt/lib/luarocks/tree" +) + +// nativeEngine is the pure-Go backend (BackendNative). It holds the same +// state the *Rocks methods used before the Engine extraction and retains +// EXACT behavioral compatibility with the pre-task client.Rocks: the +// five implemented method bodies and their private helpers are moved here +// verbatim, with only the receiver renamed from (r *Rocks) to (e *nativeEngine). +// +// The thirteen operations the native backend does not implement return +// rocks.ErrNotImplemented — never a silent no-op. +type nativeEngine struct { + cfg rocks.Config + store rocks.ManifestStore + index rocks.RemoteIndex + logger *slog.Logger +} + +// unpackDirMode is the permission applied to directories created while +// extracting a .rock archive: owner rwx, group rx, no world access. +const unpackDirMode = 0o750 + +// luaInterpreter is the interpreter name baked into generated command +// wrappers, matching the tarantool/luarocks fork's hardcoded.LUA_INTERPRETER. +const luaInterpreter = "tarantool" + +// defaultSysconfDir is upstream LuaRocks' cfg.sysconfdir fallback on Unix +// (luarocks/core/cfg.lua) when neither LUAROCKS_SYSCONFDIR nor a detected +// install prefix applies. +const defaultSysconfDir = "/etc/luarocks" + +// sysconfDir mirrors upstream's cfg.sysconfdir resolution for the value +// exported as LUAROCKS_SYSCONFDIR in command wrappers: honor the +// LUAROCKS_SYSCONFDIR env override, else fall back to the Unix default. +func sysconfDir() string { + if v := os.Getenv("LUAROCKS_SYSCONFDIR"); v != "" { + return v + } + + return defaultSysconfDir +} + +// Install installs `name` (with optional version constraint in +// opts.Version) into e.cfg.Tree, including transitive deps per +// opts.Deps. The general algorithm: +// +// 1. Query the remote index for `name` candidates. +// 2. Pick the newest version satisfying opts.Version. +// 3. Resolve transitive deps (unless DepsNone). +// 4. For each step in topo order: fetch source, eval rockspec, merge +// platforms, validate, build, deploy, update tree manifest. +// 5. Install the requested rock itself. +// +// Returns ErrUnsupportedRockspecFeature for unrecognized build types +// (bubbled up from build.RunBackend). May also surface +// ErrMissingTarantoolHeaders when a C-extension rock is built. +func (e *nativeEngine) Install(ctx context.Context, name string, opts InstallOpts) error { + if name == "" { + return errors.New("rocks.Install: empty name") + } + + idx := e.index + if len(opts.Servers) > 0 { + idx = &remote.HTTPRemoteIndex{ + Servers: opts.Servers, + InsecureServers: e.cfg.InsecureServers, + } + } + + cs, err := deps.ParseConstraints(opts.Version) + if err != nil { + return fmt.Errorf("rocks.Install: parse version %q: %w", opts.Version, err) + } + + candidates, err := idx.Query(ctx, name) + if err != nil { + return fmt.Errorf("rocks.Install: query %q: %w", name, err) + } + + root, ok := pickNewest(candidates, cs) + if !ok { + return fmt.Errorf("rocks.Install: no version of %q matches %q", name, opts.Version) + } + + e.logger.Info("rocks.Install: selected", "name", name, "version", root.Version.Raw, "url", root.URL) + + // Fetch + eval root rockspec so we can resolve transitive deps. + rootSpec, err := e.fetchAndEval(ctx, root.URL) + if err != nil { + return fmt.Errorf("rocks.Install: fetch/eval root: %w", err) + } + + root.Spec = rootSpec + + var plan []rocks.InstallStep + if opts.Deps != DepsNone { + plan, err = deps.Resolve(ctx, rootSpec, idx) + if err != nil { + return fmt.Errorf("rocks.Install: resolve deps: %w", err) + } + } + + for _, step := range plan { + err := e.installStep(ctx, step) + if err != nil { + return fmt.Errorf("rocks.Install: dep %s-%s: %w", step.Name, step.Version.Raw, err) + } + } + + // Finally install the requested rock itself. Its source is fetched from + // the rockspec's source.url, exactly like every dependency step. + rootStep := rocks.InstallStep{ + Name: rootSpec.Package, + Version: root.Version, + URL: root.URL, + Rockspec: rootSpec, + } + if err := e.installFromSource(ctx, rootStep); err != nil { + return fmt.Errorf("rocks.Install: install root: %w", err) + } + + return nil +} + +// Build evaluates the rockspec at specPath, fetches its declared source, +// runs the build backend, and deploys the result into e.cfg.Tree. +// +// Unlike Install, Build does not perform dependency resolution — it +// assumes prerequisites are already present (matching upstream +// `luarocks build`). +func (e *nativeEngine) Build(ctx context.Context, specPath string, opts BuildOpts) error { + spec, err := evalAndPrepare(specPath, e.cfg) + if err != nil { + return err + } + + tmp, err := os.MkdirTemp(e.cfg.WorkingDir, "rocks-build-*") + if err != nil { + return fmt.Errorf("rocks.Build: mkdir tmp: %w", err) + } + + if !opts.Keep { + defer func() { _ = os.RemoveAll(tmp) }() + } + + srcDir, err := e.fetchSource(ctx, spec, tmp) + if err != nil { + return fmt.Errorf("rocks.Build: %w", err) + } + + return e.deployFromSource(ctx, spec, srcDir) +} + +// Make is "build the rockspec found in cwd against the source already +// present in cwd" — the upstream `luarocks make` flow. It is the +// developer-iteration form of Build. +func (e *nativeEngine) Make(ctx context.Context, opts MakeOpts) error { + specPath := opts.RockspecPath + if specPath == "" { + entries, err := os.ReadDir(e.cfg.WorkingDir) + if err != nil { + return fmt.Errorf("rocks.Make: read %s: %w", e.cfg.WorkingDir, err) + } + + var found []string + + for _, ent := range entries { + if !ent.IsDir() && strings.HasSuffix(ent.Name(), ".rockspec") { + found = append(found, filepath.Join(e.cfg.WorkingDir, ent.Name())) + } + } + + if len(found) == 0 { + return fmt.Errorf("rocks.Make: no .rockspec found in %s", e.cfg.WorkingDir) + } + + if len(found) > 1 { + return fmt.Errorf("rocks.Make: multiple .rockspec found in %s (%v); pass MakeOpts.RockspecPath", e.cfg.WorkingDir, found) + } + + specPath = found[0] + } + + spec, err := evalAndPrepare(specPath, e.cfg) + if err != nil { + return err + } + + return e.deployFromSource(ctx, spec, e.cfg.WorkingDir) +} + +// Pack produces a .rock or .src.rock archive for `target` (rock name) in +// e.cfg.WorkingDir and returns its path. +// +// - opts.SrcOnly == false: zip the installed tree at +// /share/tarantool/rocks/// into -.rock. +// - opts.SrcOnly == true: zip the rockspec only into +// -.src.rock. (Source download lives outside the rockspec; +// upstream pulls source per `source.url` and inlines it. We omit that +// until a real packaging need surfaces — fail loud over over-engineering.) +func (e *nativeEngine) Pack(ctx context.Context, target string, opts PackOpts) (string, error) { + _ = ctx + + t, err := tree.Open(e.cfg) + if err != nil { + return "", err + } + + m, err := e.store.ReadTree(t.RocksDir()) + if err != nil { + return "", err + } + + versions, ok := m.Repository[target] + if !ok { + return "", fmt.Errorf("rocks.Pack: %q not installed in %s", target, t.Tree) + } + + var picked string + for v := range versions { + if picked == "" || v < picked { + picked = v + } + } + + installDir := t.InstallDir(target, picked) + + suffix := ".rock" + if opts.SrcOnly { + suffix = ".src.rock" + } + + outPath := filepath.Join(e.cfg.WorkingDir, target+"-"+picked+suffix) + + if opts.SrcOnly { + specPath := filepath.Join(installDir, target+"-"+picked+".rockspec") + + err := zipSingleFile(outPath, specPath, target+"-"+picked+".rockspec") + if err != nil { + return "", fmt.Errorf("rocks.Pack: zip rockspec: %w", err) + } + } else { + err := zipDir(outPath, installDir) + if err != nil { + return "", fmt.Errorf("rocks.Pack: zip install dir: %w", err) + } + } + + return outPath, nil +} + +// Unpack extracts `archive` (a .rock or .src.rock zip) into destDir. +func (e *nativeEngine) Unpack(ctx context.Context, archive, destDir string) error { + _ = ctx + + if err := os.MkdirAll(destDir, unpackDirMode); err != nil { + return fmt.Errorf("rocks.Unpack: mkdir: %w", err) + } + + zr, err := zip.OpenReader(archive) + if err != nil { + return fmt.Errorf("rocks.Unpack: open %s: %w", archive, err) + } + + defer func() { _ = zr.Close() }() + + cleanDest := filepath.Clean(destDir) + + for _, f := range zr.File { + target := filepath.Join(destDir, f.Name) + if !strings.HasPrefix(target, cleanDest+string(os.PathSeparator)) && target != cleanDest { + return fmt.Errorf("rocks.Unpack: entry %q escapes destDir", f.Name) + } + + if f.FileInfo().IsDir() { + err := os.MkdirAll(target, unpackDirMode) + if err != nil { + return err + } + + continue + } + + if err := os.MkdirAll(filepath.Dir(target), unpackDirMode); err != nil { + return err + } + + rc, err := f.Open() + if err != nil { + return err + } + + out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + _ = rc.Close() + + return err + } + + if _, err := io.Copy(out, rc); err != nil { + _ = rc.Close() + _ = out.Close() + + return err + } + + _ = rc.Close() + + if err := out.Close(); err != nil { + return err + } + } + + return nil +} + +// --- internal helpers (used only by the five native write operations) --- + +func (e *nativeEngine) installStep(ctx context.Context, step rocks.InstallStep) error { + spec, err := e.fetchAndEval(ctx, step.URL) + if err != nil { + return err + } + + step.Rockspec = spec + + return e.installFromSource(ctx, step) +} + +// fetchAndEval fetches the resolver-produced rock/rockspec URL into a +// throwaway directory, locates the `.rockspec` it contains (a bare +// .rockspec, or one bundled inside a .src.rock), and evaluates it. Only the +// parsed *Rockspec is returned — the fetched directory is the registry +// artifact, NOT the rock's source tree, so it is removed before returning. +// The actual source is fetched separately from spec.Source.URL at build +// time (see installFromSource / Build), mirroring upstream luarocks which +// never builds against the rockspec download directory. +func (e *nativeEngine) fetchAndEval(ctx context.Context, urlStr string) (*rocks.Rockspec, error) { + tmp, err := os.MkdirTemp(e.cfg.WorkingDir, "rocks-fetch-*") + if err != nil { + return nil, fmt.Errorf("mkdir tmp: %w", err) + } + + defer func() { _ = os.RemoveAll(tmp) }() + + srcDir, err := fetch.FetchWith(ctx, urlStr, tmp, fetch.Options{ + InsecureServers: e.cfg.InsecureServers, + }) + if err != nil { + return nil, fmt.Errorf("fetch %s: %w", urlStr, err) + } + + specPath, err := findRockspecIn(srcDir) + if err != nil { + return nil, err + } + + return evalAndPrepare(specPath, e.cfg) +} + +// installFromSource fetches the rockspec's declared source (spec.Source.URL) +// into a fresh build directory and builds+deploys against THAT — never +// against the rockspec download directory, which holds only the registry +// artifact. This mirrors nativeEngine.Build and upstream luarocks' +// fetch_sources → build flow. +func (e *nativeEngine) installFromSource(ctx context.Context, step rocks.InstallStep) error { + spec := step.Rockspec + + tmp, err := os.MkdirTemp(e.cfg.WorkingDir, "rocks-src-*") + if err != nil { + return fmt.Errorf("mkdir tmp: %w", err) + } + + defer func() { _ = os.RemoveAll(tmp) }() + + srcDir, err := e.fetchSource(ctx, spec, tmp) + if err != nil { + return err + } + + return e.deployFromSource(ctx, spec, srcDir) +} + +// fetchSource downloads spec.Source.URL into tmp and returns the source +// root to build against. After unpacking an archive the real source usually +// lives in a single top-level subdirectory (e.g. inspect.lua-3.1.3/), and +// build.modules paths are relative to that root — so descend into it, +// mirroring upstream luarocks' fetch.find_base_dir. +func (e *nativeEngine) fetchSource(ctx context.Context, spec *rocks.Rockspec, tmp string) (string, error) { + unpacked, err := fetch.FetchWith(ctx, spec.Source.URL, tmp, fetch.Options{ + InsecureServers: e.cfg.InsecureServers, + Tag: spec.Source.Tag, + Branch: spec.Source.Branch, + }) + if err != nil { + return "", fmt.Errorf("fetch %s: %w", spec.Source.URL, err) + } + + return findSourceBaseDir(unpacked, spec) +} + +// findSourceBaseDir mirrors upstream luarocks fetch.find_base_dir: pick the +// directory the rock's source actually lives in after unpacking. +// +// - If source.dir is set and names an existing subdirectory, use it +// (explicit override from the rockspec). +// - Else, if dir contains exactly one entry and it is a directory, descend +// into it (the common single-versioned-subdir tarball layout). +// - Else, use dir as-is (flat layout: a bare module file, or a git/file +// checkout whose files already sit at the top level). +func findSourceBaseDir(dir string, spec *rocks.Rockspec) (string, error) { + if spec.Source.Dir != "" { + cand := filepath.Join(dir, spec.Source.Dir) + if fi, err := os.Stat(cand); err == nil && fi.IsDir() { + return cand, nil + } + } + + entries, err := os.ReadDir(dir) + if err != nil { + return "", fmt.Errorf("read source dir %s: %w", dir, err) + } + + if len(entries) == 1 && entries[0].IsDir() { + return filepath.Join(dir, entries[0].Name()), nil + } + + return dir, nil +} + +func (e *nativeEngine) deployFromSource(ctx context.Context, spec *rocks.Rockspec, srcDir string) error { + t, err := tree.Open(e.cfg) + if err != nil { + return err + } + // Match upstream's command-wrapper deploy (repos.deploy_files → + // fs.wrap_script): the interpreter path is LUA_BINDIR/lua_interpreter + // (prefix/bin/tarantool), and LUAROCKS_SYSCONFDIR mirrors cfg.sysconfdir. + t.BinWrap = &tree.BinWrap{ + Interpreter: filepath.Join(e.cfg.Tarantool.Prefix, "bin", luaInterpreter), + Sysconfdir: sysconfDir(), + } + + destDir := filepath.Join(t.InstallDir(spec.Package, spec.Version), "build") + if err := os.MkdirAll(destDir, unpackDirMode); err != nil { + return fmt.Errorf("mkdir destDir: %w", err) + } + + if err := build.RunBackend(ctx, spec, srcDir, destDir, e.cfg); err != nil { + return err + } + // srcDir holds original .lua / install / copy_dirs files; destDir holds + // compiled .so artifacts from the build phase. Deploy reads from both. + rm, err := t.Deploy(spec, srcDir, destDir) + if err != nil { + return fmt.Errorf("deploy: %w", err) + } + + rockManifestPath := filepath.Join(t.InstallDir(spec.Package, spec.Version), "rock_manifest") + if err := e.store.WriteRock(rockManifestPath, rm); err != nil { + return fmt.Errorf("write rock_manifest: %w", err) + } + // Update the top-level tree manifest. + m, err := e.store.ReadTree(t.RocksDir()) + if err != nil { + // Treat missing manifest as a fresh tree. + if os.IsNotExist(unwrapPathErr(err)) { + m = &rocks.Manifest{ + Repository: map[string]map[string]rocks.RepoEntry{}, + Modules: map[string][]string{}, + Commands: map[string][]string{}, + Dependencies: map[string]map[string][]rocks.Dep{}, + } + } else { + return fmt.Errorf("read tree manifest: %w", err) + } + } + + if m.Repository == nil { + m.Repository = map[string]map[string]rocks.RepoEntry{} + } + + if m.Repository[spec.Package] == nil { + m.Repository[spec.Package] = map[string]rocks.RepoEntry{} + } + + if m.Modules == nil { + m.Modules = map[string][]string{} + } + + if m.Commands == nil { + m.Commands = map[string][]string{} + } + // Build the per-arch entry's modules/commands index from what tree.Deploy + // actually wrote (rm.Lua, rm.Lib, rm.Bin). Module names in the rockspec + // are dotted; on-disk paths are slashed — invert by reading rm directly. + entry := rocks.RepoEntry{Arch: "installed"} + pkgVer := spec.Package + "/" + spec.Version + + if len(rm.Lua) > 0 || len(rm.Lib) > 0 { + entry.Modules = map[string]string{} + + for modName := range spec.Build.Modules { + // Derive on-disk path from the deployed RockManifest: prefer + // rm.Lib (compiled .so) over rm.Lua (the .lua source). + slashed := strings.ReplaceAll(modName, ".", "/") + if p, ok := matchModulePath(rm.Lib, slashed+".so"); ok { + entry.Modules[modName] = p + m.Modules[modName] = appendUnique(m.Modules[modName], pkgVer) + + continue + } + + if p, ok := matchModulePath(rm.Lua, slashed+".lua"); ok { + entry.Modules[modName] = p + m.Modules[modName] = appendUnique(m.Modules[modName], pkgVer) + + continue + } + } + } + + if len(rm.Bin) > 0 { + entry.Commands = map[string]string{} + for binName, srcRel := range spec.Build.Install.Bin { + entry.Commands[binName] = srcRel + m.Commands[binName] = appendUnique(m.Commands[binName], pkgVer) + } + } + + m.Repository[spec.Package][spec.Version] = entry + if err := e.store.WriteTree(t.RocksDir(), m); err != nil { + return fmt.Errorf("write tree manifest: %w", err) + } + + return nil +} + +// --- unimplemented operations: loud ErrNotImplemented --- + +func (e *nativeEngine) Remove(ctx context.Context, name string, opts RemoveOpts) error { + return rocks.ErrNotImplemented +} + +func (e *nativeEngine) Purge(ctx context.Context, opts PurgeOpts) error { + return rocks.ErrNotImplemented +} + +func (e *nativeEngine) Search(ctx context.Context, pattern string, opts SearchOpts) ([]SearchResult, error) { + return nil, rocks.ErrNotImplemented +} + +func (e *nativeEngine) Download(ctx context.Context, name string, opts DownloadOpts) (string, error) { + return "", rocks.ErrNotImplemented +} + +func (e *nativeEngine) Lint(ctx context.Context, specPath string, opts LintOpts) error { + return rocks.ErrNotImplemented +} + +func (e *nativeEngine) NewVersion(ctx context.Context, specPath string, opts NewVersionOpts) (string, error) { + return "", rocks.ErrNotImplemented +} + +func (e *nativeEngine) WriteRockspec(ctx context.Context, url string, opts WriteRockspecOpts) (string, error) { + return "", rocks.ErrNotImplemented +} + +func (e *nativeEngine) Doc(ctx context.Context, name string, opts DocOpts) error { + return rocks.ErrNotImplemented +} + +func (e *nativeEngine) Test(ctx context.Context, specPath string, opts TestOpts) error { + return rocks.ErrNotImplemented +} + +func (e *nativeEngine) Config(ctx context.Context, opts ConfigOpts) (string, error) { + return "", rocks.ErrNotImplemented +} + +func (e *nativeEngine) Upload(ctx context.Context, specPath string, opts UploadOpts) error { + return rocks.ErrNotImplemented +} + +func (e *nativeEngine) InitProject(ctx context.Context, opts InitProjectOpts) error { + return rocks.ErrNotImplemented +} + +func (e *nativeEngine) Admin(ctx context.Context, subCmd string, args []string, opts AdminOpts) error { + return rocks.ErrNotImplemented +} diff --git a/lib/luarocks/client/types.go b/lib/luarocks/client/types.go new file mode 100644 index 000000000..b804eb030 --- /dev/null +++ b/lib/luarocks/client/types.go @@ -0,0 +1,206 @@ +package client + +// This file holds the option/result types for the thirteen Engine write +// operations the native backend does not implement. Fields are modeled from +// the upstream luarocks/cmd/.lua argparse blocks (cmd:flag → Go bool, +// cmd:option → Go string, cmd:argument that varies → method parameter). +// +// Intentionally lean: only the commonly useful flags are surfaced, not every +// hidden/debug option. For every field, the Go zero value (false / "" / nil) +// maps to the upstream default — i.e. the flag/option is simply omitted from +// the argv when the field is unset. + +// RemoveOpts tunes Remove (luarocks remove). See cmd/remove.lua. +type RemoveOpts struct { + // Version, if set, restricts removal to that exact version; empty removes + // all versions of the rock (the `version` positional). + Version string + // Force maps to --force: remove even if it would break dependencies. + Force bool + // ForceFast maps to --force-fast: forced removal without reporting + // dependency issues. + ForceFast bool + // Deps maps to --deps-mode {all,one,order,none}. + Deps DepsPolicy +} + +// PurgeOpts tunes Purge (luarocks purge). See cmd/purge.lua. Purge always +// operates on the engine's configured tree (the --tree global is mandatory +// upstream and is emitted automatically). +type PurgeOpts struct { + // OldVersions maps to --old-versions: keep the highest version of each + // rock and remove the rest. + OldVersions bool + // Force maps to --force: with --old-versions, force removal even if it + // would break dependencies. + Force bool + // ForceFast maps to --force-fast: like Force but without dependency + // reporting. + ForceFast bool +} + +// SearchOpts tunes Search (luarocks search). See cmd/search.lua. The engine +// always passes --porcelain so the listing is machine-parseable; that flag is +// not modeled here. +type SearchOpts struct { + // Version, if set, is the `version` positional to search for. + Version string + // Source maps to --source: return only rockspecs and source rocks. + Source bool + // Binary maps to --binary: return only pure-Lua and binary rocks. + Binary bool + // All maps to --all: list all suitable contents of the server(s). + All bool + // Servers, when non-empty, appends a --server global option per entry. + Servers []string +} + +// SearchResult is a single match returned by Search. It mirrors the +// --porcelain print format of search.print_result_tree: +// "\t\t\t\t". +type SearchResult struct { + // Name is the matched rock name. + Name string + // Version is the matched version-revision string. + Version string + // Server is the repository URL the match came from. + Server string +} + +// DownloadOpts tunes Download (luarocks download). See cmd/download.lua. +type DownloadOpts struct { + // Version, if set, is the `version` positional. + Version string + // All maps to --all: download all files when multiple match. + All bool + // Source maps to --source: download the .src.rock if available. + Source bool + // Rockspec maps to --rockspec: download the .rockspec if available. + Rockspec bool + // Arch maps to --arch : download for a specific architecture. + Arch string + // Servers, when non-empty, appends a --server global option per entry. + Servers []string +} + +// LintOpts tunes Lint (luarocks lint). See cmd/lint.lua. lint takes only the +// rockspec positional, so there are no tunable flags. +type LintOpts struct{} + +// NewVersionOpts tunes NewVersion (luarocks new_version). See +// cmd/new_version.lua. The rockspec/name is the method parameter. +type NewVersionOpts struct { + // NewVersion, if set, is the `new_version` positional. + NewVersion string + // NewURL, if set, is the `new_url` positional (requires NewVersion). + NewURL string + // Dir maps to --dir : output directory for the new rockspec. + Dir string + // Tag maps to --tag : new SCM tag. + Tag string +} + +// WriteRockspecOpts tunes WriteRockspec (luarocks write_rockspec). See +// cmd/write_rockspec.lua. The location/url is the method parameter; name and +// version are options here since upstream can infer them. +type WriteRockspecOpts struct { + // Name, if set, is the `name` positional. + Name string + // Version, if set, is the `version` positional. + Version string + // Output maps to --output : write the rockspec with this filename. + Output string + // License maps to --license . + License string + // Summary maps to --summary . + Summary string + // Detailed maps to --detailed . + Detailed string + // Homepage maps to --homepage . + Homepage string + // LuaVersions maps to --lua-versions . + LuaVersions string + // RockspecFormat maps to --rockspec-format . + RockspecFormat string + // Tag maps to --tag . + Tag string + // Lib maps to --lib : comma-separated C libraries to link to. + Lib string +} + +// DocOpts tunes Doc (luarocks doc). See cmd/doc.lua. Doc is tree-scoped (it +// looks up an installed rock), so the --tree global is emitted automatically. +type DocOpts struct { + // Version, if set, is the `version` positional. + Version string + // Home maps to --home: open the project home page. + Home bool + // List maps to --list: list documentation files only. + List bool +} + +// TestOpts tunes Test (luarocks test). See cmd/test.lua. The rockspec is the +// method parameter. +type TestOpts struct { + // Prepare maps to --prepare: install test deps only, do not run the suite. + Prepare bool + // TestType maps to --test-type : select the test suite type. + TestType string + // Args are passed through as the trailing `args` positionals to the suite. + Args []string +} + +// ConfigOpts tunes Config (luarocks config). See cmd/config.lua. +type ConfigOpts struct { + // Key, if set, is the `key` positional: prints that entry's value. Empty + // prints the whole effective configuration. + Key string + // Value, if set, is the `value` positional: writes Key=Value instead of + // printing. + Value string + // Unset maps to --unset: delete Key from the configuration file. + Unset bool + // Scope maps to --scope {system,user,project}. + Scope string + // JSON maps to --json: output as JSON. + JSON bool +} + +// UploadOpts tunes Upload (luarocks upload). See cmd/upload.lua. The rockspec +// is the method parameter. +type UploadOpts struct { + // SrcRock, if set, is the `src-rock` positional (a matching .src.rock). + SrcRock string + // SkipPack maps to --skip-pack: do not pack and send the source rock. + SkipPack bool + // APIKey maps to --api-key . + APIKey string + // TempKey maps to --temp-key . + TempKey string + // Force maps to --force: replace an existing rockspec of the same revision. + Force bool + // Sign maps to --sign: upload a signature file alongside each file. + Sign bool +} + +// InitProjectOpts tunes InitProject (luarocks init). See cmd/init.lua. +type InitProjectOpts struct { + // Name, if set, is the `name` positional (the project name). Empty lets + // upstream derive it from the working directory. + Name string + // Version, if set, is the `version` positional. + Version string + // Reset maps to --reset: delete and regenerate the project config and + // ./lua tree. + Reset bool +} + +// AdminOpts tunes Admin (luarocks admin ). See luarocks/admin/cmd/*. +// Admin subcommands vary widely; common cross-cutting flags are modeled and +// the rest are passed verbatim via the Admin args parameter. +type AdminOpts struct { + // Server maps to --server : the server to operate on. + Server string + // Force maps to --force where the subcommand supports it. + Force bool +} diff --git a/lib/luarocks/config.go b/lib/luarocks/config.go new file mode 100644 index 000000000..f2191dde7 --- /dev/null +++ b/lib/luarocks/config.go @@ -0,0 +1,53 @@ +package rocks + +import "log/slog" + +// Config carries every input a Rocks operation needs. +// +// WorkingDir is explicit — the facade never reads process cwd; callers pass +// WorkingDir directly. +// +// Tarantool fields come from one source of truth on the caller side; the +// library does not parse `tarantool --version` itself. +type Config struct { + // Tree is the root of the Tarantool rocks tree to read from and install into. + Tree string + // WorkingDir is the base directory for relative paths and build staging; + // the facade never reads the process cwd. + WorkingDir string + // Tarantool describes the Tarantool installation rocks are built against. + Tarantool TarantoolConfig + // Servers are the rocks-repository base URLs queried for remote operations. + Servers []string + // Rockspec governs the sandboxed rockspec evaluator. + Rockspec RockspecConfig + // Logger receives structured operation logs; nil disables logging. + Logger *slog.Logger + + // InsecureServers lists URL hosts (no scheme) for which TLS certificate + // verification is skipped by the http fetcher. Escape hatch: upstream + // luarocks disables certs for `rocks.tarantool.org`; we ALLOW that opt-in + // via this field but default to verifying. + InsecureServers []string +} + +// TarantoolConfig describes the Tarantool installation rocks are built against. +// Executable is the path to the `tarantool` binary; Prefix and IncludeDir +// come from the caller (typically tt's GetTarantoolPrefix). Version is the +// "x.y.z" string for diagnostic / golden-file headers. +type TarantoolConfig struct { + Executable string + Prefix string + IncludeDir string + Version string +} + +// RockspecConfig governs the sandboxed evaluator. +// +// - Env == nil → os.getenv pass-through to the host process env (matches +// upstream luarocks 1:1). +// - Env != nil → os.getenv returns Env[name] if present, else nil. +// - Empty non-nil map → os.getenv always returns nil. +type RockspecConfig struct { + Env map[string]string +} diff --git a/lib/luarocks/deps/constraint.go b/lib/luarocks/deps/constraint.go new file mode 100644 index 000000000..d33b0a3ea --- /dev/null +++ b/lib/luarocks/deps/constraint.go @@ -0,0 +1,162 @@ +package deps + +import ( + "fmt" + "regexp" + "strings" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// operatorAliases maps the surface forms accepted by upstream +// queries.lua:84-96 to their canonical operator strings used in +// rocks.VersionConstraint.Op. +var operatorAliases = map[string]string{ + "==": "==", + "~=": "~=", + ">": ">", + "<": "<", + ">=": ">=", + "<=": "<=", + "~>": "~>", + "": "==", + "=": "==", + "!=": "~=", +} + +// constraintRe matches one constraint segment "". The op group +// is greedy over `[<>=~!]*` so multi-character operators like ">=" win +// over a bare ">". Trailing whitespace + comma are stripped by the caller +// when splitting segments. +var constraintRe = regexp.MustCompile(`^\s*(@?)([<>=~!]*)\s*([0-9A-Za-z._-]+)\s*$`) + +// ParseConstraint parses one constraint segment (e.g. ">= 1.2.3", "~> 1.2", +// "1.0" with implicit "==") into a rocks.VersionConstraint. +// +// Whitespace is tolerated around the operator and version. An empty +// operator defaults to "==". An `@` prefix is accepted for upstream +// compatibility (no-upgrade marker) and silently dropped — the engine does +// not model no_upgrade since the facade does not perform automatic upgrades. +func ParseConstraint(s string) (rocks.VersionConstraint, error) { + m := constraintRe.FindStringSubmatch(s) + if m == nil { + return rocks.VersionConstraint{}, fmt.Errorf("deps: cannot parse constraint %q", s) + } + + op, ok := operatorAliases[m[2]] + if !ok { + return rocks.VersionConstraint{}, fmt.Errorf("deps: unknown constraint operator %q in %q", m[2], s) + } + + v, err := ParseVersion(m[3]) + if err != nil { + return rocks.VersionConstraint{}, fmt.Errorf("deps: constraint %q: %w", s, err) + } + + return rocks.VersionConstraint{Op: op, Version: v}, nil +} + +// ParseConstraints parses a full constraint expression (e.g. +// ">= 1.2.3, < 2.0") into the AND'd list of constraints upstream stores +// under queries[i].constraints. +// +// An empty input yields an empty (non-nil) slice — i.e. "no constraints, +// accept any version". +func ParseConstraints(s string) ([]rocks.VersionConstraint, error) { + out := []rocks.VersionConstraint{} + + for _, seg := range splitConstraints(s) { + if seg == "" { + continue + } + + c, err := ParseConstraint(seg) + if err != nil { + return nil, err + } + + out = append(out, c) + } + + return out, nil +} + +// splitConstraints splits on commas — operators do not contain commas, so +// this is unambiguous in the grammar. +func splitConstraints(s string) []string { + parts := strings.Split(s, ",") + for i := range parts { + parts[i] = strings.TrimSpace(parts[i]) + } + + return parts +} + +// Match reports whether v satisfies every constraint in cs. An empty +// constraint list always matches (matches upstream — no constraints means +// any version is acceptable). +func Match(v rocks.Version, cs []rocks.VersionConstraint) bool { + for _, c := range cs { + if !matchOne(v, c) { + return false + } + } + + return true +} + +func matchOne(v rocks.Version, c rocks.VersionConstraint) bool { + cv := c.Version + + cmp := Compare(v, cv) + + switch c.Op { + case "==": + return cmp == 0 + case "~=": + return cmp != 0 + case ">": + return cmp > 0 + case "<": + return cmp < 0 + case ">=": + return cmp >= 0 + case "<=": + return cmp <= 0 + case "~>": + return partialMatch(v, cv) + default: + // Unknown op — fail closed. The constructor rejects unknown + // ops, but if a hand-constructed VersionConstraint reaches us we'd + // rather refuse the match than silently allow it. + return false + } +} + +// partialMatch implements the `~>` pessimistic operator: every component +// of `requested` must match `version`. Trailing components of `version` +// beyond requested's length are wildcards. If requested has a non-zero +// revision, that revision must match exactly too. +// +// Examples: +// +// - `~> 1.2` matches 1.2, 1.2.1, 1.2.99; does NOT match 1.3. +// - `~> 1.2.3` matches 1.2.3, 1.2.3-1; does NOT match 1.2.4. +func partialMatch(version, requested rocks.Version) bool { + for i, ri := range requested.Components { + var vi int + if i < len(version.Components) { + vi = version.Components[i] + } + + if ri != vi { + return false + } + } + + if requested.Revision != 0 { + return version.Revision == requested.Revision + } + + return true +} diff --git a/lib/luarocks/deps/resolver.go b/lib/luarocks/deps/resolver.go new file mode 100644 index 000000000..4e549f2d0 --- /dev/null +++ b/lib/luarocks/deps/resolver.go @@ -0,0 +1,178 @@ +package deps + +import ( + "context" + "errors" + "fmt" + "sort" + "strings" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// providedRocks are dependency names that ship inside the Tarantool VM and +// therefore never need to be fetched. Matches upstream +// `util.get_rocks_provided` minimum set; only `lua` is universally +// pre-supplied for our purposes (Tarantool embeds LuaJIT 5.1). +var providedRocks = map[string]bool{ + "lua": true, + "luajit": true, + "luabitop": true, +} + +// Resolve performs a greedy depth-first walk over root.Dependencies, +// choosing the newest version of each dep that satisfies its constraints +// and recursing into the chosen rock's transitive dependencies before +// moving on to its siblings. +// +// The result is in topological order (deepest-dep first), suitable for +// passing to Rocks.Install in sequence. The root rock itself is NOT +// included in the result — callers install it separately after their +// chosen pre-requisites are in place. +// +// Conflicts (two branches demanding incompatible versions of the same +// dep) and cycles are reported as errors with the dependency chain +// included in the message. +// +// `lua` and other VM-provided names short-circuit: they are silently +// dropped from the install list. +func Resolve(ctx context.Context, root *rocks.Rockspec, idx rocks.RemoteIndex) ([]rocks.InstallStep, error) { + if root == nil { + return nil, errors.New("deps.Resolve: nil root rockspec") + } + + r := &resolver{ + idx: idx, + chosen: map[string]*rocks.InstallStep{}, + inFlight: map[string]bool{}, + } + + if err := r.walk(ctx, root.Package, root.Dependencies, nil); err != nil { + return nil, err + } + // Topo order: by recording when each name leaves the recursion (post- + // order), the resulting slice already has deepest deps first. r.order + // preserves that order. + out := make([]rocks.InstallStep, 0, len(r.order)) + for _, name := range r.order { + out = append(out, *r.chosen[name]) + } + + return out, nil +} + +type resolver struct { + idx rocks.RemoteIndex + chosen map[string]*rocks.InstallStep + inFlight map[string]bool + order []string // post-order — deepest first +} + +// walk visits every entry in deps. parent is included in cycle errors. +// chain tracks the dep-name stack for diagnostics. +func (r *resolver) walk(ctx context.Context, parent string, deps []rocks.Dep, chain []string) error { + for _, d := range deps { + if err := ctx.Err(); err != nil { + return err + } + + if providedRocks[d.Name] { + continue + } + + if r.inFlight[d.Name] { + return fmt.Errorf("deps.Resolve: cycle detected on %q in chain %v", d.Name, append(chain, d.Name)) + } + + if existing, ok := r.chosen[d.Name]; ok { + // Already chose a version for this name. Make sure the version + // also satisfies the current constraints; if not, that's a + // hard conflict. + if !Match(existing.Version, d.Constraints) { + return fmt.Errorf( + "deps.Resolve: conflict for %q: previously chose %s but %s requires %v", + d.Name, existing.Version.Raw, parent, formatConstraints(d.Constraints)) + } + + continue + } + + r.inFlight[d.Name] = true + + candidates, err := r.idx.Query(ctx, d.Name) + if err != nil { + return fmt.Errorf("deps.Resolve: query %q: %w", d.Name, err) + } + + picked, ok := pickNewest(candidates, d.Constraints) + if !ok { + return fmt.Errorf( + "deps.Resolve: no version of %q satisfies %s (parent=%s, %d candidates)", + d.Name, formatConstraints(d.Constraints), parent, len(candidates)) + } + + step := &rocks.InstallStep{ + Name: picked.Name, + Version: picked.Version, + URL: picked.URL, + Rockspec: picked.Spec, + } + r.chosen[d.Name] = step + + // Recurse into the picked version's deps. We only have transitive + // info if the index preloaded a *Rockspec; otherwise the caller + // must invoke Resolve again after fetching the rockspec (the + // facade does this; manifest-based indices typically don't preload). + if picked.Spec != nil { + child := append(append([]string{}, chain...), d.Name) + if err := r.walk(ctx, d.Name, picked.Spec.Dependencies, child); err != nil { + return err + } + } + + delete(r.inFlight, d.Name) + r.order = append(r.order, d.Name) + } + + return nil +} + +// pickNewest selects the highest version in candidates whose Version +// matches all of cs. +func pickNewest(candidates []rocks.VersionedRock, cs []rocks.VersionConstraint) (rocks.VersionedRock, bool) { + filtered := make([]rocks.VersionedRock, 0, len(candidates)) + + for _, c := range candidates { + if Match(c.Version, cs) { + filtered = append(filtered, c) + } + } + + if len(filtered) == 0 { + return rocks.VersionedRock{}, false + } + + sort.SliceStable(filtered, func(i, j int) bool { + return Compare(filtered[i].Version, filtered[j].Version) > 0 + }) + + return filtered[0], true +} + +func formatConstraints(cs []rocks.VersionConstraint) string { + if len(cs) == 0 { + return "(any)" + } + + var out strings.Builder + + for i, c := range cs { + if i > 0 { + out.WriteString(", ") + } + + out.WriteString(c.Op + " " + c.Version.Raw) + } + + return out.String() +} diff --git a/lib/luarocks/deps/types.go b/lib/luarocks/deps/types.go new file mode 100644 index 000000000..7d2a9b35d --- /dev/null +++ b/lib/luarocks/deps/types.go @@ -0,0 +1,9 @@ +package deps + +// Re-export note: the user-facing types VersionedRock, InstallStep and +// the RemoteIndex interface live in the root rocks package (interfaces.go) +// so that the rocks facade can refer to them without importing deps — +// keeping rocks → deps → rocks from forming an import cycle. +// +// This file remains as a stable home for any deps-only types we add in +// the future. diff --git a/lib/luarocks/deps/version.go b/lib/luarocks/deps/version.go new file mode 100644 index 000000000..8cf522049 --- /dev/null +++ b/lib/luarocks/deps/version.go @@ -0,0 +1,196 @@ +// Package deps implements version parsing, constraint matching and the +// transitive dependency resolver used by the Rocks facade. +// +// Upstream references: +// +// - luarocks/src/luarocks/core/vers.lua — Version parsing + comparator. +// - luarocks/src/luarocks/queries.lua — Constraint grammar. +// - luarocks/src/luarocks/search.lua — Remote search / pick-latest. +// +// The on-the-wire shapes (rocks.Version, rocks.VersionConstraint) live in +// the root rocks package so callers can talk about versions without +// depending on deps. +package deps + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// Numeric deltas applied to the version-component slot when a token is one +// of the recognized keywords (upstream core/vers.lua:9-17). `scm` and `dev` +// sort ABOVE numeric releases by virtue of these large positive deltas. +const ( + deltaDev = 120000000 + deltaSCM = 110000000 + deltaCVS = 100000000 + deltaRC = -1000 + deltaPre = -10000 + deltaBeta = -100000 + deltaAlpha = -1000000 + + // asciiFallbackDivisor scales the first byte of an unknown alpha token + // into a small delta (upstream's `token[0]/1000` ASCII fallback). + asciiFallbackDivisor = 1000 +) + +var keywordDeltas = map[string]int{ + "dev": deltaDev, + "scm": deltaSCM, + "cvs": deltaCVS, + "rc": deltaRC, + "pre": deltaPre, + "beta": deltaBeta, + "alpha": deltaAlpha, +} + +// digitsRe matches a leading numeric token; matches upstream's +// `^(%d+)[%.%-%_]*(.*)`. +var digitsRe = regexp.MustCompile(`^([0-9]+)[._-]*(.*)$`) + +// alphaRe matches a leading alpha token; matches upstream's +// `^(%a+)[%.%-%_]*(.*)`. +var alphaRe = regexp.MustCompile(`^([A-Za-z]+)[._-]*(.*)$`) + +// revisionRe matches the trailing `-N` revision suffix. +var revisionRe = regexp.MustCompile(`^(.*)-([0-9]+)$`) + +// ParseVersion parses a rock version string (e.g. "1.2.3-4", "scm-1", +// "dev-1") into a rocks.Version. +// +// The semantics match upstream `core/vers.parse_version`: +// +// - Strip surrounding whitespace. +// - Strip a trailing `-N` digit revision and store as Version.Revision. +// - Walk the remainder splitting on `.`, `-`, `_`. Numeric tokens become +// components; recognized keywords are mapped via keywordDeltas and +// contribute at the current slot; unknown alpha tokens fall back to +// `token[0]/1000` per upstream's ASCII fallback. +// - "scm" and "dev" set IsSCM / IsDev as user-facing booleans; the +// numeric Components encode the actual ordering. +func ParseVersion(s string) (rocks.Version, error) { + raw := s + + trimmed := strings.TrimSpace(s) + if trimmed == "" { + return rocks.Version{}, errors.New("deps: empty version string") + } + + v := rocks.Version{Raw: raw} + + // Trailing -N revision. + if m := revisionRe.FindStringSubmatch(trimmed); m != nil { + // Heuristic: keywords like "scm-1" have m[1] == "scm" — that's still + // the version body, with revision == 1. + rev, err := strconv.Atoi(m[2]) + if err != nil { + return rocks.Version{}, fmt.Errorf("deps: parse revision in %q: %w", s, err) + } + + trimmed = m[1] + v.Revision = rev + } + + cur := trimmed + for len(cur) > 0 { + if m := digitsRe.FindStringSubmatch(cur); m != nil { + n, err := strconv.Atoi(m[1]) + if err != nil { + return rocks.Version{}, fmt.Errorf("deps: parse %q in %q: %w", m[1], s, err) + } + + v.Components = append(v.Components, n) + cur = m[2] + + continue + } + + m := alphaRe.FindStringSubmatch(cur) + if m == nil { + return rocks.Version{}, fmt.Errorf("deps: cannot parse remainder %q of version %q", cur, s) + } + + tok := strings.ToLower(m[1]) + + delta, ok := keywordDeltas[tok] + if !ok { + // Upstream fallback: token's first byte divided by 1000. + // This is integer division in our model. + delta = int(tok[0]) / asciiFallbackDivisor + } + + switch tok { + case "scm": + v.IsSCM = true + case "dev": + v.IsDev = true + } + + // Upstream `add_token` adds the delta to the current slot when a + // component already exists; we emulate by appending if no slot + // has been opened yet, otherwise merging into the last slot. + if len(v.Components) == 0 { + v.Components = append(v.Components, delta) + } else { + v.Components[len(v.Components)-1] += delta + } + + cur = m[2] + } + + if len(v.Components) == 0 { + // All-keyword input ("scm") still needs a placeholder slot to + // participate in comparisons. Upstream would have appended the delta. + v.Components = append(v.Components, 0) + } + + return v, nil +} + +// Compare returns -1 if a < b, 0 if equal, 1 if a > b. +// +// Comparison is component-wise; missing trailing components on either side +// are treated as 0 (matches upstream __lt). When all components are equal the +// revision breaks the tie, compared UNCONDITIONALLY (a missing revision is 0) +// so callers always get a total order. This differs from upstream, which +// returns "equal" when one side lacks a revision — the Go API cannot tell +// "absent" from "0", so it always tie-breaks deterministically. +func Compare(a, b rocks.Version) int { + n := max(len(b.Components), len(a.Components)) + for i := range n { + var ai, bi int + if i < len(a.Components) { + ai = a.Components[i] + } + + if i < len(b.Components) { + bi = b.Components[i] + } + + if ai != bi { + if ai < bi { + return -1 + } + + return 1 + } + } + // All components equal — fall back to revision. We compare revisions + // unconditionally (treating missing as 0) so callers get a total order; + // upstream returns "equal" when one side lacks revision, but our Go + // API does not distinguish "absent" from "0". + if a.Revision != b.Revision { + if a.Revision < b.Revision { + return -1 + } + + return 1 + } + + return 0 +} diff --git a/lib/luarocks/errors.go b/lib/luarocks/errors.go new file mode 100644 index 000000000..5cefb8a21 --- /dev/null +++ b/lib/luarocks/errors.go @@ -0,0 +1,40 @@ +package rocks + +import "errors" + +// Sentinel errors callers can branch on with errors.Is. +// +// These replace the "deep cc/cmake failure or generic os.exit from +// the Lua side" pattern that tt currently string-matches on. Every facade +// method documents which of these it can return. +var ( + // ErrMissingTarantoolHeaders signals that a build backend needs the + // Tarantool development headers but Config.Tarantool.IncludeDir is + // unset or does not contain `tarantool/module.h`. + ErrMissingTarantoolHeaders = errors.New("rocks: tarantool headers not found") + + // ErrMissingTarantoolBinary signals that an operation requires the + // `tarantool` binary on disk (e.g. shelling out for capability probes) + // and Config.Tarantool.Executable is unset or does not point at an + // executable. + ErrMissingTarantoolBinary = errors.New("rocks: tarantool binary not found") + + // ErrUnsupportedCommand signals that the v1 Exec shim received a + // luarocks subcommand outside the v1 in-scope set (download, search, + // remove, purge, lint, new_version, write_rockspec, doc, test, config, + // upload, admin/*). + ErrUnsupportedCommand = errors.New("rocks: unsupported command") + + // ErrUnsupportedRockspecFeature signals that a rockspec used a feature + // the evaluator does not implement (e.g. build.type outside the + // {"builtin","cmake","make","command","none"} set, or a platforms + // block we don't recognize). Fail loud rather than silently skip. + ErrUnsupportedRockspecFeature = errors.New("rocks: unsupported rockspec feature") + + // ErrNotImplemented signals that the active backend (Engine) has no + // implementation for the invoked method. It is the SOLE error a method + // returns in that case — no silent no-op, no zero-value success. + // Callers branch on it with errors.Is(err, ErrNotImplemented) to + // detect an unsupported operation for the selected backend. + ErrNotImplemented = errors.New("rocks: method not implemented by this backend") +) diff --git a/lib/luarocks/fetch/dispatch.go b/lib/luarocks/fetch/dispatch.go new file mode 100644 index 000000000..95154467e --- /dev/null +++ b/lib/luarocks/fetch/dispatch.go @@ -0,0 +1,128 @@ +// Package fetch retrieves a rock's `source.url` into a working directory. +// +// The dispatcher selects a backend per URL scheme: +// +// http, https → http.go (net/http GET + unpack) +// git, git+http, git+https, git+ssh, +// git+file → git.go (go-git clone, no binary) +// file → file.go (copy local tree) +// +// Unknown schemes return ErrUnsupportedRockspecFeature wrapped with the +// scheme name. +// +// All backends honor ctx for cancellation at the network/transport level +// — the HTTP request and the go-git clone are ctx-bound. Note that +// local archive extraction after an HTTP fetch is not interrupted mid-unpack. +// None mutate process state — no os.Setenv, no os.Chdir. +package fetch + +import ( + "context" + "errors" + "fmt" + "net/url" + "strings" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// Options tunes a Fetch invocation. The zero value is the documented +// default: no insecure hosts, no extra User-Agent override, no source +// metadata. Pass via FetchWith. +type Options struct { + // InsecureServers lists URL hosts for which the http backend skips + // TLS certificate verification. Escape hatch for rocks.tarantool.org. + InsecureServers []string + + // UserAgent overrides the default User-Agent header sent by + // the http backend. + UserAgent string + + // Tag is the value of `source.tag` from the rockspec, passed to the + // git backend as `--branch `. Empty means default branch. + Tag string + + // Branch is the value of `source.branch` from the rockspec, passed + // to the git backend as `--branch `. Set Tag or Branch but + // not both; if both are set Tag wins. + Branch string +} + +// Backend is the per-scheme fetch implementation. The dispatch table +// registers one Backend per scheme group (http*, git*, file). +type Backend interface { + Fetch(ctx context.Context, rawURL, destDir string, opts Options) (string, error) +} + +// Fetch retrieves rawURL into destDir using the default options and +// returns the on-disk path to the unpacked working tree. +// +// Equivalent to FetchWith(ctx, rawURL, destDir, Options{}). +func Fetch(ctx context.Context, rawURL, destDir string) (string, error) { + return FetchWith(ctx, rawURL, destDir, Options{}) +} + +// FetchWith is the options-bearing form of Fetch. +func FetchWith(ctx context.Context, rawURL, destDir string, opts Options) (string, error) { + scheme, err := schemeOf(rawURL) + if err != nil { + return "", err + } + + b, err := backendFor(scheme) + if err != nil { + return "", err + } + + return b.Fetch(ctx, rawURL, destDir, opts) +} + +// schemeOf returns the lowercase URL scheme (the part before `://`). For +// `git+ssh://...` it returns `git+ssh`. Empty rawURL is an error. +func schemeOf(rawURL string) (string, error) { + if rawURL == "" { + return "", errors.New("fetch: empty URL") + } + // net/url.Parse rejects `git+ssh://...` in some Go versions; do a + // manual split to avoid that and to preserve case-insensitive match. + if i := strings.Index(rawURL, "://"); i > 0 { + return strings.ToLower(rawURL[:i]), nil + } + // Fall back to net/url for SCP-like forms (we don't claim to support + // those, but the error message stays consistent). + u, err := url.Parse(rawURL) + if err != nil { + return "", fmt.Errorf("fetch: parse %q: %w", rawURL, err) + } + + if u.Scheme == "" { + return "", fmt.Errorf("fetch: URL %q has no scheme", rawURL) + } + + return strings.ToLower(u.Scheme), nil +} + +// backendFor returns the registered Backend for the given scheme, or +// ErrUnsupportedRockspecFeature wrapped with the scheme. +// +//nolint:ireturn // dispatcher intentionally returns the Backend interface +func backendFor(scheme string) (Backend, error) { + if b, ok := backends[scheme]; ok { + return b, nil + } + + return nil, fmt.Errorf("fetch: scheme %q: %w", scheme, rocks.ErrUnsupportedRockspecFeature) +} + +// backends is the scheme → Backend dispatch table, fixed at compile time. +// Tests may override entries by saving and restoring the original. +var backends = map[string]Backend{ + "http": httpBackend{}, + "https": httpBackend{}, + "git": gitBackend{}, + "git+file": gitBackend{}, + "git+http": gitBackend{}, + "git+https": gitBackend{}, + "git+ssh": gitBackend{}, + "file": fileBackend{}, +} diff --git a/lib/luarocks/fetch/file.go b/lib/luarocks/fetch/file.go new file mode 100644 index 000000000..be019a900 --- /dev/null +++ b/lib/luarocks/fetch/file.go @@ -0,0 +1,117 @@ +package fetch + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +const ( + // dirPerm is the mode for directories created while copying a tree: + // owner rwx, group rx, no world access. + dirPerm = 0o750 + + // ownerSearchBit forces the owner-search (execute) bit on directories + // so we can descend into them even when the source mode omits it. + ownerSearchBit = 0o100 +) + +// fileBackend implements file:// fetches by copying the source tree into +// destDir. We always copy (rather than returning the on-disk path +// directly) so that build steps which scribble into the working tree +// don't corrupt the user's local source — matches upstream luarocks's +// `fetch.fetch_local`. +type fileBackend struct{} + +// Fetch strips the `file://` prefix, validates the source path, copies +// the tree into destDir, and returns the destination path. +func (fileBackend) Fetch(ctx context.Context, rawURL, destDir string, _ Options) (string, error) { + if err := ctx.Err(); err != nil { + return "", err + } + + src := strings.TrimPrefix(rawURL, "file://") + + st, err := os.Stat(src) + if err != nil { + return "", fmt.Errorf("fetch.file: stat %q: %w", src, err) + } + + if err := os.MkdirAll(destDir, dirPerm); err != nil { + return "", fmt.Errorf("fetch.file: mkdir %q: %w", destDir, err) + } + + if !st.IsDir() { + // Single file (e.g. a rockspec or archive). Copy into destDir + // under its basename and return destDir. + dst := filepath.Join(destDir, filepath.Base(src)) + + err := copyOneFile(src, dst, st.Mode().Perm()) + if err != nil { + return "", err + } + + return destDir, nil + } + // Copy tree rooted at src into destDir. + if err := copyDir(ctx, src, destDir); err != nil { + return "", fmt.Errorf("fetch.file: %w", err) + } + + return destDir, nil +} + +func copyDir(ctx context.Context, src, dst string) error { + return filepath.Walk(src, func(p string, info os.FileInfo, werr error) error { + if werr != nil { + return werr + } + + if err := ctx.Err(); err != nil { + return err + } + + rel, err := filepath.Rel(src, p) + if err != nil { + return err + } + + target := filepath.Join(dst, rel) + if info.IsDir() { + return os.MkdirAll(target, info.Mode().Perm()|ownerSearchBit) + } + + return copyOneFile(p, target, info.Mode().Perm()) + }) +} + +func copyOneFile(src, dst string, mode os.FileMode) error { + if err := os.MkdirAll(filepath.Dir(dst), dirPerm); err != nil { + return fmt.Errorf("mkdir %q: %w", filepath.Dir(dst), err) + } + + // src is a caller-supplied file:// path being copied into destDir; reading + // the user's own local source tree is the documented purpose of this backend. + in, err := os.Open(src) + if err != nil { + return fmt.Errorf("open %q: %w", src, err) + } + + defer func() { _ = in.Close() }() + + out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) + if err != nil { + return fmt.Errorf("create %q: %w", dst, err) + } + + if _, err := io.Copy(out, in); err != nil { + _ = out.Close() + + return fmt.Errorf("copy %q->%q: %w", src, dst, err) + } + + return out.Close() +} diff --git a/lib/luarocks/fetch/git.go b/lib/luarocks/fetch/git.go new file mode 100644 index 000000000..f61c5712c --- /dev/null +++ b/lib/luarocks/fetch/git.go @@ -0,0 +1,156 @@ +package fetch + +import ( + "context" + "fmt" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport/ssh" +) + +// gitBackend clones a git repository into destDir using the go-git library +// (no `git` binary required). The dispatcher routes every `git*` scheme here. +// +// Shallow vs full clone follows upstream luarocks: +// +// git, git+file → Depth 1 (local / direct git protocol) +// git+http, git+https, +// git+ssh → full clone (some servers reject shallow) +// +// The `git+` prefix is stripped before the URL is handed to go-git; bare +// `git` keeps the git:// protocol. Options.Tag / Options.Branch selects the +// reference to check out (a tag ref or a branch ref respectively). +type gitBackend struct{} + +func (gitBackend) Fetch(ctx context.Context, rawURL, destDir string, opts Options) (string, error) { + scheme, err := schemeOf(rawURL) + if err != nil { + return "", err + } + + if err := os.MkdirAll(destDir, dirPerm); err != nil { + return "", fmt.Errorf("fetch.git: mkdir %q: %w", destDir, err) + } + + cloneURL := stripGitPlus(rawURL) + repoDir := filepath.Join(destDir, repoNameFromURL(cloneURL)) + + co := &git.CloneOptions{URL: cloneURL} + + if shallowScheme(scheme) { + co.Depth = 1 + } + + if refOpt(opts) != "" { + // The caller distinguishes Tag from Branch, so we resolve the + // fully-qualified ref instead of relying on git's `--branch` + // auto-detection. SingleBranch keeps the clone to that one ref, + // matching `git clone --branch --single-branch`. + co.ReferenceName = refName(opts) + co.SingleBranch = true + } + + // go-git does not consult ssh-agent automatically; wire it up for the + // git+ssh scheme on a best-effort basis (anonymous clone otherwise). + if scheme == "git+ssh" { + if auth, authErr := sshAuth(cloneURL); authErr == nil { + co.Auth = auth + } + } + + // PlainCloneContext honors ctx for network/transport cancellation + // and creates repoDir itself; it mutates no process state. + if _, err := git.PlainCloneContext(ctx, repoDir, false, co); err != nil { + return "", fmt.Errorf("fetch.git: clone %q: %w", cloneURL, err) + } + + return repoDir, nil +} + +// refName resolves the go-git reference to check out from the rockspec +// options: a tag ref when Tag is set, otherwise a branch ref. Callers must +// check refOpt first — when both are empty this returns the branch form of +// the empty string, which is not a valid clone target. +func refName(opts Options) plumbing.ReferenceName { + if opts.Tag != "" { + return plumbing.NewTagReferenceName(opts.Tag) + } + + return plumbing.NewBranchReferenceName(opts.Branch) +} + +// sshAuth builds an ssh-agent auth method for a git+ssh clone. It fails if no +// agent is reachable, in which case the caller falls back to an +// unauthenticated clone. +func sshAuth(cloneURL string) (*ssh.PublicKeysCallback, error) { + return ssh.NewSSHAgentAuth(sshUser(cloneURL)) +} + +// sshUser extracts the remote user from a git+ssh URL, defaulting to "git" +// (the conventional account for git hosting) when the URL carries no userinfo. +func sshUser(cloneURL string) string { + if u, err := url.Parse(cloneURL); err == nil && u.User != nil && u.User.Username() != "" { + return u.User.Username() + } + + return "git" +} + +// stripGitPlus removes a leading `git+` from the URL scheme so the value +// is acceptable as a git remote. +func stripGitPlus(rawURL string) string { + if strings.HasPrefix(rawURL, "git+") { + return rawURL[len("git+"):] + } + + return rawURL +} + +// shallowScheme reports whether `--depth=1` is safe for this scheme. We +// follow upstream's conservatism: only bare `git` and `git+file`. +func shallowScheme(scheme string) bool { + return scheme == "git" || scheme == "git+file" +} + +// refOpt picks the git ref to checkout. Tag wins over Branch when both +// are set (caller bug, but defined behavior). +func refOpt(opts Options) string { + if opts.Tag != "" { + return opts.Tag + } + + return opts.Branch +} + +// repoNameFromURL extracts the directory name git would default to (the +// last path segment with a trailing `.git` stripped). Falls back to +// "repo" if extraction fails. +func repoNameFromURL(rawURL string) string { + // Try net/url first. + if u, err := url.Parse(rawURL); err == nil && u.Path != "" { + base := filepath.Base(u.Path) + + base = strings.TrimSuffix(base, ".git") + + if base != "" && base != "/" && base != "." { + return base + } + } + // SCP-form fallback: user@host:path/to/repo.git + if i := strings.LastIndex(rawURL, "/"); i >= 0 && i < len(rawURL)-1 { + base := rawURL[i+1:] + + base = strings.TrimSuffix(base, ".git") + + if base != "" { + return base + } + } + + return "repo" +} diff --git a/lib/luarocks/fetch/http.go b/lib/luarocks/fetch/http.go new file mode 100644 index 000000000..6ed1ef525 --- /dev/null +++ b/lib/luarocks/fetch/http.go @@ -0,0 +1,321 @@ +package fetch + +import ( + "archive/tar" + "archive/zip" + "compress/gzip" + "context" + "crypto/tls" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + "time" +) + +// defaultUserAgent is the User-Agent header sent by httpBackend when +// Options.UserAgent is empty. +const defaultUserAgent = "go-luarocks/0.1" + +const ( + // httpStatusErrThreshold is the first status code treated as an error; + // any response at or above it (4xx/5xx) fails the fetch. + httpStatusErrThreshold = 400 + + // httpClientTimeout bounds a single fetch end-to-end. + httpClientTimeout = 2 * time.Minute + + // maxRedirects caps the redirect chain followed by the HTTP client. + maxRedirects = 5 +) + +// httpBackend downloads rawURL via net/http and unpacks recognized +// archive formats (.zip, .tar.gz, .tgz, .tar) into destDir. Other content +// types are written to destDir/ and destDir is +// returned as-is. +type httpBackend struct{} + +// Fetch performs the GET, writes the body to a temp file under destDir, +// then unpacks if the URL or response content suggests an archive. +// +// TLS verification is enabled by default. If the host appears in +// opts.InsecureServers, a Transport with InsecureSkipVerify=true is used +// instead — opt-in for tarantool's rocks server which historically +// served over HTTP without TLS. +func (httpBackend) Fetch(ctx context.Context, rawURL, destDir string, opts Options) (string, error) { + u, err := url.Parse(rawURL) + if err != nil { + return "", fmt.Errorf("fetch.http: parse %q: %w", rawURL, err) + } + + if err := os.MkdirAll(destDir, dirPerm); err != nil { + return "", fmt.Errorf("fetch.http: mkdir %q: %w", destDir, err) + } + + client := buildHTTPClient(u.Host, opts) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) + if err != nil { + return "", fmt.Errorf("fetch.http: new request: %w", err) + } + + ua := opts.UserAgent + if ua == "" { + ua = defaultUserAgent + } + + req.Header.Set("User-Agent", ua) + + resp, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("fetch.http: GET %s: %w", rawURL, err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode >= httpStatusErrThreshold { + return "", fmt.Errorf("fetch.http: GET %s: status %d", rawURL, resp.StatusCode) + } + + base := filepath.Base(u.Path) + if base == "" || base == "/" || base == "." { + base = "download" + } + + tmp := filepath.Join(destDir, "."+base+".part") + + // tmp is built from destDir (caller-provided) plus a sanitized basename. + f, err := os.Create(tmp) + if err != nil { + return "", fmt.Errorf("fetch.http: create %q: %w", tmp, err) + } + + if _, err := io.Copy(f, resp.Body); err != nil { + _ = f.Close() + _ = os.Remove(tmp) + + return "", fmt.Errorf("fetch.http: copy body: %w", err) + } + + if err := f.Close(); err != nil { + _ = os.Remove(tmp) + + return "", fmt.Errorf("fetch.http: close: %w", err) + } + + dst := filepath.Join(destDir, base) + if err := os.Rename(tmp, dst); err != nil { + _ = os.Remove(tmp) + + return "", fmt.Errorf("fetch.http: rename: %w", err) + } + + lower := strings.ToLower(base) + + switch { + // `.rock`/`.src.rock` archives are zip files under a different + // extension — upstream luarocks unpacks them the same way (a .src.rock + // bundles the rockspec plus the source). Treat them like .zip so the + // rockspec/source inside become visible to the caller. + case strings.HasSuffix(lower, ".zip"), + strings.HasSuffix(lower, ".rock"): + err := unzip(dst, destDir) + if err != nil { + return "", fmt.Errorf("fetch.http: unzip: %w", err) + } + + _ = os.Remove(dst) + case strings.HasSuffix(lower, ".tar.gz"), + strings.HasSuffix(lower, ".tgz"): + err := untargz(dst, destDir) + if err != nil { + return "", fmt.Errorf("fetch.http: untar.gz: %w", err) + } + + _ = os.Remove(dst) + case strings.HasSuffix(lower, ".tar"): + err := untar(dst, destDir) + if err != nil { + return "", fmt.Errorf("fetch.http: untar: %w", err) + } + + _ = os.Remove(dst) + } + + return destDir, nil +} + +func buildHTTPClient(host string, opts Options) *http.Client { + insecure := false + + for _, h := range opts.InsecureServers { + if strings.EqualFold(h, host) { + insecure = true + + break + } + } + + tr := &http.Transport{} + if insecure { + tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + + return &http.Client{ + Transport: tr, + Timeout: httpClientTimeout, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if len(via) >= maxRedirects { + return fmt.Errorf("stopped after %d redirects", maxRedirects) + } + + return nil + }, + } +} + +func unzip(archive, dst string) error { + r, err := zip.OpenReader(archive) + if err != nil { + return err + } + + defer func() { _ = r.Close() }() + + for _, f := range r.File { + // target is validated against dst below to reject path traversal. + target := filepath.Join(dst, f.Name) + if !strings.HasPrefix(target, filepath.Clean(dst)+string(os.PathSeparator)) && target != filepath.Clean(dst) { + return fmt.Errorf("zip entry %q escapes dst", f.Name) + } + + if f.FileInfo().IsDir() { + err := os.MkdirAll(target, dirPerm) + if err != nil { + return err + } + + continue + } + + if err := os.MkdirAll(filepath.Dir(target), dirPerm); err != nil { + return err + } + + rc, err := f.Open() + if err != nil { + return err + } + + out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + _ = rc.Close() + + return err + } + + // Archive members are rocks sources of bounded size; copying the full + // entry is intended. A decompression-bomb limit would break valid rocks. + if _, err := io.Copy(out, rc); err != nil { + _ = rc.Close() + _ = out.Close() + + return err + } + + _ = rc.Close() + + if err := out.Close(); err != nil { + return err + } + } + + return nil +} + +func untargz(archive, dst string) error { + f, err := os.Open(archive) + if err != nil { + return err + } + + defer func() { _ = f.Close() }() + + gz, err := gzip.NewReader(f) + if err != nil { + return err + } + + defer func() { _ = gz.Close() }() + + return extractTar(tar.NewReader(gz), dst) +} + +func untar(archive, dst string) error { + f, err := os.Open(archive) + if err != nil { + return err + } + + defer func() { _ = f.Close() }() + + return extractTar(tar.NewReader(f), dst) +} + +func extractTar(tr *tar.Reader, dst string) error { + cleanDst := filepath.Clean(dst) + + for { + hdr, err := tr.Next() + if err == io.EOF { + return nil + } + + if err != nil { + return err + } + + // target is validated against cleanDst below to reject path traversal. + target := filepath.Join(dst, hdr.Name) + if !strings.HasPrefix(target, cleanDst+string(os.PathSeparator)) && target != cleanDst { + return fmt.Errorf("tar entry %q escapes dst", hdr.Name) + } + + switch hdr.Typeflag { + case tar.TypeDir: + // hdr.Mode is a tar permission word; the low bits are the only + // meaningful ones and always fit in a FileMode. + err := os.MkdirAll(target, os.FileMode(uint32(hdr.Mode))|ownerSearchBit) + if err != nil { + return err + } + case tar.TypeReg: + if err := os.MkdirAll(filepath.Dir(target), dirPerm); err != nil { + return err + } + + out, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(uint32(hdr.Mode))) + if err != nil { + return err + } + + if _, err := io.Copy(out, tr); err != nil { + _ = out.Close() + + return err + } + + if err := out.Close(); err != nil { + return err + } + case tar.TypeSymlink, tar.TypeLink: + // Skip links: rocks sources shouldn't need them in v1, and + // blindly creating symlinks crosses safety lines for archive + // extraction. + continue + } + } +} diff --git a/lib/luarocks/interfaces.go b/lib/luarocks/interfaces.go new file mode 100644 index 000000000..27e1f09fe --- /dev/null +++ b/lib/luarocks/interfaces.go @@ -0,0 +1,49 @@ +package rocks + +import "context" + +// Interfaces let the facade compose default implementations from the +// subsystem packages while tests inject fakes. No global state. + +// Fetcher pulls a source at url into destDir and returns the on-disk path +// to the unpacked working tree. +// +// The default implementation lives in package fetch and switches on URL +// scheme: http(s) via net/http, git* via the git binary, file:// directly. +type Fetcher interface { + Fetch(ctx context.Context, url, destDir string) (string, error) +} + +// Builder runs the build backend declared in spec.Build.Type against +// srcDir (the unpacked source) and writes the build output under destDir +// (`/share/tarantool/rocks///build`). +// +// The default implementation lives in package build and dispatches on +// spec.Build.Type ∈ {"builtin","cmake","make","command","none"}. +type Builder interface { + Build(ctx context.Context, spec *Rockspec, srcDir, destDir string) error +} + +// RemoteIndex queries a rock server (or aggregate of servers) for every +// known version of `name`. It is the seam the deps resolver uses to +// discover candidate versions to install. +// +// The default implementation is remote.HTTPRemoteIndex, constructed by +// client.New(cfg) from cfg.Servers. Tests inject fakes via this interface. +type RemoteIndex interface { + Query(ctx context.Context, name string) ([]VersionedRock, error) +} + +// ManifestStore reads and writes the on-disk manifest files in a tree. +// +// - ReadTree / WriteTree handle `/manifest` (the top-level index). +// - ReadRock / WriteRock handle `/share/tarantool/rocks///rock_manifest` +// (the per-rock file index). +// +// The default implementation is manif.FileStore. +type ManifestStore interface { + ReadTree(path string) (*Manifest, error) + WriteTree(path string, m *Manifest) error + ReadRock(path string) (*RockManifest, error) + WriteRock(path string, rm *RockManifest) error +} diff --git a/lib/luarocks/internal/luarocks/README.md b/lib/luarocks/internal/luarocks/README.md new file mode 100644 index 000000000..87a1a4e20 --- /dev/null +++ b/lib/luarocks/internal/luarocks/README.md @@ -0,0 +1,31 @@ +# Vendored LuaRocks + +`src/` holds the **Tarantool fork** of LuaRocks 3.9.2 +([tarantool/luarocks](https://github.com/tarantool/luarocks), branch +`luarocks-3.9.2-tarantool`), vendored via `git subtree` (do not edit it +directly). This is the same source `tt` ships (`tt/cli/rocks/third_party`), not +stock upstream — the fork registers `tarantool` as a provided dependency +(via `TT_CLI_TARANTOOL_VERSION`), reads the build/install layout from +`hardcoded.lua`, uses Tarantool's `digest` module for md5, and adds embedded-use +guards (`rawget(_G,'arg')`, `cfg.initialized`). Stock upstream lacks all of +these, so tarantool-dependent rockspecs fail there. Because the subtree placed +the repo root here, the Lua modules live under `src/src/luarocks/`. + +Bump with (track the fork's branch, not upstream tags): + +``` +git subtree pull --prefix=internal/luarocks/src \ + https://github.com/tarantool/luarocks.git luarocks-3.9.2-tarantool --squash +``` + +`extra/` holds the only files we control: two shims preloaded ahead of upstream +modules. + +- `wrapper.lua` — dispatch entry point (`exec(bin, ...)`), maps subcommands to + `luarocks.cmd.*` and runs `cmd.run_command`. +- `hardcoded.lua` — LuaRocks build/install settings. Reads `LUAROCKS_PREFIX`, + `LUA_BINDIR`, `LUA_INCDIR`, `TARANTOOL_DIR` via the lua engine's custom + `os.getenv` (backed by a Go map from `Config.Tarantool`), and the current + working dir via the Go-bound `glr_getwd()` global. + +The embedded FS is declared in `embed.go`. diff --git a/lib/luarocks/internal/luarocks/embed.go b/lib/luarocks/internal/luarocks/embed.go new file mode 100644 index 000000000..62c5f1ca9 --- /dev/null +++ b/lib/luarocks/internal/luarocks/embed.go @@ -0,0 +1,18 @@ +// Package luarocksembed exposes the vendored Tarantool LuaRocks fork (luarocks-3.9.2-tarantool) +// (under src/) and the extra shims (under extra/) as an embedded +// filesystem. The lua engine in client/lua.go preloads these modules into a +// gopher-lua VM. Do not edit src/ directly; it is the vendored Tarantool LuaRocks fork. +// Shim behavior lives in extra/. +package luarocksembed + +import "embed" + +//go:embed src extra +var FS embed.FS + +// ReadFile reads a file from the embedded FS by its slash-separated path +// relative to the internal/luarocks directory (e.g. "extra/wrapper.lua" or +// "src/src/luarocks/core/cfg.lua"). +func ReadFile(name string) ([]byte, error) { + return FS.ReadFile(name) +} diff --git a/cli/rocks/extra/hardcoded.lua b/lib/luarocks/internal/luarocks/extra/hardcoded.lua similarity index 69% rename from cli/rocks/extra/hardcoded.lua rename to lib/luarocks/internal/luarocks/extra/hardcoded.lua index 4bea07a38..697b70bd1 100644 --- a/cli/rocks/extra/hardcoded.lua +++ b/lib/luarocks/internal/luarocks/extra/hardcoded.lua @@ -1,33 +1,21 @@ -- This file contains LuaRocks hardcoded settings. +-- Adapted from tt/cli/rocks/extra/hardcoded.lua. Env var names align with +-- upstream LuaRocks conventions; the go-luarocks lua engine serves them via a +-- custom os.getenv backed by a Go map populated from Config.Tarantool (IV7). local function get_tarantool_path() - local path = os.getenv('TT_CLI_TARANTOOL_PATH') - if path ~= nil then - return path - end - - return "/usr/bin" + return os.getenv('LUA_BINDIR') or "/usr/bin" end local function get_tarantool_include_path() - local path = os.getenv('TT_CLI_TARANTOOL_INCLUDE') - if path ~= nil then - return path - end - - return "/usr/include/tarantool" + return os.getenv('LUA_INCDIR') or "/usr/include/tarantool" end local function get_tarantool_prefix_path() - local path = os.getenv('TT_CLI_TARANTOOL_PREFIX') - if path ~= nil then - return path - end - - return "/usr" + return os.getenv('LUAROCKS_PREFIX') or "/usr" end -local cwd = tt_getwd() +local cwd = glr_getwd() return { PREFIX = get_tarantool_prefix_path(), diff --git a/cli/rocks/extra/wrapper.lua b/lib/luarocks/internal/luarocks/extra/wrapper.lua similarity index 79% rename from cli/rocks/extra/wrapper.lua rename to lib/luarocks/internal/luarocks/extra/wrapper.lua index 3e6f381f8..4efe3a19e 100644 --- a/cli/rocks/extra/wrapper.lua +++ b/lib/luarocks/internal/luarocks/extra/wrapper.lua @@ -7,11 +7,11 @@ local function exec(bin, ...) -- Tweak help messages. if arg == "admin" then util.this_program = function(default) -- luacheck: no unused args - return bin .. " rocks admin" + return bin .. " admin" end else util.this_program = function(default) -- luacheck: no unused args - return bin .. " rocks" + return bin end end local cmd = require("luarocks.cmd") @@ -33,18 +33,12 @@ local function exec(bin, ...) return end - --[[ Disabled: path, upload, - -- init: luarocks init command generates a project, including local - dependency management. It also creates two wrapper scripts that can be used to run - lua & luarocks from inside the project. tt rocks init is unable to create correct luarocks - wrapper because luarocks script, that must be wrapped, is missing. - So, rocks init is disabled for now. - --]] local rocks_commands = { build = "luarocks.cmd.build", config = "luarocks.cmd.config", doc = "luarocks.cmd.doc", download = "luarocks.cmd.download", + init = "luarocks.cmd.init", install = "luarocks.cmd.install", lint = "luarocks.cmd.lint", list = "luarocks.cmd.list", @@ -52,12 +46,14 @@ local function exec(bin, ...) make_manifest = "luarocks.admin.cmd.make_manifest", new_version = "luarocks.cmd.new_version", pack = "luarocks.cmd.pack", + path = "luarocks.cmd.path", purge = "luarocks.cmd.purge", remove = "luarocks.cmd.remove", search = "luarocks.cmd.search", show = "luarocks.cmd.show", test = "luarocks.cmd.test", unpack = "luarocks.cmd.unpack", + upload = "luarocks.cmd.upload", which = "luarocks.cmd.which", write_rockspec = "luarocks.cmd.write_rockspec", } diff --git a/lib/luarocks/internal/luarocks/src/.busted b/lib/luarocks/internal/luarocks/src/.busted new file mode 100644 index 000000000..a8d40bef6 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/.busted @@ -0,0 +1,9 @@ +return { + default = { + output = "htest", + verbose = true, + ["exclude-pattern"] = "sum_spec", -- do not run spec files inside fixture + helper = "spec.util.test_env", + ["auto-insulate"] = false + } +} diff --git a/lib/luarocks/internal/luarocks/src/.codecov.yml b/lib/luarocks/internal/luarocks/src/.codecov.yml new file mode 100644 index 000000000..f3b713950 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/.codecov.yml @@ -0,0 +1,6 @@ +coverage: + notify: + gitter: + default: + url: "https://webhooks.gitter.im/e/c8ee7e8a380f711aea39" + threshold: 1% diff --git a/lib/luarocks/internal/luarocks/src/.editorconfig b/lib/luarocks/internal/luarocks/src/.editorconfig new file mode 100644 index 000000000..dc5519a73 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +charset = utf-8 + +[*.lua] +indent_style = space +indent_size = 3 + +[Makefile] +indent_style = tab + +[*.bat] +end_of_line = crlf \ No newline at end of file diff --git a/lib/luarocks/internal/luarocks/src/.github/ISSUE_TEMPLATE/bug_report.md b/lib/luarocks/internal/luarocks/src/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..b5d22c696 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bugs +about: Bugs and unintented behaviour + +--- + +### Please read the following before submitting: +- Please do NOT submit bug reports for questions. Ask questions in the [Gitter + chat-room](https://gitter.im/luarocks/luarocks). +- Please do NOT submit duplicate bug reports. Look through the [issue + tracker](https://github.com/luarocks/luarocks/issues) before submitting. + +### Please fill out the following: +- **Platform:** + - Windows/Linux/MacOS. + +- **LuaRocks version:** + - `luarocks --version` + +- **Configuration file:** + - Please try to produce with the default configuration. + - If you cannot reproduce with the default configuration, please try to find + the minimal configuration to reproduce. + - Upload the config to a pastebin such as gist.github.com. + +- **LuaRocks output from when the issue occurred:** + - Use the `--verbose` flag. + +- **Description:** + - The steps you took in plain English to reproduce the problem. diff --git a/lib/luarocks/internal/luarocks/src/.github/ISSUE_TEMPLATE/enhancement.md b/lib/luarocks/internal/luarocks/src/.github/ISSUE_TEMPLATE/enhancement.md new file mode 100644 index 000000000..da65922a1 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/.github/ISSUE_TEMPLATE/enhancement.md @@ -0,0 +1,11 @@ +--- +name: Feature request +about: New functionality +labels: 'feature request' + +--- + +### Please fill out the following: +- **Description:** + - Please describe in plain English what the desired enhancement is and + what the use case is. diff --git a/lib/luarocks/internal/luarocks/src/.github/workflows/luacheck.yml b/lib/luarocks/internal/luarocks/src/.github/workflows/luacheck.yml new file mode 100644 index 000000000..ffcb08fec --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/.github/workflows/luacheck.yml @@ -0,0 +1,13 @@ +name: Luacheck + +on: [ push, pull_request ] + +jobs: + + luacheck: + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Luacheck + uses: lunarmodules/luacheck@v1 diff --git a/lib/luarocks/internal/luarocks/src/.github/workflows/test.yml b/lib/luarocks/internal/luarocks/src/.github/workflows/test.yml new file mode 100644 index 000000000..12fa91969 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/.github/workflows/test.yml @@ -0,0 +1,84 @@ +name: test + +on: + push: + branches: main + pull_request: + branches: '*' + +jobs: + ############################################################################## + ShellLint: + runs-on: "ubuntu-latest" + + steps: + - uses: actions/checkout@master + + - name: Prep + run: | + sudo apt-get install -y shellcheck + + - name: Shellcheck + run: | + shellcheck ./configure + + ############################################################################## + TestMatrix: + strategy: + matrix: + lua-version: ["5.4", "luajit-2.1"] + os: ["ubuntu-latest", "macos-13"] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@master + + - uses: hishamhm/gh-actions-lua@master + with: + luaVersion: ${{ matrix.lua-version }} + + - uses: leafo/gh-actions-luarocks@v4.0.0 + + - name: Prep + run: | + luarocks install busted + luarocks install cluacov + luarocks install busted-htest + + - name: Unit Test + run: | + eval $(luarocks path) + busted -o htest --exclude-tags=git,integration --verbose -Xhelper "lua_dir=$(luarocks config variables.LUA_DIR),ci" + busted -o htest --exclude-tags=git,integration --verbose -Xhelper "lua_dir=$(luarocks config variables.LUA_DIR),ci,env=full" + + - name: Integration Test + run: | + eval $(luarocks path) + busted -o htest --exclude-tags=ssh,gpg,git,unit --verbose -Xhelper "lua_dir=$(luarocks config variables.LUA_DIR),ci" + busted -o htest --exclude-tags=ssh,gpg,git,unit --verbose -Xhelper "lua_dir=$(luarocks config variables.LUA_DIR),ci,env=full" + + - name: Coverage + if: startsWith(matrix.os, 'ubuntu') + run: | + eval $(luarocks path) + luacov -c testrun/luacov.config + curl -Os https://uploader.codecov.io/latest/$([ `uname -s` = "Linux" ] && echo "linux" || echo "macos")/codecov + chmod +x codecov + ( cd testrun/ && ../codecov ) + grep "Summary" -B1 -A1000 testrun/luacov.report.out + + ############################################################################## + SmokeTest: + runs-on: "ubuntu-latest" + steps: + - uses: actions/checkout@master + + - uses: hishamhm/gh-actions-lua@master + with: + luaVersion: "5.4" + + - name: Smoke Test + run: | + ./configure + ./makedist dev + ./smoke_test.sh luarocks-dev.tar.gz diff --git a/lib/luarocks/internal/luarocks/src/.gitignore b/lib/luarocks/internal/luarocks/src/.gitignore new file mode 100644 index 000000000..35ddbf429 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/.gitignore @@ -0,0 +1,21 @@ +/config.* +/src/luarocks/core/hardcoded.lua +/testrun/* +/lua_install +# Stuff that pops up during development but shouldn't be in the repo (helps clean up `git status`) +/build +/build-binary +/*.rock +/*.tar.gz +/*.zip +/src/*.* +/prefix-* +/dev_* +/gh-pages +/wiki +/lua +/lua_modules +/luarocks +/luarocks-admin +/.luarocks +/.luacheckcache diff --git a/lib/luarocks/internal/luarocks/src/.luacheckrc b/lib/luarocks/internal/luarocks/src/.luacheckrc new file mode 100644 index 000000000..98e05c23d --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/.luacheckrc @@ -0,0 +1,32 @@ +codes = true + +ignore = { + "6..", + "542", + "212", + "213", + "421/ok", + "421/err", + "421/errcode", + "411/ok", + "411/err", + "411/errcode", + "113/unpack", + "211/require", + "211/ok", + "211/err", + "211/errcode", + "431/ok", + "431/err", + "431/errcode", + "311/ok", + "311/err", + "311/errcode", + "143/table.unpack", +} + +include_files = { + "src/luarocks/**/*.lua" +} + +unused_secondaries = false diff --git a/lib/luarocks/internal/luarocks/src/CHANGELOG.md b/lib/luarocks/internal/luarocks/src/CHANGELOG.md new file mode 100644 index 000000000..2748fe661 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/CHANGELOG.md @@ -0,0 +1,567 @@ +## What's new in LuaRocks 3.9.1 + +* Fixed error message when Lua library is not found +* Fixed build of Windows binary +* A couple of minor feature additions: + * API: `loader.which` has a new mode for searching `package.path/cpath` + * Adds a new second argument, `where`, a string which indicates places + to search for the module. If `where` contains `"l"`, it will search + using the LuaRocks loader; if it contains `"p"`, it will look in the + filesystem using `package.path` and `package.cpath`. You can use both + at the same time. + * `--no-project` flag can be used to override `.luarocks` project directory + detection + +## What's new in LuaRocks 3.9.0 + +* `builtin` build mode now always respects CC, CFLAGS and LDFLAGS +* Check that lua.h version matches the desired Lua version +* Check that the version of the Lua C library matches the desired Lua version +* Fixed deployment of non-wrapped binaries +* Fixed crash when `--lua-version` option is malformed +* Fixed help message for `--pin` option +* Unix: use native methods and don't always rely on $USER to determine user +* Windows: use native CLI tooling more +* macOS: support .tbd extension when checking for libraries +* macOS: add XCode SDK path to search paths +* macOS: add best-effort heuristic for library search using Homebrew paths +* macOS: avoid quoting issues with LIBFLAG +* macOS: deployment target is now 11.0 on macOS 11+ +* added DragonFly BSD support +* LuaRocks test suite now runs on Lua 5.4 and LuaJIT +* Internal dependencies of standalone LuaRocks executable were bumped + +## What's new in LuaRocks 3.8.0 + +* Support GitHub's protocol security changes transparently. + * The raw git:// protocol will stop working on GitHub. LuaRocks already + supports git+https:// as an alternative, but to avoid having to update + every rockspec in the repository that uses git://github.com, which would + require a large coordinated effort, LuaRocks now auto-converts github.com + and www.github.com URLs that use git:// to git+https:// +* `luarocks test` has a new flag `--prepare` that checks, downloads and + installs the tool requirements and rockspec dependencies but does not + run the test suite for the rockspec being tested. +* Code tweaks so that LuaRocks can run on a Lua interpreter built without + the `debug` library. +* `luarocks upload` supports uploading pre-packaged `.src.rock` files. +* Configuration fixes for OpenBSD. +* Respect the existing value for the `variables.LUALIB` configuration + variable if given explicitly by the user in the config file, rather + than trying to override it with auto-detection. +* Windows fixes for setting file permissions: + * Revert the use of `Everyone` back to `*S-1-1-0` + * Quote the use of the `%USERNAME%` variable to support names with spaces + +## What's new in LuaRocks 3.7.0 + +* Improved connectivity resiliency + * LuaRocks can now use mirrors for downloading rocks even if downloading + the manifest from the main server succeeds. + In previous versions, LuaRocks would check whether to use a mirror in the first + download operation, when it fetches the manifest. Once the server + (luarocks.org or one of its default mirrors) was chosen, it would stick with + it for the rest of the command. + The resulting behavior was that if the manifest fails to load, it switches to + a mirror and continues from there. But if the manifest fetches ok and the then + actual rock download fails, it would give up, instead of trying that in a + mirror as well. + Now, it retries every download on a mirror whenever the base URL matches one + configured in cfg.rocks_servers. The original behavior was satisfactory if + there was complete downtime in the main server, but this new behavior should + make the CLI much more resilient with regard to any intermittent failures + happening on the main server. +* On Unix, it now respects environment variables $XDG_CACHE_HOME and $XDG_CONFIG_HOME + * This means the user's configuration typically resides in ~/.config/luarocks/ + as per the XDG standard + * The legacy path ~/.luarocks/ continues to be tested first, for backwards + compatibility +* Fixes check for the default Lua version set in the user's home configuration +* Fixes an issue on Windows where it would incorrectly revoke permissions + from the current user when installing + +## What's new in LuaRocks 3.6.0 + +* Adds a double-check step to verify that all files from a rock are installed +* Improve resilience of the manifest reader to deal with manifests + written with older versions of LuaRocks lower than 3.0 +* `luarocks pack` now checks that the directory inside the archive being packed + as a `.src.rock` actually exists, refusing to pack an invalid rock from + a badly configured rockspec. +* Fixes behavior of `luarocks pack` when the `url` entry of a rockspec + points to a bare file. +* Remove an entry from the manifest if the rock itself is already missing +* The `configure` script now checks that the version of `lua.h` + found matches that of the Lua interpreter detected or configured +* Fixes the renaming of scripts when multiple versions are installed +* Fixes availability check for `svn` for rockspecs using Subversion +* Fixes for running with an empty PATH environment variable +* Portability improvements: + * Windows: vcvarsall.bat output is now properly redirected to NUL + meaning that the output of `luarocks path` can be used in scripts + * Fixes autodetection for Cygwin + * Handles macOS versions greater than 10.10 + * Adds platform specific configurations for NetBSD + * Respects CC/CFLAGS/LDFLAGS on FreeBSD +* Luacheck now runs on the LuaRocks CI +* Distributed binaries are built using Lua 5.3 + +## What's new in LuaRocks 3.5.0 + +This is a small release: + +* Added support for MSYS2 and Mingw-w64 +* Reverted the change in MSVC environment variable set up script +* Fixes a bug where `--verbose` raised an exception with a nil argument +* Added proper error messages when lua.h is invalid + + +## What's new in LuaRocks 3.4.0 + +### Features + +* `luarocks make` now supports `--only-deps` +* `luarocks make` new flag: `--no-install`, which only performs + the compilation step +* `--deps-only` is now an alias for `--only-deps` (useful in case + you always kept getting it wrong, like me!) +* `luarocks build` and `luarocks make` now support using + `--pin` and `--only-deps` at the same time, to produce a lock + file of dependencies in use without installing the main package. +* `luarocks show` can now accept a substring of the rock's name, + like `list`. +* `luarocks config`: when running without system-wide permissions, + try storing the config locally by default. + Also, if setting both lua_dir and --lua-version explicitly, + auto-switch the default Lua version. +* `luarocks` with no arguments now prints more info about the + location of the Lua interpreter which is being used +* `luarocks new_version` now keeps the old URL if the MD5 doesn't + change. +* `DEPS_DIR` is now accepted as a generic variable for dependency + directories (e.g. `luarocks install foo DEPS_DIR=/usr/local`) +* Handle quoting of arguments at the application level, for + improved Windows support +* All-in-one binary bundles `dkjson`, so it runs `luarocks upload` + without requiring any additional dependencies. +* Tweaks for Terra compatibility + +### Fixes + +* win32: generate proper temp filename +* No longer assume that Lua 5.3 is built with compat libraries and + bundles `bit32` +* `luarocks show`: do not crash when rockspec description is empty +* When detecting the location of `lua.h`, check that its version + matches the version of Lua being used +* Fail gracefully when a third-party tool (wget, etc.) is missing +* Fix logic for disabling mirrors that return network errors +* Fix detection of Lua path based on arg variable +* Fix regression on dependency matching of luarocks.loader + + +## What's new in LuaRocks 3.3.1 + +This is a bugfix release: + +* Fix downgrades of rocks containing directories: stop it + from creating spurious 0-byte files where directories have been +* Fix error message when attempting to copy a file that is missing +* Detect OpenBSD-specific dependency paths + +## What's new in LuaRocks 3.3.0 + +### Features + +* **Dependency pinning** + * Adds a new flag called `--pin` which creates a `luarocks.lock` + when building a rock with `luarocks build` or `luarocks make`. + This lock file contains the exact version numbers of every + direct or indirect dependency of the rock (in other words, + it is the transitive closure of the dependencies.) + For `make`, the `luarocks.lock` file is created in the current + directory. + The lock file is also installed as part of the rock in + its metadata directory alongside its rockspec. + When using `--pin`, if a lock file already exists, it is + ignored and overwritten. + * When building a rock with `luarocks make`, if there is a + `luarocks.lock` file in the current directory, the exact + versions specified there will be used for resolving dependencies. + * When building a rock with `luarocks build`, if there is a + `luarocks.lock` file in root of its sources, the exact + versions specified there will be used for resolving dependencies. + * When installing a `.rock` file with `luarocks install`, if the + rock contains a `luarocks.lock` file (i.e., if its dependencies + were pinned with `--pin` when the rock was built), the exact + versions specified there will be used for resolving dependencies. +* Improved VM type detection to support moonjit +* git: Support for shallow recommendations +* Initial support for Windows on ARM +* Support for building 64-bit Windows all-in-one binary +* More filesystem debugging output when using `--verbose` (now it + reports operations even when using LuaFileSystem-backed implementations) +* `--no-manifest` flag for creating a package without updating the + manifest files +* `--no-doc` flag is now supported by `luarocks make` + +### Performance improvements + +* Speed up dependency checks +* Speed up installation and deletion when deploying files +* build: do not download sources when when building with `--only-deps` +* New flag `--check-lua-versions`: when a rock name is not found, only + checks for availability in other Lua versions if this flag is given + +### Fixes + +* safer rollback on installation failure +* config: fix `--unset` flag +* Fix command name invocations with dashes (e.g. `luarocks-admin make-manifest`) +* Fix fallback to PATH search when Lua interpreter is not configured +* Windows: support usernames with spaces +* Windows: fix generation of temporary filenames (#1058) +* Windows: force `.lib` over `.dll` extension when resolving `LUALIB` + +## What's new in LuaRocks 3.2.1 + +* fix installation of LuaRocks via rockspec (`make bootstrap` and +`luarocks install`): correct a problem in the initialization of the +luarocks.fs module and its interaction with the cfg module. +* fix luarocks build --pack-binary-rock --no-doc +* fix luarocks build --branch +* luarocks init: fix Lua wrapper for interactive mode +* fix compatibility issues with command add-ons loaded via +luarocks.cmd.external modules +* correct override of config values via CLI flags + +## What's new in LuaRocks 3.2.0 + +LuaRocks 3.2.0 now uses argument parsing based on argparse +instead of a homegrown parser. This was implemented by Paul +Ouellette as his Google Summer of Code project, mentored by +Daurnimator. + +Release highlights: + +* Bugfix: luarocks path does not change the order of pre-existing path +items when prepending or appending to path variables +* Bugfix: fix directory detection on the Mac +* When building with --force-config, LuaRocks now never uses the +"project" directory, but only the forced configuration +* Lua libdir is now only checked for commands/platforms that really +need to link Lua explicitly +* LuaJIT is now detected dynamically +* RaptorJIT is now detected as a LuaJIT variant +* Improvements in Lua autodetection at runtime +* luarocks new_version: new option --dir +* luarocks which: report modules found via package.path and +package.cpath as well +* install.bat: Improved detection for Visual Studio 2017 and higher +* Bundled LuaSec in all-in-one binary bumped to version 0.8.1 + +## What's new in LuaRocks 3.1.3 + +This is another bugfix release, that incldes a couple of fixes, +including better Lua detection, and fixes specific to MacOS and +FreeBSD. + +## What's new in LuaRocks 3.1.2 + +This is again a small fix release. + +## What's new in LuaRocks 3.1.1 + +This is a hotfix release fixing an issue that affected initialization +in some scenarios. + +## What's new in LuaRocks 3.1.0 + +### More powerful `luarocks config` + +The `luarocks config` command used to only list the current +configuration. It is now able to query and also _set_ individual +values, like `git config`. You can now do things such as: + + luarocks config variables.OPENSSL_DIR /usr/local/openssl + luarocks config lua_dir /usr/local + luarocks config lua_version 5.3 + +and it will rewrite your luarocks configuration to store that value +for later reuse. Note that setting `lua_version` will make that Lua +version the default for `luarocks` invocations (you can always +override on a per-call basis with `--lua-version`. + +You can specify the scope where you will apply the configuration +change: system-wide, to the user's home config (with --local), or +specifically to a project, if you run the command from within a +project directory initialized with `luarocks init`. + +### New `--global` flag + +Some users prefer that LuaRocks default to system-wide installations, +some users prefer to install everything to their home directory. The +`local_by_default` configuration file controls this preference: when +it is off, the `--local` file triggers user-specific. Before 3.1.0 +there was no convenient way to trigger system-wide installations when +`local_by_default` was set to true. LuaRocks 3.1.0 adds a `--global` +flag to this purpose. To enable local-by-default, you can now do: + + luarocks config local_by_default true + +### `luarocks make` can deal with patches + +A rockspec can include embedded patch files, which are applied when a +source rock is built. Now, when you run `luarocks make` on a source +tree unpacked with `luarocks unpack`, the patches will be applied as +well (and a hidden lockfile is created to avoid the patches to be +re-applied incorrectly). + +### Smarter defaults when working with projects + +When working on a project initialized with `luarocks init`, the +presence of a ./.luarocks/config-5.x.lua file will be enough to detect +the project-based workflow and have `luarocks` default to that 5.x +version. That means the `./luarocks` wrapper becomes less necessary; +the `luarocks` from your $PATH will deal with the project just fine, +git-style. + +### And more! + +There are also other improvements. LuaRocks uses the manifest cache a +bit more aggressively, resulting in increased performance. Also, it no +longer complains with a warning message if the home cache cannot be +created (it just uses a temporary dir instead). And of course, the +release includes multiple bugfixes. + +## What's new in LuaRocks 3.0.4 + +* Fork-free platform detection at startup +* Improved detection of the default rockspec in commands such as `luarocks test` +* Various minor bugfixes + +## What's new in LuaRocks 3.0.3 + +LuaRocks 3.0.3 is a minor bugfix release, fixing a regression in +luarocks.loader introduced in 3.0.2. + +## What's new in LuaRocks 3.0.2 + +* Improvements in luarocks init, new --reset flag +* write_rockspec: --lua-version renamed to --lua-versions +* Improved behavior in module autodetection +* Bugfixes in luarocks show +* Fix upgrade/downgrade when a single rock has clashing module +filenames (should fix the issue when downgrading luasec) +* Fix for autodetected external dependencies with non-alphabetic +characters (should fix the libstdc++ issue when installing xml) + + +## What's new in LuaRocks 3.0.1 + +* Numerous bugfixes including: + * Handle missing global `arg` + * Fix umask behavior + * Do not overwrite paths in format 5.x.y when cleaning up path +variables (#868) + * Do not detect files under lua_modules as part of your sources +when running `luarocks write_rockspec` + * Windows: do not hardcode MINGW in the all-in-one binary: instead +it properly detects when running from a Visual Studio Developer +Console and uses that compiler instead + * configure: --sysconfdir was fixed to its correct meaning: it now +defaults to /etc and not /etc/luarocks (`/luarocks` is appended to the +value of sysconfdir) + * configure: fixed --force-config +* Store Lua location in config file, so that a user can run `luarocks +init --lua-dir=/my/lua/location` and have that location remain active +for that project +* Various improvements to the Unix makefile, including $(DESTDIR) +support and an uninstall rule +* Autodetect FreeBSD-style include paths (/usr/include/lua5x/) + +## What's new in LuaRocks 3.0.0 + +- [New rockspec format](#new-rockspec-format) +- [New commands](#new-commands), including [luarocks init](https://github.com/luarocks/luarocks/wiki/Project:-LuaRocks-per-project-workflow) for per-project workflows +- [New flags](#new-flags), including `--lua-dir` and `--lua-version` for using multiple Lua installs with a single LuaRocks +- [New build system](#new-build-system) +- [General improvements](#general-improvements), including [namespaces](https://github.com/luarocks/luarocks/wiki/Namespaces) +- [User-visible changes](#user-visible-changes), including some **breaking changes** +- [Internal changes](#internal-changes) + +### New rockspec format + +**New rockspec format:** if you add `rockspec_format = "3.0"` to your rockspec, +you can use a number of new features. Note that these rockspecs will only work +with LuaRocks 3.0 and above, but older versions will detect that directive and +fail gracefully, giving the user a message telling them to upgrade. Rockspecs +without the `rockspec_format` directive are interpreted as having format 1.0 +(the same format from LuaRocks series 1.x and 2.x) and are still supported. + +The following features are only enabled if `rockspec_format = "3.0"` is set in +the rockspec: + +* Build type `builtin` is the default if `build.type` is not specified. +* The `builtin` type auto-detects modules using the same heuristics as + `write_rockspec` (for example, if you have a `src` directory). With + auto-detection of the build type and modules, many rockspecs don't + even need an explicit `build` table anymore. +* New table `build_dependencies`: dependencies used only for running + `luarocks build` but not when installing binary rocks. +* New table `test_dependencies`: dependencies used only for running `luarocks test` +* New table `test`: settings for configuring the behavior of `luarocks test`. + Supports a `test.type` field so that the test backend can be specified. + Currently supported test backends are: + * `"busted"`, for running [Busted](https://olivinelabs.com/busted) + * `"command"`, for running a plain command. + * Custom backends can be loaded via `test_dependencies` +* New field `build.macosx_deployment_target = "10.9"` is supported in Mac platforms, + and adjusts `$(CC)` and `$(LD)` variables to export the corresponding + environment variable. +* LuaJIT can be detected in dependencies and uses version reported by the + running interpreter: e.g. `"luajit >= 2.1"`. +* Auto-detection of `source.dir` is improved: when the tarball contains + only one directory at the root, assume that is where the sources are. +* New `description` fields: + * `labels`, an array of strings; + * `issues_url`, URL to the project's bug tracker. +* `cmake` build type now supports `build.build_pass` and `build_install_pass` + to disable `make` passes. +* `git` fetch type fetches submodules by default. +* Patches added in `patches` can create and delete files, following standard + patch rules. + +### New commands + +* **New command:** `luarocks init`. This command performs the setup for using + LuaRocks in a "project directory": + * it creates a `lua_modules` directory in the current directory for + storing rocks + * it creates a `.luarocks/config-5.x.lua` local configuration file + * it creates `lua` and `luarocks` wrapper scripts in the current + directory that are configured to use `lua_modules` and + `.luarocks/config-5.x.lua` + * if there are no rockspecs in the current directory, it creates one + based on the directory name and contents. +* **New command:** `luarocks test`. It runs a rock's test suite, as specified + in the new `test` section of the rockspec file. It also does some + autodetection, so it already works with many existing rocks as well. +* **New command:** `luarocks which`. Given the name of an installed, it tells + you which rock it is a part of. For example, `luarocks which lfs` + will tell you it is a part of `luafilesystem` (and give the full + path name to the module). In this sense, `luarocks which` is the + dual command to `luarocks show`. + +### New flags + +* **New flags** `--lua-dir` and `--lua-version` which can be used with + all commands. This allows you to specify a Lua version and installation + prefix at runtime, so a single LuaRocks installation can be used + to manage packages for any Lua version. It is no longer necessary to + install separate copies of LuaRocks to manage packages for Lua 5.x + and 5.y. +* **New flags** added to `luarocks show`: `--porcelain`, giving a stable + script-friendly output (named after the Git `--porcelain` flag that + serves the same purpose) and `--rock-license`. +* **New flag** `--temp-key` for `luarocks upload`, allowing you to easily + upload rocks into an alternate account without disrupting the + stored configuration of your main account. +* **New flag** `--dev`, for enabling development-branch sub-repositories. + This adds support for easily requesting `dev` modules from LuaRocks.org, as in: + `luarocks install --dev luafilesystem`. The list of URLs configured + in `rocks_servers` is prepended with a list containing "/dev" in their paths. +* `luarocks config`, when called with no arguments, now displays your + entire active configuration, using the same Lua syntax as the configuration + file. It is sensitive to the flags given to it (`--tree`, `--lua-dir`, etc.) + so it presents the resulting configuration produced by loading the + currently-active configuration files and the given flags. + +### New build system + +**New build system**: the `configure` and `Makefile` scripts were completely +overhauled, making use of LuaRocks 3 features to greatly simplify them: + +* Much of the detection and configuration work they performed were moved + to runtime, to make LuaRocks more dynamic and resilient to environment + changes +* The system-package-manager-friendly mode is still available, as the + default target (`make`, formerly `make build`). +* The LuaRocks-as-a-rock mode (`make bootstrap`) is also still available, + and was greatly simplified: it no longer uses custom Makefiles: + LuaRocks installs itself using `luarocks make`, and its own rockspec + uses the `builtin` build mode. +* A new build mode: `make binary` compiles all of LuaRocks into a single + executable, bundling various Lua modules to make it self-sufficient, + such as LuaFileSystem, LuaSocket and LuaSec. + * For version 3.0, this will remain as an option, as we evaluate + its suitability moving forward to become the default mode of + distribution. + * The goal is to eventually use this mode to produce the Windows + version of LuaRocks. We currently include an experimental + `make windows-binary` target which builds a Windows version + using the MinGW-w64 cross-compiler on Linux. + +### General improvements + +* **New feature:** [namespaces](https://github.com/luarocks/luarocks/wiki/Namespaces): + you can use `luarocks install user/package` to install a package from a + specific user of the repository. +* Improved defaults for finding external libraries on Linux and Windows. +* Detection of the Lua library and header directories is now done at runtime. + This uses the same machinery that LuaRocks employs for `external_dependencies` + in general (with some added logic to cope with the unfortunate + rampant inconsistency in naming of Lua libraries and header paths + due to lack of upstream standardization). +* `luarocks-admin add` now works with `file://` repositories +* some UI improvements in `luarocks list` and `luarocks search`. +* Preliminary support for the upcoming Lua 5.4: LuaRocks is written in + the common dialect supporting Lua 5.1-5.3 and LuaJIT, but since a + single installation can manage packages for any Lua version now, + it can already manage packages for Lua 5.4 even though that's not + out yet. + +### User-visible changes + +* **Breaking change:** The support for deprecated unversioned paths + (e.g. `/usr/local/lib/luarocks/rocks/` and `/etc/luarocks/config.lua`) + was removed, LuaRocks will now only create and use paths versioned + to the specific Lua version in use + (e.g. `/usr/local/lib/luarocks/rocks-5.3/` and `/etc/luarocks/config-5.3.lua`). +* **Breaking changes:** `luarocks path` now exports versioned variables + `LUA_PATH_5_x` and `LUA_CPATH_5_x` instead of `LUA_PATH` and `LUA_CPATH` + when those are in use in your system. +* Package paths are sanitized to only reference the current Lua version. + For example, if you have `/some/dir/lua/5.1/` in your `$LUA_PATH` and + you are running Lua 5.2, `luarocks.loader` and the `luarocks` command-line + tool will convert it to `/some/dir/lua/5.2/`. +* LuaRocks now uses `dev` instead of `scm` as the favored version identifier + to describe development versions of a rock, aligning it with the terminology + used in https://luarocks.org. It still understands `scm` as a + compatibility fallback. +* LuaRocks no longer conflates modules `foo` and `foo.init` as being the + same in its internal manifest. Instead, the `luarocks.loader` module + is adapted to handle the `.init` case. +* Wrappers installed using `--tree` now prepend the tree's prefix to their + package paths. +* `luarocks-admin` commands no longer creates an `index.html` file in the + repository by default (it does update it if it already exists) + +### Internal changes + +* Major improvements in the test suite done by @georgeroman as part of the ongoing + Google Summer of Code 2018 program. The coverage improvements and test suite + speed-ups have been essential in getting the sprint towards LuaRocks 3.0 more + efficient and reliable! +* Modules needed by `luarocks.loader` were moved below the `luarocks.core` namespace. + Modules in `luarocks.core` only depend on other `luarocks.core` modules. + (Notably, `luarocks.core` does not use `luarocks.fs`.) +* Modules representing `luarocks` commands were moved into the `luarocks.cmd` namespace, + and `luarocks.command_line` was renamed to `luarocks.cmd`. Eventually, all CLI-related + code will live under `luarocks.cmd`, as we move towards a clean CLI-API separation, + in preparation for a stable public API. +* Likewise, modules representing `luarocks-admin` commands were moved into the + `luarocks.admin.cmd` namespace. +* New internal objects for representing interaction with the repostories: + `luarocks.queries` and `luarocks.results` +* Type checking rules of file formats were moved into the `luarocks.type` namespace. diff --git a/lib/luarocks/internal/luarocks/src/CODE_OF_CONDUCT.md b/lib/luarocks/internal/luarocks/src/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..ba7483965 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# LuaRocks Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers of LuaRocks pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at hisham@gobolinux.org. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/lib/luarocks/internal/luarocks/src/COPYING b/lib/luarocks/internal/luarocks/src/COPYING new file mode 100644 index 000000000..1d1ed74d2 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/COPYING @@ -0,0 +1,20 @@ +Copyright 2007-2011, Kepler Project. +Copyright 2011-2022, the LuaRocks project authors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/lib/luarocks/internal/luarocks/src/GNUmakefile b/lib/luarocks/internal/luarocks/src/GNUmakefile new file mode 100644 index 000000000..cbb47996e --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/GNUmakefile @@ -0,0 +1,188 @@ + +-include config.unix + +datarootdir = $(prefix)/share +bindir = $(prefix)/bin +INSTALL = install +INSTALL_DATA = $(INSTALL) -m 644 +BINARY_PLATFORM = unix + +SHEBANG = \#!$(LUA_BINDIR)/$(LUA_INTERPRETER) +LUA = $(LUA_BINDIR)/$(LUA_INTERPRETER) +luarocksconfdir = $(sysconfdir)/luarocks +luadir = $(datarootdir)/lua/$(LUA_VERSION) +builddir = ./build +buildbinarydir = ./build-binary + +LUAROCKS_FILES = $(shell find src/luarocks/ -type f -name '*.lua') + +LUA_ENV_VARS = LUA_PATH LUA_PATH_5_2 LUA_PATH_5_3 LUA_PATH_5_4 LUA_CPATH LUA_CPATH_5_2 LUA_CPATH_5_3 LUA_CPATH_5_4 + +all: build + +# ---------------------------------------- +# Base build +# ---------------------------------------- + +build: luarocks luarocks-admin $(builddir)/luarocks $(builddir)/luarocks-admin + +config.unix: + @echo Please run the "./configure" script before building. + @echo + @exit 1 + +$(builddir)/config-$(LUA_VERSION).lua: config.unix + mkdir -p "$(@D)" + @printf -- '-- LuaRocks configuration\n\n'\ + 'rocks_trees = {\n'\ + ' { name = "user", root = home .. "/.luarocks" };\n'\ + "$$([ "$(rocks_tree)" != "$(HOME)/.luarocks" ] && printf ' { name = "system", root = "'"$(rocks_tree)"'" };\\n')"\ + '}\n'\ + "$$([ -n "$(LUA_INTERPRETER)" ] && printf 'lua_interpreter = "%s";\\n' "$(LUA_INTERPRETER)")"\ + 'variables = {\n'\ + "$$([ -n "$(LUA_DIR)" ] && printf ' LUA_DIR = "%s";\\n' "$(LUA_DIR)")"\ + "$$([ -n "$(LUA_INCDIR)" ] && printf ' LUA_INCDIR = "%s";\\n' "$(LUA_INCDIR)")"\ + "$$([ -n "$(LUA_BINDIR)" ] && printf ' LUA_BINDIR = "%s";\\n' "$(LUA_BINDIR)")"\ + "$$([ -n "$(LUA_LIBDIR)" ] && printf ' LUA_LIBDIR = "%s";\\n' "$(LUA_LIBDIR)")"\ + '}\n'\ + > $@ + +luarocks: config.unix $(builddir)/config-$(LUA_VERSION).lua + mkdir -p .luarocks + cp $(builddir)/config-$(LUA_VERSION).lua .luarocks/config-$(LUA_VERSION).lua + rm -f src/luarocks/core/hardcoded.lua + echo "#!/bin/sh" > luarocks + echo "unset $(LUA_ENV_VARS)" >> luarocks + echo 'LUAROCKS_SYSCONFDIR="$(luarocksconfdir)" LUA_PATH="$(CURDIR)/src/?.lua;;" exec "$(LUA)" "$(CURDIR)/src/bin/luarocks" --project-tree="$(CURDIR)/lua_modules" "$$@"' >> luarocks + chmod +rx ./luarocks + ./luarocks init + +luarocks-admin: config.unix + rm -f src/luarocks/core/hardcoded.lua + echo "#!/bin/sh" > luarocks-admin + echo "unset $(LUA_ENV_VARS)" >> luarocks-admin + echo 'LUAROCKS_SYSCONFDIR="$(luarocksconfdir)" LUA_PATH="$(CURDIR)/src/?.lua;;" exec "$(LUA)" "$(CURDIR)/src/bin/luarocks-admin" --project-tree="$(CURDIR)/lua_modules" "$$@"' >> luarocks-admin + chmod +rx ./luarocks-admin + +$(builddir)/luarocks: src/bin/luarocks config.unix + mkdir -p "$(@D)" + (printf '$(SHEBANG)\n'\ + 'package.loaded["luarocks.core.hardcoded"] = { '\ + "$$([ -n "$(FORCE_CONFIG)" ] && printf 'FORCE_CONFIG = true, ')"\ + 'SYSCONFDIR = [[$(luarocksconfdir)]] }\n'\ + 'package.path=[[$(luadir)/?.lua;]] .. package.path\n'\ + 'local list = package.searchers or package.loaders; table.insert(list, 1, function(name) if name:match("^luarocks%%.") then return loadfile([[$(luadir)/]] .. name:gsub([[%%.]], [[/]]) .. [[.lua]]) end end)\n'; \ + tail -n +2 src/bin/luarocks \ + )> "$@" + +$(builddir)/luarocks-admin: src/bin/luarocks-admin config.unix + mkdir -p "$(@D)" + (printf '$(SHEBANG)\n'\ + 'package.loaded["luarocks.core.hardcoded"] = { '\ + "$$([ -n "$(FORCE_CONFIG)" ] && printf 'FORCE_CONFIG = true, ')"\ + 'SYSCONFDIR = [[$(luarocksconfdir)]] }\n'\ + 'package.path=[[$(luadir)/?.lua;]] .. package.path\n'\ + 'local list = package.searchers or package.loaders; table.insert(list, 1, function(name) if name:match("^luarocks%%.") then return loadfile([[$(luadir)/]] .. name:gsub([[%%.]], [[/]]) .. [[.lua]]) end end)\n'; \ + tail -n +2 src/bin/luarocks-admin \ + )> "$@" + +# ---------------------------------------- +# Base build +# ---------------------------------------- + +binary: luarocks $(buildbinarydir)/luarocks.exe $(buildbinarydir)/luarocks-admin.exe + +$(buildbinarydir)/luarocks.exe: src/bin/luarocks $(LUAROCKS_FILES) + (unset $(LUA_ENV_VARS); \ + "$(LUA)" binary/all_in_one "$<" "$(LUA_DIR)" "^src/luarocks/admin/" "$(luarocksconfdir)" "$(@D)" "$(FORCE_CONFIG)" $(BINARY_PLATFORM) $(CC) $(NM) $(BINARY_SYSROOT)) + +$(buildbinarydir)/luarocks-admin.exe: src/bin/luarocks-admin $(LUAROCKS_FILES) + (unset $(LUA_ENV_VARS); \ + "$(LUA)" binary/all_in_one "$<" "$(LUA_DIR)" "^src/luarocks/cmd/" "$(luarocksconfdir)" "$(@D)" "$(FORCE_CONFIG)" $(BINARY_PLATFORM) $(CC) $(NM) $(BINARY_SYSROOT)) + +# ---------------------------------------- +# Regular install +# ---------------------------------------- + +INSTALL_FILES = $(DESTDIR)$(bindir)/luarocks \ + $(DESTDIR)$(bindir)/luarocks-admin \ + $(DESTDIR)$(luarocksconfdir)/config-$(LUA_VERSION).lua \ + $(patsubst src/%, $(DESTDIR)$(luadir)/%, $(LUAROCKS_FILES)) + +install: $(INSTALL_FILES) + +install-config: $(DESTDIR)$(luarocksconfdir)/config-$(LUA_VERSION).lua + +$(DESTDIR)$(bindir)/luarocks: $(builddir)/luarocks + mkdir -p "$(@D)" + $(INSTALL) "$<" "$@" + +$(DESTDIR)$(bindir)/luarocks-admin: $(builddir)/luarocks-admin + mkdir -p "$(@D)" + $(INSTALL) "$<" "$@" + +$(DESTDIR)$(luadir)/luarocks/%.lua: src/luarocks/%.lua + mkdir -p "$(@D)" + $(INSTALL_DATA) "$<" "$@" + +$(DESTDIR)$(luarocksconfdir)/config-$(LUA_VERSION).lua: $(builddir)/config-$(LUA_VERSION).lua + mkdir -p "$(@D)" + $(INSTALL_DATA) "$<" "$@" + +uninstall: + rm -rf $(INSTALL_FILES) + +# ---------------------------------------- +# Binary install +# ---------------------------------------- + +LUAROCKS_CORE_FILES = $(wildcard src/luarocks/core/* src/luarocks/loader.lua) +INSTALL_BINARY_FILES = $(patsubst src/%, $(DESTDIR)$(luadir)/%, $(LUAROCKS_CORE_FILES)) \ + $(DESTDIR)$(luarocksconfdir)/config-$(LUA_VERSION).lua + +install-binary: $(INSTALL_BINARY_FILES) + mkdir -p "$(buildbinarydir)" + $(INSTALL) "$(buildbinarydir)/luarocks.exe" "$(DESTDIR)$(bindir)/luarocks" + $(INSTALL) "$(buildbinarydir)/luarocks-admin.exe" "$(DESTDIR)$(bindir)/luarocks-admin" + +# ---------------------------------------- +# Bootstrap install +# ---------------------------------------- + +bootstrap: luarocks $(DESTDIR)$(luarocksconfdir)/config-$(LUA_VERSION).lua + ./luarocks make --tree="$(DESTDIR)$(rocks_tree)" + +# ---------------------------------------- +# Windows binary build +# ---------------------------------------- + +windows-binary: windows-binary-32 windows-binary-64 + +windows-clean: windows-clean-32 windows-clean-64 + +windows-binary-32: luarocks + $(MAKE) -f binary/Makefile.windows windows-binary MINGW_PREFIX=i686-w64-mingw32 OPENSSL_PLATFORM=mingw + +windows-clean-32: + $(MAKE) -f binary/Makefile.windows windows-clean MINGW_PREFIX=i686-w64-mingw32 OPENSSL_PLATFORM=mingw + +windows-binary-64: luarocks + $(MAKE) -f binary/Makefile.windows windows-binary MINGW_PREFIX=x86_64-w64-mingw32 OPENSSL_PLATFORM=mingw64 + +windows-clean-64: + $(MAKE) -f binary/Makefile.windows windows-clean MINGW_PREFIX=x86_64-w64-mingw32 OPENSSL_PLATFORM=mingw64 + +# ---------------------------------------- +# Clean +# ---------------------------------------- + +clean: windows-clean + rm -rf ./config.unix \ + ./luarocks \ + ./luarocks-admin \ + $(builddir)/ \ + $(buildbinarydir)/ \ + ./.luarocks \ + ./lua_modules + +.PHONY: all build install binary install-binary bootstrap clean windows-binary windows-clean diff --git a/lib/luarocks/internal/luarocks/src/Makefile b/lib/luarocks/internal/luarocks/src/Makefile new file mode 100644 index 000000000..d0f0d4723 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/Makefile @@ -0,0 +1,7 @@ +.POSIX: + +all: + +gmake -f GNUmakefile all + +.DEFAULT: + +gmake -f GNUmakefile $< diff --git a/lib/luarocks/internal/luarocks/src/README.md b/lib/luarocks/internal/luarocks/src/README.md new file mode 100644 index 000000000..4a748224c --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/README.md @@ -0,0 +1,33 @@ + + +A package manager for Lua modules. + +[![Build Status](https://github.com/luarocks/luarocks/actions/workflows/test.yml/badge.svg)](https://github.com/luarocks/luarocks/actions) +[![Luacheck](https://github.com/luarocks/luarocks/actions/workflows/luacheck.yml/badge.svg)](https://github.com/luarocks/luarocks/actions/workflows/luacheck.yml) +[![Build Status](https://ci.appveyor.com/api/projects/status/4x4630tcf64da48i/branch/master?svg=true)](https://ci.appveyor.com/project/hishamhm/luarocks/branch/master) +[![Coverage Status](https://codecov.io/gh/luarocks/luarocks/coverage.svg?branch=master)](https://codecov.io/gh/luarocks/luarocks/branch/master) +[![Join the chat at https://gitter.im/luarocks/luarocks](https://badges.gitter.im/luarocks/luarocks.svg)](https://gitter.im/luarocks/luarocks) + +Main website: [luarocks.org](http://www.luarocks.org) + +It allows you to install Lua modules as self-contained packages called +[*rocks*][1], which also contain version [dependency][2] information. This +information can be used both during installation, so that when one rock is +requested all rocks it depends on are installed as well, and also optionally +at run time, so that when a module is required, the correct version is loaded. +LuaRocks supports both local and [remote][3] repositories, and multiple local +rocks trees. + +## Installing + +* [Installation instructions for Unix](http://luarocks.org/en/Installation_instructions_for_Unix) (Linux, BSDs, etc.) +* [Installation instructions for macOS](http://luarocks.org/en/Installation_instructions_for_macOS) +* [Installation instructions for Windows](http://luarocks.org/en/Installation_instructions_for_Windows) + +## License + +LuaRocks is free software and uses the [MIT license](http://luarocks.org/en/License), the same as Lua 5.x. + +[1]: http://luarocks.org/en/Types_of_rocks +[2]: http://luarocks.org/en/Dependencies +[3]: http://luarocks.org/en/Rocks_repositories diff --git a/lib/luarocks/internal/luarocks/src/SECURITY.md b/lib/luarocks/internal/luarocks/src/SECURITY.md new file mode 100644 index 000000000..de2b983a4 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +The LuaRocks project supports the _latest version_ of the tool +for bugfixes and security updates. In other words, if an +issue is reported and we produce a fix, it will appear in a subsequent +patch version (x.y.Z) of the tool, but we do not backport fixes +to previous minor (x.Y.z) or major (X.y.z) versions. + +## Reporting a Vulnerability + +To report a vulnerability on the LuaRocks CLI tool, email +Hisham Muhammad at hisham@luarocks.org. + +To report a vulnerability on the https://luarocks.org website, +email Leaf Corcoran at leafot@gmail.com. + +We will acknowledge your contact as soon as the message is +received, then assess the vulnerability and get back to you +with further feedback once analysis on our end is done. diff --git a/lib/luarocks/internal/luarocks/src/appveyor.yml b/lib/luarocks/internal/luarocks/src/appveyor.yml new file mode 100644 index 000000000..81d0310f2 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/appveyor.yml @@ -0,0 +1,80 @@ +version: 3.9.2.{build}-test + +shallow_clone: true + +matrix: + fast_finish: true + +environment: + LUAROCKS_VER: 3.9.2 + + matrix: + # Lua 5.4 tests + - LUAV: "5.4" + LUAT: "lua" + COMPILER: "vs" + FILES: "" + EXCLUDE: "integration" + - LUAV: "5.4" + LUAT: "lua" + COMPILER: "vs" + FILES: "" + EXCLUDE: "unit" + - LUAV: "5.4" + LUAT: "lua" + COMPILER: "mingw" + FILES: "spec//build_spec.lua" + EXCLUDE: "" + # LuaJIT 2.1 tests + - LUAV: "2.1" + LUAT: "luajit" + COMPILER: "vs" + FILES: "" + EXCLUDE: "integration" + - LUAV: "2.1" + LUAT: "luajit" + COMPILER: "vs" + FILES: "" + EXCLUDE: "unit" + - LUAV: "2.1" + LUAT: "luajit" + COMPILER: "mingw" + FILES: "spec//build_spec.lua" + EXCLUDE: "" + +init: +# Setup Lua development/build environment +# Make VS 2015 command line tools available +- call "%ProgramFiles(x86)%\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" %platform% +# Add MinGW compiler to the path +- set PATH=C:\MinGW\bin;%PATH% + +before_build: + - set PATH=C:\Python37;C:\Python37\Scripts;%PATH% # Add directory containing 'pip' to PATH + - IF NOT EXIST lua_install-%LUAV%\bin\activate.bat ( pip install --upgrade certifi ) + - FOR /F "tokens=* USEBACKQ" %%F IN (`python -c "import certifi;print(certifi.where())"`) DO ( SET SSL_CERT_FILE=%%F ) + - IF NOT EXIST lua_install-%LUAV%\bin\activate.bat ( pip install hererocks ) + - IF NOT EXIST lua_install-%LUAV%\bin\activate.bat ( hererocks lua_install-%LUAV% --%LUAT% %LUAV% --luarocks latest --target=%COMPILER% ) + - call lua_install-%LUAV%\bin\activate + +build_script: + - IF NOT EXIST lua_install-%LUAV%\bin\busted.bat ( luarocks install busted 1> NUL 2> NUL ) + - IF NOT EXIST lua_install-%LUAV%\bin\luacov.bat ( luarocks install cluacov 1> NUL 2> NUL ) + - luarocks install busted-htest 1> NUL 2> NUL + +test_script: + - busted -o htest -v --lpath=.//?.lua --exclude-tags=ssh,unix,%EXCLUDE% -Xhelper lua_dir=%CD%\lua_install-%LUAV%,appveyor,%COMPILER% %FILES% + +after_test: + - pip install codecov + - luacov -c testrun/luacov.config + - cd testrun && codecov -f luacov.report.out -X gcov + +cache: + - lua_install-5.4 + - lua_install-2.1 + - testrun/testing_cache-5.4 + - testrun/testing_cache-2.1 + - testrun/testing_server-5.4 + - testrun/testing_server-2.1 + - testrun/binary-samples diff --git a/lib/luarocks/internal/luarocks/src/binary/Makefile.windows b/lib/luarocks/internal/luarocks/src/binary/Makefile.windows new file mode 100644 index 000000000..8d943ed19 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/binary/Makefile.windows @@ -0,0 +1,76 @@ + +# "i686-w64-mingw32" or "x86_64-w64-mingw32" +MINGW_PREFIX?=i686-w64-mingw32 +# sysroot of your mingw-w64 installation +MINGW_SYSROOT=/usr/lib/mingw-w64-sysroot/$(MINGW_PREFIX) +# "mingw" or "mingw64" +OPENSSL_PLATFORM=mingw +# Versions of dependencies +LIBLUA_VERSION=5.4.3 +OPENSSL_VERSION=1.0.2o +ZLIB_VERSION=1.2.13 +BZIP2_VERSION=1.0.6 + +WINDOWS_DEPS_DIR=windows-deps-$(MINGW_PREFIX) +BUILD_WINDOWS_DEPS_DIR=build-windows-deps-$(MINGW_PREFIX) +BUILD_WINDOWS_BINARY_DIR=build-windows-binary-$(MINGW_PREFIX) + +windows-binary: $(WINDOWS_DEPS_DIR)/lib/liblua.a $(WINDOWS_DEPS_DIR)/lib/libssl.a $(WINDOWS_DEPS_DIR)/lib/libz.a $(WINDOWS_DEPS_DIR)/lib/libbz2.a + STATIC_GCC_AR=$(MINGW_PREFIX)-ar \ + STATIC_GCC_RANLIB=$(MINGW_PREFIX)-ranlib \ + STATIC_GCC_CC=$(MINGW_PREFIX)-gcc \ + LUAROCKS_CROSS_COMPILING=1 \ + $(MAKE) binary LUA_DIR=$(CURDIR)/$(WINDOWS_DEPS_DIR) CC=$(MINGW_PREFIX)-gcc NM=$(MINGW_PREFIX)-nm BINARY_PLATFORM=windows buildbinarydir=$(BUILD_WINDOWS_BINARY_DIR) BINARY_SYSROOT=$(MINGW_SYSROOT) + +$(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION).tar.gz: + mkdir -p $(@D) + cd $(BUILD_WINDOWS_DEPS_DIR) && curl -OL https://www.lua.org/ftp/lua-$(LIBLUA_VERSION).tar.gz +$(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION): $(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION).tar.gz + cd $(BUILD_WINDOWS_DEPS_DIR) && tar zxvpf lua-$(LIBLUA_VERSION).tar.gz +$(WINDOWS_DEPS_DIR)/lib/liblua.a: $(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION) + $(MAKE) -C "$(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION)/src" LUA_A=liblua.a CC=$(MINGW_PREFIX)-gcc AR="$(MINGW_PREFIX)-ar rcu" RANLIB=$(MINGW_PREFIX)-ranlib SYSCFLAGS= SYSLIBS= SYSLDFLAGS= liblua.a + mkdir -p $(WINDOWS_DEPS_DIR)/include + cd $(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION)/src && cp lauxlib.h lua.h lua.hpp luaconf.h lualib.h ../../../$(WINDOWS_DEPS_DIR)/include + mkdir -p $(WINDOWS_DEPS_DIR)/lib + cd $(BUILD_WINDOWS_DEPS_DIR)/lua-$(LIBLUA_VERSION)/src && cp liblua.a ../../../$(WINDOWS_DEPS_DIR)/lib + +$(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION).tar.gz: + mkdir -p $(@D) + cd $(BUILD_WINDOWS_DEPS_DIR) && curl -OL https://www.openssl.org/source/openssl-$(OPENSSL_VERSION).tar.gz +$(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION): $(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION).tar.gz + cd $(BUILD_WINDOWS_DEPS_DIR) && tar zxvpf openssl-$(OPENSSL_VERSION).tar.gz +$(WINDOWS_DEPS_DIR)/lib/libssl.a: $(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION) + cd $(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION) && ./Configure --prefix=$(CURDIR)/$(WINDOWS_DEPS_DIR) --cross-compile-prefix=$(MINGW_PREFIX)- $(OPENSSL_PLATFORM) + $(MAKE) -C "$(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION)" + $(MAKE) -C "$(BUILD_WINDOWS_DEPS_DIR)/openssl-$(OPENSSL_VERSION)" install_sw + +$(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION).tar.gz: + mkdir -p $(@D) + cd $(BUILD_WINDOWS_DEPS_DIR) && curl -OL https://www.zlib.net/zlib-$(ZLIB_VERSION).tar.gz +$(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION): $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION).tar.gz + cd $(BUILD_WINDOWS_DEPS_DIR) && tar zxvpf zlib-$(ZLIB_VERSION).tar.gz +$(WINDOWS_DEPS_DIR)/lib/libz.a: $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && sed -ie "s,dllwrap,$(MINGW_PREFIX)-dllwrap," win32/Makefile.gcc + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && ./configure --prefix=$(CURDIR)/$(WINDOWS_DEPS_DIR) --static + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && $(MAKE) -f win32/Makefile.gcc CC=$(MINGW_PREFIX)-gcc AR=$(MINGW_PREFIX)-ar RC=$(MINGW_PREFIX)-windres STRIP=$(MINGW_PREFIX)-strip IMPLIB=libz.dll.a + mkdir -p $(WINDOWS_DEPS_DIR)/include + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && cp zlib.h zconf.h ../../$(WINDOWS_DEPS_DIR)/include + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && $(MINGW_PREFIX)-strip -g libz.a + mkdir -p $(@D) + cd $(BUILD_WINDOWS_DEPS_DIR)/zlib-$(ZLIB_VERSION) && cp libz.a ../../$(WINDOWS_DEPS_DIR)/lib + +$(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION).tar.gz: + mkdir -p $(@D) + cd $(BUILD_WINDOWS_DEPS_DIR) && curl -OL http://downloads.sourceforge.net/project/bzip2/bzip2-$(BZIP2_VERSION).tar.gz +$(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION): $(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION).tar.gz + cd $(BUILD_WINDOWS_DEPS_DIR) && tar zxvpf bzip2-$(BZIP2_VERSION).tar.gz +$(WINDOWS_DEPS_DIR)/lib/libbz2.a: $(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION) + $(MAKE) -C "$(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION)" libbz2.a CC=$(MINGW_PREFIX)-gcc AR=$(MINGW_PREFIX)-ar RANLIB=$(MINGW_PREFIX)-ranlib + mkdir -p $(WINDOWS_DEPS_DIR)/include + cd $(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION) && cp bzlib.h ../../$(WINDOWS_DEPS_DIR)/include + cd $(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION) && $(MINGW_PREFIX)-strip -g libbz2.a + mkdir -p $(WINDOWS_DEPS_DIR)/lib + cd $(BUILD_WINDOWS_DEPS_DIR)/bzip2-$(BZIP2_VERSION) && cp libbz2.a ../../$(WINDOWS_DEPS_DIR)/lib + +windows-clean: + rm -rf $(WINDOWS_DEPS_DIR) $(BUILD_WINDOWS_BINARY_DIR) diff --git a/lib/luarocks/internal/luarocks/src/binary/all_in_one b/lib/luarocks/internal/luarocks/src/binary/all_in_one new file mode 100755 index 000000000..eace5290f --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/binary/all_in_one @@ -0,0 +1,495 @@ +#!/usr/bin/env lua +--[[ + +All-in-one packager for LuaRocks + * by Hisham Muhammad + * licensed under the same terms as Lua (MIT license). + +Based on: + +* srlua.c - Lua interpreter for self-running programs + * by Luiz Henrique de Figueiredo + * 03 Nov 2014 15:31:43 + * srlua.c is placed in the public domain. +* bin2c.lua - converts a binary to a C string that can be embedded + * by Mark Edgar + * http://lua-users.org/wiki/BinTwoCee + * bin2c.lua is licensed under the same terms as Lua (MIT license). +* lua.c - Lua stand-alone interpreter + * by Luiz Henrique de Figueiredo, Waldemar Celes, Roberto Ierusalimschy + * lua.c is licensed under the same terms as Lua (MIT license). +* luastatic - builds a standalone executable from a Lua program + * by Eric R. Schulz + * https://github.com/ers35/luastatic + * luastatic is licensed under the CC0 1.0 Universal license + +]] + +local MAIN_PROGRAM = arg[1] or "src/bin/luarocks" +local LUA_DIR = arg[2] or "/usr" +local EXCLUDE = arg[3] or "^src/luarocks/admin/" +local SYSCONFDIR = arg[4] or "/etc/luarocks" +local TARGET_DIR = arg[5] or "build-binary" +local FORCE_CONFIG = (arg[6] == "yes") +local MY_PLATFORM = arg[7] or "unix" +local CC = arg[8] or "gcc" +local NM = arg[9] or "nm" +local CROSSCOMPILER_SYSROOT = arg[10] or "/usr/lib/mingw-w64-sysroot/i686-w64-mingw32" +local TRIPLET = arg[11] or CROSSCOMPILER_SYSROOT:gsub(".*/", "") +local PROCESSOR = arg[12] or TRIPLET:gsub("%-.*", "") +if PROCESSOR == "i686" then + PROCESSOR = "x86" +end + +local LUA_MODULES = TARGET_DIR .. "/lua_modules" +local CONFIG_DIR = TARGET_DIR .. "/.luarocks" + +package.path = "./src/?.lua;" .. package.path + +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local cmd = require("luarocks.cmd") +local deps = require("luarocks.deps") +local path = require("luarocks.path") +local manif = require("luarocks.manif") +local queries = require("luarocks.queries") +local persist = require("luarocks.persist") +local sysdetect = require("luarocks.core.sysdetect") + +-------------------------------------------------------------------------------- + +local function if_platform(plat, val) + if MY_PLATFORM == plat then + return val + end +end + +local function reindent_c(input) + local out = {} + local indent = 0 + local previous_is_blank = true + for line in input:gmatch("([^\n]*)") do + line = line:match("^[ \t]*(.-)[ \t]*$") + + local is_blank = (#line == 0) + local do_print = + (not is_blank) or + (not previous_is_blank and indent == 0) + + if line:match("^[})]") then + indent = indent - 1 + if indent < 0 then indent = 0 end + end + if do_print then + table.insert(out, string.rep(" ", indent)) + table.insert(out, line) + table.insert(out, "\n") + end + if line:match("[{(]$") then + indent = indent + 1 + end + + previous_is_blank = is_blank + end + return table.concat(out) +end + +local hexdump +do + local numtab = {} + for i = 0, 255 do + numtab[string.char(i)] = ("%-3d,"):format(i) + end + function hexdump(str) + return (str:gsub(".", numtab):gsub(("."):rep(80), "%0\n")) + end +end + +local c_preamble = [[ + +#include +#include +#include +#include +#include +#include + +/* portable alerts, from srlua */ +#ifdef _WIN32 +#include +#define alert(message) MessageBox(NULL, message, progname, MB_ICONERROR | MB_OK) +#define getprogname() char name[MAX_PATH]; argv[0]= GetModuleFileName(NULL,name,sizeof(name)) ? name : NULL; +#else +#define alert(message) fprintf(stderr,"%s: %s\n", progname, message) +#define getprogname() +#endif + +static int registry_key; + +/* fatal error, from srlua */ +static void fatal(const char* message) { + alert(message); + exit(EXIT_FAILURE); +} + +]] + +local function bin2c_file(out, filename) + local fd = io.open(filename, "rb") + local content = fd:read("*a"):gsub("^#![^\n]+\n", "") + fd:close() + table.insert(out, ("static const unsigned char code[] = {")) + table.insert(out, hexdump(content)) + table.insert(out, ("};")) +end + +local function write_hardcoded_module(dir) + + local system, processor + if if_platform("unix", true) then + system, processor = sysdetect.detect() + else + system, processor = "windows", PROCESSOR + end + + local hardcoded = { + SYSTEM = system, + PROCESSOR = processor, + FORCE_CONFIG = FORCE_CONFIG, + IS_BINARY = true, + + SYSCONFDIR = if_platform("unix", SYSCONFDIR), + + LUA_DIR = if_platform("unix", cfg.variables.LUA_DIR), + LUA_BINDIR = if_platform("unix", cfg.variables.LUA_BINDIR), + LUA_INTERPRETER = if_platform("unix", cfg.lua_interpreter), + } + + local name = dir .. "/luarocks/core/hardcoded.lua" + persist.save_as_module(name, hardcoded) + return name +end + +local function declare_modules(out, dirs, skip) + skip = skip or {} + table.insert(out, [[ + static void declare_modules(lua_State* L) { + lua_settop(L, 0); /* */ + lua_newtable(L); /* modules */ + lua_pushlightuserdata(L, (void*) ®istry_key); /* modules registry_key */ + lua_pushvalue(L, 1); /* modules registry_key modules */ + lua_rawset(L, LUA_REGISTRYINDEX); /* modules */ + ]]) + for _, dir in ipairs(dirs) do + for _, name in ipairs(fs.find(dir)) do + local run = true + for _, pat in ipairs(skip) do + if name:match(pat) then + run = false + break + end + end + if run then + local filename = dir .. "/" .. name + if fs.is_file(filename) then + print(name) + local modname = name:gsub("%.lua$", ""):gsub("/", ".") + table.insert(out, ("/* %s */"):format(modname)) + table.insert(out, ("{")) + bin2c_file(out, filename) + table.insert(out, ("luaL_loadbuffer(L, code, sizeof(code), %q);"):format(filename)) + table.insert(out, ("lua_setfield(L, 1, %q);"):format(modname)) + table.insert(out, ("}")) + end + end + end + end + table.insert(out, [[ + lua_settop(L, 0); /* */ + } + ]]) +end + +local function nm(filename) + local pd = io.popen(NM .. " " .. filename) + local out = pd:read("*a") + pd:close() + return out +end + +local function declare_libraries(out, dir) + local a_files = {} + local externs = {} + local fn = {} + table.insert(fn, [[ + static void declare_libraries(lua_State* L) { + lua_getglobal(L, "package"); /* package */ + lua_getfield(L, -1, "preload"); /* package package.preload */ + ]]) + for _, name in ipairs(fs.find(dir)) do + local filename = dir .. "/" .. name + if name:match("%.a$") then + table.insert(a_files, filename) + local nmout = nm(filename) + for luaopen in nmout:gmatch("[^dD] _?(luaopen_[%a%p%d]+)") do + + -- FIXME what about module names with underscores? + local modname = luaopen:gsub("^_?luaopen_", ""):gsub("_", ".") + + table.insert(externs, "extern int " .. luaopen .. "(lua_State* L);") + table.insert(fn, "lua_pushcfunction(L, " .. luaopen .. ");") + table.insert(fn, "lua_setfield(L, -2, \"" .. modname .. "\");") + end + end + end + local pd = io.popen("find " .. dir .. " -name '*.a'", "r") + for line in pd:lines() do + table.insert(a_files, line) + end + pd:close() + table.insert(fn, [[ + lua_settop(L, 0); /* */ + } + ]]) + + table.insert(out, "\n") + for _, line in ipairs(externs) do + table.insert(out, line) + end + table.insert(out, "\n") + for _, line in ipairs(fn) do + table.insert(out, line) + end + table.insert(out, "\n") + + return a_files +end + +local function load_main(out, main_program, program_name) + table.insert(out, [[static void load_main(lua_State* L) {]]) + bin2c_file(out, main_program) + table.insert(out, ("if(luaL_loadbuffer(L, code, sizeof(code), %q) != LUA_OK) {"):format(program_name)) + table.insert(out, (" fatal(lua_tostring(L, -1));")) + table.insert(out, ("}")) + table.insert(out, [[}]]) + table.insert(out, [[]]) +end + +local c_main = [[ + +/* custom package loader */ +static int pkg_loader(lua_State* L) { + lua_pushlightuserdata(L, (void*) ®istry_key); /* modname ? registry_key */ + lua_rawget(L, LUA_REGISTRYINDEX); /* modname ? modules */ + lua_pushvalue(L, -1); /* modname ? modules modules */ + lua_pushvalue(L, 1); /* modname ? modules modules modname */ + lua_gettable(L, -2); /* modname ? modules mod */ + if (lua_type(L, -1) == LUA_TNIL) { + lua_pop(L, 1); /* modname ? modules */ + lua_pushvalue(L, 1); /* modname ? modules modname */ + lua_pushliteral(L, ".init"); /* modname ? modules modname ".init" */ + lua_concat(L, 2); /* modname ? modules modname..".init" */ + lua_gettable(L, -2); /* modname ? mod */ + } + return 1; +} + +static void install_pkg_loader(lua_State* L) { + lua_settop(L, 0); /* */ + lua_getglobal(L, "table"); /* table */ + lua_getfield(L, -1, "insert"); /* table table.insert */ + lua_getglobal(L, "package"); /* table table.insert package */ + lua_getfield(L, -1, "searchers"); /* table table.insert package package.searchers */ + if (lua_type(L, -1) == LUA_TNIL) { + lua_pop(L, 1); + lua_getfield(L, -1, "loaders"); /* table table.insert package package.loaders */ + } + lua_copy(L, 4, 3); /* table table.insert package.searchers */ + lua_settop(L, 3); /* table table.insert package.searchers */ + lua_pushnumber(L, 1); /* table table.insert package.searchers 1 */ + lua_pushcfunction(L, pkg_loader); /* table table.insert package.searchers 1 pkg_loader */ + lua_call(L, 3, 0); /* table */ + lua_settop(L, 0); /* */ +} + +/* main script launcher, from srlua */ +static int pmain(lua_State *L) { + int argc = lua_tointeger(L, 1); + char** argv = lua_touserdata(L, 2); + int i; + load_main(L); + lua_createtable(L, argc, 0); + for (i = 0; i < argc; i++) { + lua_pushstring(L, argv[i]); + lua_rawseti(L, -2, i); + } + lua_setglobal(L, "arg"); + luaL_checkstack(L, argc - 1, "too many arguments to script"); + for (i = 1; i < argc; i++) { + lua_pushstring(L, argv[i]); + } + lua_call(L, argc - 1, 0); + return 0; +} + +/* error handler, from luac */ +static int msghandler (lua_State *L) { + /* is error object not a string? */ + const char *msg = lua_tostring(L, 1); + if (msg == NULL) { + /* does it have a metamethod that produces a string */ + if (luaL_callmeta(L, 1, "__tostring") && lua_type(L, -1) == LUA_TSTRING) { + /* then that is the message */ + return 1; + } else { + msg = lua_pushfstring(L, "(error object is a %s value)", luaL_typename(L, 1)); + } + } + /* append a standard traceback */ + luaL_traceback(L, L, msg, 1); + return 1; +} + +/* main function, from srlua */ +int main(int argc, char** argv) { + lua_State* L; + getprogname(); + if (argv[0] == NULL) { + fatal("cannot locate this executable"); + } + L = luaL_newstate(); + if (L == NULL) { + fatal("not enough memory for state"); + } + luaL_openlibs(L); + install_pkg_loader(L); + declare_libraries(L); + declare_modules(L); + lua_pushcfunction(L, &msghandler); + lua_pushcfunction(L, &pmain); + lua_pushinteger(L, argc); + lua_pushlightuserdata(L, argv); + if (lua_pcall(L, 2, 0, -4) != 0) { + fatal(lua_tostring(L, -1)); + } + lua_close(L); + return EXIT_SUCCESS; +} + +]] + +local function filter_in(f, xs) + for i = #xs, 1, -1 do + if not f(xs[i]) then + table.remove(xs, i) + end + end + return xs +end + +local function nonnull(x) return x ~= nil end + +local function generate(main_program, dir, skip) + local program_name = main_program:gsub(".*/", "") + + local hardcoded = write_hardcoded_module(dir) + + local out = {} + table.insert(out, ([[static const char* progname = %q;]]):format(program_name)) + table.insert(out, c_preamble) + load_main(out, main_program, program_name) + local lua_modules = LUA_MODULES .. "/share/lua/" .. cfg.lua_version + declare_modules(out, { dir, lua_modules }, skip) + local a_files = declare_libraries(out, LUA_MODULES .. "/lib/lua/" .. cfg.lua_version) + table.insert(out, c_main) + + os.remove(hardcoded) + + local c_filename = TARGET_DIR .. "/" .. program_name .. ".exe.c" + local fd = io.open(c_filename, "w") + fd:write(reindent_c(table.concat(out, "\n"))) + fd:close() + + assert(deps.check_lua_incdir(cfg.variables)) + assert(deps.check_lua_libdir(cfg.variables)) + + cmd = table.concat(filter_in(nonnull, { + CC, "-o", TARGET_DIR .. "/" .. program_name .. ".exe", + "-I", cfg.variables.LUA_INCDIR, + if_platform("unix", "-rdynamic"), + "-Os", + c_filename, + "-L", cfg.variables.LUA_LIBDIR, + table.concat(a_files, " "), + --if_platform("unix", cfg.variables.LUA_LIBDIR .. "/" .. cfg.variables.LUALIB:gsub("%.so.*$", ".a")), + --if_platform("windows", "mingw/liblua.a"), -- FIXME + cfg.variables.LUA_LIBDIR .. "/" .. cfg.variables.LUALIB:gsub("%.so.*$", ".a"), + if_platform("unix", "-ldl"), + if_platform("unix", "-lpthread"), + if_platform("windows", "-mconsole -mwindows"), + "-lm" + }), " ") + print(cmd) + os.execute(cmd) +end + +-------------------------------------------------------------------------------- + +local function main() + + os.remove("src/luarocks/core/hardcoded.lua") + cfg.init() + cfg.variables.LUA_DIR = LUA_DIR + cfg.variables.LUA_INCDIR = nil -- let it autodetect later + cfg.variables.LUA_LIBDIR = nil -- let it autodetect later + fs.init() + path.use_tree(LUA_MODULES) + + local CONFIG_FILE = CONFIG_DIR .. "/config-" .. cfg.lua_version .. ".lua" + + fs.make_dir(CONFIG_DIR) + + persist.save_from_table(CONFIG_FILE, { + lib_extension = "a", + external_lib_extension = "a", + variables = { + CC = fs.current_dir() .. "/binary/static-gcc", + LD = fs.current_dir() .. "/binary/static-gcc", + LIB_EXTENSION = "a", + LUA_DIR = LUA_DIR, + LIBFLAG = "-static", + PWD = "pwd", + }, + platforms = if_platform("windows", { "windows", "win32", "mingw32" }), + external_deps_dirs = if_platform("windows", { CROSSCOMPILER_SYSROOT, fs.current_dir() .. "/windows-deps-" .. TRIPLET }), + }) + + local dependencies = { + md5 = "md5", + luasec = "./binary/luasec-1.0.2-1.rockspec", + ["lua-zlib"] = "./binary/lua-zlib-1.2-0.rockspec", + ["lua-bz2"] = "./binary/lua-bz2-0.2.1-1.rockspec", + luaposix = if_platform("unix", "./binary/luaposix-35.1-1.rockspec"), + luasocket = "./binary/luasocket-3.1.0-1.rockspec", + luafilesystem = "luafilesystem", + dkjson = "dkjson", + } + + fs.make_dir(LUA_MODULES) + for name, arg in pairs(dependencies) do + print("----------------------------------------------------------------") + print(name) + print("----------------------------------------------------------------") + local vers = manif.get_versions(queries.from_dep_string(name), "one") + if not next(vers) then + local ok = os.execute("LUAROCKS_CONFIG='" .. CONFIG_FILE .. "' ./luarocks install --no-project '--tree=" .. LUA_MODULES .. "' " .. arg) + if ok ~= 0 and ok ~= true then + error("Failed building dependency: " .. name) + end + end + end + + generate(MAIN_PROGRAM, "src", { EXCLUDE, "^bin/?" }) +end + +main() diff --git a/lib/luarocks/internal/luarocks/src/binary/lua-bz2-0.2.1-1.rockspec b/lib/luarocks/internal/luarocks/src/binary/lua-bz2-0.2.1-1.rockspec new file mode 100644 index 000000000..d5a73778b --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/binary/lua-bz2-0.2.1-1.rockspec @@ -0,0 +1,44 @@ +package = "lua-bz2" +version = "0.2.1-1" +source = { + url = "git+ssh://git@github.com/hishamhm/lua-bz2.git", + tag = "0.2.1", +} +description = { + summary = "A Lua binding to Julian Seward's libbzip2", + detailed = [[ + Support for reading and writing .bz2 files + and handling streams compressed in bzip2 format. + ]], + homepage = "https://github.com/harningt/lua-bz2", + license = "ISC" +} +external_dependencies = { + BZ2 = { + library = "bz2" + } +} +build = { + type = "builtin", + modules = { + bz2 = { + incdirs = { + "$(BZ2_INCDIR)" + }, + libdirs = { + "$(BZ2_LIBDIR)" + }, + libraries = { + "bz2" + }, + sources = { + "lbz.c", + "lbz2_common.c", + "lbz2_file_reader.c", + "lbz2_file_writer.c", + "lbz2_stream.c", + } + }, + ["bz2.ltn12"] = "bz2/ltn12.lua", + } +} diff --git a/lib/luarocks/internal/luarocks/src/binary/lua-zlib-1.2-0.rockspec b/lib/luarocks/internal/luarocks/src/binary/lua-zlib-1.2-0.rockspec new file mode 100644 index 000000000..9d3adc8f1 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/binary/lua-zlib-1.2-0.rockspec @@ -0,0 +1,39 @@ +package = "lua-zlib" +version = "1.2-0" +source = { + url = "git+https://github.com/brimworks/lua-zlib.git", + tag = "v1.2", +} +description = { + summary = "Simple streaming interface to zlib for Lua.", + detailed = [[ + Simple streaming interface to zlib for Lua. + Consists of two functions: inflate and deflate. + Both functions return "stream functions" (takes a buffer of input and returns a buffer of output). + This project is hosted on github. + ]], + homepage = "https://github.com/brimworks/lua-zlib", + license = "MIT" +} +dependencies = { + "lua >= 5.1, <= 5.4" +} +external_dependencies = { + ZLIB = { + header = "zlib.h", + library = "z", + } +} + +build = { + type = "builtin", + modules = { + zlib = { + sources = { "lua_zlib.c" }, + libraries = { "z" }, + defines = { "LZLIB_COMPAT" }, + incdirs = { "$(ZLIB_INCDIR)" }, + libdirs = { "$(ZLIB_LIBDIR)" }, + } + }, +} diff --git a/lib/luarocks/internal/luarocks/src/binary/luaposix-35.1-1.rockspec b/lib/luarocks/internal/luarocks/src/binary/luaposix-35.1-1.rockspec new file mode 100644 index 000000000..1940c7555 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/binary/luaposix-35.1-1.rockspec @@ -0,0 +1,61 @@ +local _MODREV, _SPECREV = '35.1', '-1' + +package = 'luaposix' +version = _MODREV .. _SPECREV + +description = { + summary = 'Lua bindings for POSIX', + detailed = [[ + A library binding various POSIX APIs. POSIX is the IEEE Portable + Operating System Interface standard. luaposix is based on lposix. + ]], + homepage = 'http://github.com/luaposix/luaposix/', + license = 'MIT/X11', +} + +dependencies = { + 'lua >= 5.1, < 5.5', +} + +do + -- We only want to install a bit32 module for Lua 5.1. + local _ENV={package=nil, dependencies=dependencies} + if package then + dependencies[#dependencies + 1] = 'bit32' + end +end + +source = { + url = 'http://github.com/luaposix/luaposix/archive/v' .. _MODREV .. '.zip', + dir = 'luaposix-' .. _MODREV, +} + +build = { + type = 'command', + build_command = '$(LUA) build-aux/luke' + .. ' package="' .. package .. '"' + .. ' version="' .. _MODREV .. '"' + .. ' PREFIX="$(PREFIX)"' + .. ' LUA="$(LUA)"' + .. ' LUA_INCDIR="$(LUA_INCDIR)"' + .. ' CFLAGS="$(CFLAGS)"' + .. ' LIBFLAG="$(LIBFLAG)"' + .. ' LIB_EXTENSION="$(LIB_EXTENSION)"' + .. ' OBJ_EXTENSION="$(OBJ_EXTENSION)"' + .. ' INST_LIBDIR="$(LIBDIR)"' + .. ' INST_LUADIR="$(LUADIR)"' + , + install_command = '$(LUA) build-aux/luke install --quiet' + .. ' INST_LIBDIR="$(LIBDIR)"' + .. ' LIB_EXTENSION="$(LIB_EXTENSION)"' + .. ' INST_LUADIR="$(LUADIR)"' + , +} + +if _MODREV == 'git' then + dependencies[#dependencies + 1] = 'ldoc' + + source = { + url = 'git://github.com/luaposix/luaposix.git', + } +end diff --git a/lib/luarocks/internal/luarocks/src/binary/luasec-1.0.2-1.rockspec b/lib/luarocks/internal/luarocks/src/binary/luasec-1.0.2-1.rockspec new file mode 100644 index 000000000..b6741257e --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/binary/luasec-1.0.2-1.rockspec @@ -0,0 +1,115 @@ +package = "LuaSec" +version = "1.0.2-1" +source = { + url = "git://github.com/brunoos/luasec", + tag = "v1.0.2", +} +description = { + summary = "A binding for OpenSSL library to provide TLS/SSL communication over LuaSocket.", + detailed = "This version delegates to LuaSocket the TCP connection establishment between the client and server. Then LuaSec uses this connection to start a secure TLS/SSL session.", + homepage = "https://github.com/brunoos/luasec/wiki", + license = "MIT" +} +dependencies = { + "lua >= 5.1", "luasocket" +} +external_dependencies = { + platforms = { + unix = { + OPENSSL = { + header = "openssl/ssl.h", + library = "ssl" + } + }, + windows = { + OPENSSL = { + header = "openssl/ssl.h", + } + }, + mingw32 = { + OPENSSL = { + library = "ssl", + } + }, + } +} +build = { + type = "builtin", + copy_directories = { + "samples" + }, + platforms = { + unix = { + modules = { + ['ssl.https'] = "src/https.lua", + ['ssl.init'] = "src/ssl.lua", + ssl = { + defines = { + "WITH_LUASOCKET", "LUASOCKET_DEBUG", + }, + incdirs = { + "$(OPENSSL_INCDIR)", "src/", "src/luasocket", + }, + libdirs = { + "$(OPENSSL_LIBDIR)" + }, + libraries = { + "ssl", "crypto" + }, + sources = { + "src/options.c", "src/config.c", "src/ec.c", + "src/x509.c", "src/context.c", "src/ssl.c", + "src/luasocket/buffer.c", "src/luasocket/io.c", + "src/luasocket/usocket.c" -- , "src/luasocket/timeout.c" + } + } + } + }, + windows = { + modules = { + ['ssl.https'] = "src/https.lua", + ['ssl.init'] = "src/ssl.lua", + ssl = { + defines = { + "WIN32", "NDEBUG", "_WINDOWS", "_USRDLL", "LSEC_EXPORTS", "BUFFER_DEBUG", "LSEC_API=__declspec(dllexport)", + "WITH_LUASOCKET", "LUASOCKET_DEBUG", + "LUASEC_INET_NTOP", "WINVER=0x0501", "_WIN32_WINNT=0x0501", "NTDDI_VERSION=0x05010300" + }, + libdirs = { + "$(OPENSSL_LIBDIR)", + "$(OPENSSL_BINDIR)", + }, + libraries = { + "ssl", "crypto", "ws2_32" + }, + incdirs = { + "$(OPENSSL_INCDIR)", "src/", "src/luasocket" + }, + sources = { + "src/options.c", "src/config.c", "src/ec.c", + "src/x509.c", "src/context.c", "src/ssl.c", + "src/luasocket/buffer.c", "src/luasocket/io.c", + "src/luasocket/wsocket.c", "src/luasocket/timeout.c" + } + } + }, + patches = { +["lowercase-winsock-h.diff"] = [[ +diff --git a/src/ssl.c b/src/ssl.c +index 95109c4..e5defa8 100644 +--- a/src/ssl.c ++++ b/src/ssl.c +@@ -11,7 +11,7 @@ + #include + + #if defined(WIN32) +-#include ++#include + #endif + + #include +]] + } + } + } +} diff --git a/lib/luarocks/internal/luarocks/src/binary/luasocket-3.1.0-1.rockspec b/lib/luarocks/internal/luarocks/src/binary/luasocket-3.1.0-1.rockspec new file mode 100644 index 000000000..f33080b00 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/binary/luasocket-3.1.0-1.rockspec @@ -0,0 +1,136 @@ +package = "LuaSocket" +version = "3.1.0-1" +source = { + url = "git+https://github.com/lunarmodules/luasocket.git", + tag = "v3.1.0" +} +description = { + summary = "Network support for the Lua language", + detailed = [[ + LuaSocket is a Lua extension library composed of two parts: a set of C + modules that provide support for the TCP and UDP transport layers, and a + set of Lua modules that provide functions commonly needed by applications + that deal with the Internet. + ]], + homepage = "https://github.com/lunarmodules/luasocket", + license = "MIT" +} +dependencies = { + "lua >= 5.1" +} + +local function make_plat(plat) + local defines = { + unix = { + "LUASOCKET_DEBUG" + }, + macosx = { + "LUASOCKET_DEBUG", + "UNIX_HAS_SUN_LEN" + }, + win32 = { + "LUASOCKET_DEBUG", + "NDEBUG" + }, + mingw32 = { + "LUASOCKET_DEBUG", + "LUASOCKET_INET_PTON", + "WINVER=0x0501", + }, + } + local modules = { + ["socket.core"] = { + sources = { + "src/luasocket.c" + , "src/timeout.c" + , "src/buffer.c" + , "src/io.c" + , "src/auxiliar.c" + , "src/options.c" + , "src/inet.c" + , "src/except.c" + , "src/select.c" + , "src/tcp.c" + , "src/udp.c" + , "src/compat.c" }, + defines = defines[plat], + incdir = "/src" + }, + ["mime.core"] = { + sources = { "src/mime.c", "src/compat.c" }, + defines = defines[plat], + incdir = "/src" + }, + ["socket.http"] = "src/http.lua", + ["socket.url"] = "src/url.lua", + ["socket.tp"] = "src/tp.lua", + ["socket.ftp"] = "src/ftp.lua", + ["socket.headers"] = "src/headers.lua", + ["socket.smtp"] = "src/smtp.lua", + ltn12 = "src/ltn12.lua", + socket = "src/socket.lua", + mime = "src/mime.lua" + } + if plat == "unix" + or plat == "macosx" + or plat == "haiku" + then + modules["socket.core"].sources[#modules["socket.core"].sources+1] = "src/usocket.c" + if plat == "haiku" then + modules["socket.core"].libraries = {"network"} + end + modules["socket.unix"] = { + sources = { + "src/buffer.c" + , "src/compat.c" + , "src/auxiliar.c" + , "src/options.c" + , "src/timeout.c" + , "src/io.c" + , "src/usocket.c" + , "src/unix.c" + , "src/unixdgram.c" + , "src/unixstream.c" }, + defines = defines[plat], + incdir = "/src" + } + modules["socket.serial"] = { + sources = { + "src/buffer.c" + , "src/compat.c" + , "src/auxiliar.c" + , "src/options.c" + , "src/timeout.c" + , "src/io.c" + , "src/usocket.c" + , "src/serial.c" }, + defines = defines[plat], + incdir = "/src" + } + end + if plat == "win32" + or plat == "mingw32" + then + modules["socket.core"].sources[#modules["socket.core"].sources+1] = "src/wsocket.c" + modules["socket.core"].libraries = { "ws2_32" } + modules["socket.core"].libdirs = {} + end + return { modules = modules } +end + +build = { + type = "builtin", + platforms = { + unix = make_plat("unix"), + macosx = make_plat("macosx"), + haiku = make_plat("haiku"), + win32 = make_plat("win32"), + mingw32 = make_plat("mingw32"), + mingw64 = make_plat("mingw64") + }, + copy_directories = { + "docs" + , "samples" + , "etc" + , "test" } +} diff --git a/lib/luarocks/internal/luarocks/src/binary/static-gcc b/lib/luarocks/internal/luarocks/src/binary/static-gcc new file mode 100755 index 000000000..1e4ad0eb1 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/binary/static-gcc @@ -0,0 +1,170 @@ +#!/usr/bin/env bash + +STATIC_GCC_AR=${STATIC_GCC_AR:-ar} +STATIC_GCC_RANLIB=${STATIC_GCC_RANLIB:-ranlib} +STATIC_GCC_CC=${STATIC_GCC_CC:-gcc} + +DIR="$( cd "$( dirname "$0" )" && pwd )" + +function log() { echo -- "$@" >> $DIR/log.txt; } + +function runlog() { log "$@"; "$@"; } + +log "---------------------------" +log INP "$@" + +allargs=() +sources=() +objects=() +etc=() +libdirs=($("$STATIC_GCC_CC" -print-search-dirs | grep libraries | cut -d= -f2 | tr ':' '\n')) +incdirs=() + +linking=0 + +while [ "$1" ] +do + allargs+=("$1") + if [ "$next_libdir" = "1" ] + then + libdirs+=("$1") + next_libdir=0 + elif [ "$next_incdir" = "1" ] + then + incdirs+=("-I$1") + next_incdir=0 + elif [ "$next_lib" = "1" ] + then + libs+=("$1") + next_lib=0 + elif [ "$next_output" = "1" ] + then + output="$1" + next_output=0 + else + case "$1" in + -*) + case "$1" in + -shared) + linking=1 + ;; + -static) + linking=1 + ;; + -o) + next_output=1 + ;; + -c) + object=1 + etc+=("$1") + ;; + -L) + next_libdir=1 + ;; + -L*) + libdirs+=("${1:2}") + ;; + -I) + next_incdir=1 + ;; + -I*) + incdirs+=("$1") + ;; + -l) + next_lib=1 + ;; + -l*) + libs+=("${1:2}") + ;; + *) + etc+=("$1") + ;; + esac + ;; + *.c) + sources+=("$1") + ;; + *.o) + objects+=("$1") + ;; + *) + etc+=("$1") + ;; + esac + fi + shift +done + +staticlibs=() +for lib in "${libs[@]}" +do + found=0 + for libdir in "${libdirs[@]}" + do + staticlib="$libdir/lib$lib.a" + if [ -e "$staticlib" ] + then + staticlibs+=("$staticlib") + found=1 + break + fi + done + if [ "$found" = 0 ] + then + log "STATICLIB not found for $lib" + runlog exit 1 + fi +done + +oflag=() +if [ "$output" != "" ] +then + oflag=("-o" "$output") +fi + +if [ "$linking" = "1" ] +then + log LINK + if [ "${#sources[@]}" -gt 0 ] + then + for source in "${sources[@]}" + do + object="${source%.c}.o" + runlog $STATIC_GCC_CC "${incdirs[@]}" "${etc[@]}" -c -o "$object" "$source" + [ "$?" = 0 ] || runlog exit $? + objects+=("$object") + done + fi + + # runlog ar rcu "${oflag[@]}" "${objects[@]}" "${staticlibs[@]}" + echo "CREATE $output" > ar.script + for o in "${objects[@]}" + do + echo "ADDMOD $o" >> ar.script + done + for o in "${staticlibs[@]}" + do + echo "ADDLIB $o" >> ar.script + done + echo "SAVE" >> ar.script + echo "END" >> ar.script + cat ar.script >> "$DIR/log.txt" + cat ar.script | $STATIC_GCC_AR -M + [ "$?" = 0 ] || runlog exit $? + + [ -e "$output" ] || { + exit 1 + } + + runlog $STATIC_GCC_RANLIB "$output" + runlog exit $? +elif [ "$object" = 1 ] +then + log OBJECT + runlog $STATIC_GCC_CC "${oflag[@]}" "${incdirs[@]}" "${etc[@]}" "${sources[@]}" + runlog exit $? +else + log EXECUTABLE + runlog $STATIC_GCC_CC "${allargs[@]}" + runlog exit $? +fi diff --git a/lib/luarocks/internal/luarocks/src/configure b/lib/luarocks/internal/luarocks/src/configure new file mode 100755 index 000000000..a8bf88090 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/configure @@ -0,0 +1,512 @@ +#!/bin/sh + +# Defaults + +prefix="/usr/local" +sysconfdir="$prefix/etc" +rocks_tree="$prefix" + +# ---------------------------------------------------------------------------- +# FUNCTION DEFINITIONS +# ---------------------------------------------------------------------------- + +# Utility functions +# ----------------- + +# Resolves a full path +# - alternative to "readlink -f", which is not available on solaris +# based on https://stackoverflow.com/a/6554854/1793220 +canonicalpath() { + oldpwd="$PWD" + if [ -d "$1" ] + then + if cd "$1" >/dev/null 2>&1 + then + echo "$PWD" + else + echo "$1" + fi + else + if cd "$(dirname "$1")" >/dev/null 2>&1 + then + if [ "$PWD" = "/" ] + then + echo "/$(basename "$1")" + else + echo "$PWD/$(basename "$1")" + fi + else + echo "$1" + fi + fi + cd "$oldpwd" >/dev/null 2>&1 || return +} + +find_program() { + prog=$(command -v "$1" 2>/dev/null) + if [ -n "$prog" ] + then + dirname "$prog" + fi +} + +die() { + echo "$*" + echo + RED "configure failed." + echo + echo + exit 1 +} + +echo_n() { + printf "%s" "$*" +} + +bold='\033[1m' +red='\033[1;31m' +green='\033[1;32m' +blue='\033[1;36m' +reset='\033[0m' + +BOLD() { + printf "$bold%s$reset" "$*" +} + +RED() { + printf "$red%s$reset" "$*" +} + +GREEN() { + printf "$green%s$reset" "$*" +} + +BLUE() { + printf "$blue%s$reset" "$*" +} + +# Help +# ---- + +show_help() { +cat < /dev/null) + if [ "$detected_lua" != "nil" ] + then + if [ "$LUA_VERSION_SET" != "yes" ] + then + echo "Lua version detected: $(GREEN "$detected_lua")" + LUA_VERSION=$detected_lua + return 0 + elif [ "$LUA_VERSION" = "$detected_lua" ] + then + return 0 + fi + fi + return 1 +} + +search_interpreter() { + name="$1" + lua_at="" + if [ "$LUA_BINDIR_SET" = "yes" ] + then + lua_at="$LUA_BINDIR" + elif [ "$LUA_DIR_SET" = "yes" ] + then + LUA_BINDIR="$LUA_DIR/bin" + if [ -f "$LUA_BINDIR/$name" ] + then + lua_at="$LUA_BINDIR" + fi + else + lua_at=$(find_program "$name") + fi + if [ -n "$lua_at" ] && [ -x "$lua_at/$name" ] + then + if detect_lua_version "$lua_at/$name" + then + echo "Lua interpreter found: $(GREEN "$lua_at/$name")" + if [ "$LUA_BINDIR_SET" != "yes" ] + then + LUA_BINDIR="$lua_at" + fi + if [ "$LUA_DIR_SET" != "yes" ] + then + LUA_DIR=$(dirname "$lua_at") + fi + LUA_INTERPRETER="$name" + return 0 + fi + fi + return 1 +} + +# ---------------------------------------------------------------------------- +# MAIN PROGRAM +# ---------------------------------------------------------------------------- + +# Parse options + +while [ -n "$1" ] +do + value="$(echo "$1" | sed 's/[^=]*.\(.*\)/\1/')" + key="$(echo "$1" | sed 's/=.*//')" + if echo "$value" | grep "~" >/dev/null 2>/dev/null + then + echo + echo "$(RED WARNING:) the '~' sign is not expanded in flags." + echo "If you mean the home directory, use \$HOME instead." + echo + fi + case "$key" in + + # Help + # ---- + -h|--help) + show_help + exit 0 + ;; + + # Where to install LuaRocks: + # -------------------------- + --prefix) + [ -n "$value" ] || die "Missing value in flag $key." + prefix="$(canonicalpath "$value")" + prefix_SET=yes + ;; + --sysconfdir) + [ -n "$value" ] || die "Missing value in flag $key." + sysconfdir="$(canonicalpath "$value")" + sysconfdir_SET=yes + ;; + + + # Where to install files provided by rocks: + # ----------------------------------------- + --rocks-tree) + [ -n "$value" ] || die "Missing value in flag $key." + rocks_tree="$(canonicalpath "$value")" + rocks_tree_SET=yes + ;; + + # Where is your Lua interpreter: + # ------------------------------ + --lua-version|--with-lua-version) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_VERSION="$value" + case "$LUA_VERSION" in + 5.1|5.2|5.3|5.4) ;; + *) die "Invalid Lua version in flag $key." + esac + LUA_VERSION_SET=yes + ;; + --with-lua) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_DIR="$(canonicalpath "$value")" + [ -d "$LUA_DIR" ] || die "Bad value for --with-lua: $LUA_DIR is not a valid directory." + LUA_DIR_SET=yes + ;; + --with-lua-bin) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_BINDIR="$(canonicalpath "$value")" + [ -d "$LUA_BINDIR" ] || die "Bad value for --with-lua-bin: $LUA_BINDIR is not a valid directory." + LUA_BINDIR_SET=yes + ;; + --with-lua-include) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_INCDIR="$(canonicalpath "$value")" + [ -d "$LUA_INCDIR" ] || die "Bad value for --with-lua-include: $LUA_INCDIR is not a valid directory." + LUA_INCDIR_SET=yes + ;; + --with-lua-lib) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_LIBDIR="$(canonicalpath "$value")" + [ -d "$LUA_LIBDIR" ] || die "Bad value for --with-lua-lib: $LUA_LIBDIR is not a valid directory." + LUA_LIBDIR_SET=yes + ;; + --with-lua-interpreter) + [ -n "$value" ] || die "Missing value in flag $key." + LUA_INTERPRETER="$value" + LUA_INTERPRETER_SET=yes + ;; + + # For specialized uses of LuaRocks: + # --------------------------------- + --force-config) + FORCE_CONFIG=yes + ;; + --disable-incdir-check) + DISABLE_INCDIR_CHECK=yes + ;; + + # Old options that no longer apply + # -------------------------------- + --versioned-rocks-dir) + echo "--versioned-rocks-dir is no longer necessary." + echo "The rocks tree in LuaRocks 3.0 is always versioned." + ;; + --lua-suffix) + echo "--lua-suffix is no longer necessary." + echo "The suffix is automatically detected." + ;; + + *) + die "Error: Unknown flag: $1" + ;; + esac + shift +done + +echo +BLUE "Configuring LuaRocks version 3.9.2..." +echo +echo + +# ---------------------------------------- +# Derive options from the ones given +# ---------------------------------------- + +if [ "$prefix_SET" = "yes" ] && [ ! "$sysconfdir_SET" = "yes" ] +then + if [ "$prefix" = "/usr" ] + then sysconfdir=/etc + else sysconfdir=$prefix/etc + fi + sysconfdir_SET=yes +fi + +if [ "$prefix_SET" = "yes" ] && [ ! "$rocks_tree_SET" = "yes" ] +then + rocks_tree=$prefix +fi + +# ---------------------------------------- +# Search for Lua +# ---------------------------------------- + +lua_interp_found=no + +case "$LUA_VERSION" in +5.1) + names="lua5.1 lua51 lua-5.1 lua-51 luajit lua" + ;; +5.2) + names="lua5.2 lua52 lua-5.2 lua-52 lua" + ;; +5.3) + names="lua5.3 lua53 lua-5.3 lua-53 lua" + ;; +5.4) + names="lua5.4 lua54 lua-5.4 lua-54 lua" + ;; +*) + names="lua5.4 lua54 lua-5.4 lua-54 lua5.3 lua53 lua-5.3 lua-53 lua5.2 lua52 lua-5.2 lua-52 lua5.1 lua51 lua-5.1 lua-51 luajit lua" + ;; +esac + +if [ "$LUA_INTERPRETER_SET" = "yes" ] +then + names="$LUA_INTERPRETER" +fi + +for name in $names +do + search_interpreter "$name" && { + lua_interp_found=yes + break + } +done + +if [ "$lua_interp_found" != "yes" ] +then + if [ "$LUA_VERSION_SET" ] + then + interp="Lua $LUA_VERSION" + else + interp="Lua" + fi + if [ "$LUA_DIR_SET" ] || [ "$LUA_BINDIR_SET" ] + then + where="$LUA_BINDIR" + else + where="\$PATH" + fi + echo "$(RED $interp interpreter not found) in $where" + echo "You may want to use the flags $(BOLD --with-lua), $(BOLD --with-lua-bin) and/or $(BOLD --lua-version)" + die "Run $(BOLD ./configure --help) for details." +fi + +if [ "$LUA_VERSION_SET" = "yes" ] +then + echo_n "Checking if $LUA_BINDIR/$LUA_INTERPRETER is Lua version $LUA_VERSION... " + if detect_lua_version "$LUA_BINDIR/$LUA_INTERPRETER" + then + echo "yes" + else + echo "no" + die "You may want to use the flags --with-lua, --with-lua-bin and/or --lua-version. See --help." + fi +fi + +# ---------------------------------------- +# Additional checks +# ---------------------------------------- + +check_incdir() { + if [ "$LUA_INCDIR_SET" = "yes" ] + then + incdir="$LUA_INCDIR" + else + incdir="$LUA_DIR/include" + fi + + tried="" + # Verification of the Lua include path happens at LuaRocks runtime, + # but we also perform it here just so that the user gets an early failure + # if they try to install LuaRocks with the Lua interpreter package + # but not the "development files" that many Linux distros ship separately. + for lua_h in \ + "$incdir/lua/$LUA_VERSION/lua.h" \ + "$incdir/lua$LUA_VERSION/lua.h" \ + "$incdir/lua-$LUA_VERSION/lua.h" \ + "$incdir/lua$(echo "$LUA_VERSION" | tr -d .)/lua.h" \ + "$incdir/lua.h" \ + $("$LUA_BINDIR/$LUA_INTERPRETER" -e 'print(jit and [['"$incdir"'/luajit-]]..jit.version:match("(%d+%.%d+)")..[[/lua.h]] or "")') + do + if [ -f "$lua_h" ] + then + grep "LUA_VERSION_NUM.*$LUA_VERSION" "$lua_h" > /dev/null 2> /dev/null && return + fi + tried="$tried $lua_h" + done + + echo "$(RED "lua.h for Lua $LUA_VERSION not found") (tried$tried)" + echo + echo "If the development files for Lua (headers and libraries)" + echo "are installed in your system, you may need to use the" + echo "$(BOLD --with-lua) or $(BOLD --with-lua-include) flags to specify their location." + echo + echo "If those files are not yet installed, you need to install" + echo "them using the appropriate method for your operating system." + echo + die "Run $(BOLD ./configure --help) for details on flags." +} + +if [ "$DISABLE_INCDIR_CHECK" != "yes" ] +then + check_incdir + echo "lua.h found: $(GREEN "$lua_h")" +fi + +unzip_found=$(find_program "unzip") +if [ -n "$unzip_found" ] +then + echo "unzip found in PATH: $(GREEN "$unzip_found")" +else + RED "Could not find 'unzip'." + echo + die "Make sure it is installed and available in your PATH." +fi + +# ---------------------------------------- +# Write config +# ---------------------------------------- + +make clean > /dev/null 2> /dev/null + +rm -f built +cat < config.unix +# This file was automatically generated by the configure script. +# Run "./configure --help" for details. + +prefix=$prefix +sysconfdir=$sysconfdir +rocks_tree=$rocks_tree +LUA_VERSION=$LUA_VERSION +LUA_INTERPRETER=$LUA_INTERPRETER +LUA_DIR=$LUA_DIR +LUA_BINDIR=$LUA_BINDIR +LUA_INCDIR=$LUA_INCDIR +LUA_LIBDIR=$LUA_LIBDIR +FORCE_CONFIG=$FORCE_CONFIG +EOF + +echo +BLUE "Done configuring." +echo +echo +echo "LuaRocks will be installed at......: $(GREEN "$prefix")" +echo "LuaRocks will install rocks at.....: $(GREEN "$rocks_tree")" +echo "LuaRocks configuration directory...: $(GREEN "$sysconfdir/luarocks")" +echo "Using Lua from.....................: $(GREEN "$LUA_DIR")" +if [ "$LUA_BINDIR_SET" = "yes" ]; then echo "Lua bin directory..................: $(GREEN "$LUA_BINDIR")" ; fi +if [ "$LUA_INCDIR_SET" = "yes" ]; then echo "Lua include directory..............: $(GREEN "$LUA_INCDIR")" ; fi +if [ "$LUA_LIBDIR_SET" = "yes" ]; then echo "Lua lib directory..................: $(GREEN "$LUA_LIBDIR")" ; fi +echo +echo "* Type $(BOLD make) and $(BOLD make install):" +echo " to install to $prefix as usual." +echo "* Type $(BOLD make bootstrap):" +echo " to install LuaRocks into $rocks_tree as a rock." +echo diff --git a/lib/luarocks/internal/luarocks/src/luarocks-3.9.2-1.rockspec b/lib/luarocks/internal/luarocks/src/luarocks-3.9.2-1.rockspec new file mode 100644 index 000000000..c4fe644de --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/luarocks-3.9.2-1.rockspec @@ -0,0 +1,38 @@ +rockspec_format = "3.0" +package = "luarocks" +version = "3.9.2-1" +source = { + url = "git+https://github.com/luarocks/luarocks", + tag = "v3.9.2" +} +description = { + summary = "A package manager for Lua modules.", + detailed = [[ + LuaRocks allows you to install Lua modules as self-contained + packages called "rocks", which also contain version dependency + information. This information is used both during installation, + so that when one rock is requested all rocks it depends on are + installed as well, and at run time, so that when a module is + required, the correct version is loaded. LuaRocks supports both + local and remote repositories, and multiple local rocks trees. + ]], + homepage = "http://www.luarocks.org", + issues_url = "https://github.com/luarocks/luarocks/issues", + maintainer = "Hisham Muhammad", + license = "MIT", +} +test_dependencies = { + "luacov", + "busted-htest", +} +test = { + type = "busted", + platforms = { + windows = { + flags = { "--exclude-tags=ssh,git,unix", "-Xhelper", "lua_dir=$(LUA_DIR)", "-Xhelper", "lua_interpreter=$(LUA)" } + }, + unix = { + flags = { "--exclude-tags=ssh,git", "-Xhelper", "lua_dir=$(LUA_DIR)", "-Xhelper", "lua_interpreter=$(LUA)" } + } + } +} diff --git a/lib/luarocks/internal/luarocks/src/makedist b/lib/luarocks/internal/luarocks/src/makedist new file mode 100755 index 000000000..0ba46034d --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/makedist @@ -0,0 +1,197 @@ +#!/bin/bash -e + +if ! [ "$1" ] +then + echo "usage: $0 " + exit 1 +fi + +if ! [ -d ".git" ] +then + echo "Should be run from the LuaRocks git repo dir." + exit 1 +fi + +make clean || exit 1 + +version=$1 +shift + +#------------------------------------------------------------------------------- +if ! [ "$version" = "dev" ] +then + +# e.g. if $version is "2.3.0", $xyversion is "2.3" +xyversion=${version%.*} + +ROCKSPEC="luarocks-$version-1.rockspec" + +if [ "$1" = "branch" ] +then + shift + + if git show $version &> /dev/null + then + echo "Branch $version already exists." + exit 1 + fi + + git reset + git checkout . + git checkout -B $version + ROCKSPEC="luarocks-$version-1.rockspec" + currentrockspec=$(ls luarocks-*.rockspec) + if [ "$currentrockspec" != "$ROCKSPEC" ] && ! [ -e "$ROCKSPEC" ] + then + git mv luarocks-*.rockspec "$ROCKSPEC" + fi + sed -i 's/"Configuring LuaRocks version .*"/"Configuring LuaRocks version '$version'..."/' configure + sed -i 's/version = "[^"]*"/version = "'$version'-1"/' $ROCKSPEC + sed -i 's/\( url = "[^"]*",\)/\1\n tag = "v'$version'"/' $ROCKSPEC + sed -i 's/program_version = "[^"]*"/program_version = "'$version'"/' src/luarocks/core/cfg.lua + sed -i 's/version: [0-9.]*/version: '$version'./' appveyor.yml + sed -i 's/LUAROCKS_VER: [0-9.]*/LUAROCKS_VER: '$version'/' appveyor.yml + sed -i 's/vars.VERSION = "[0-9.]*"/vars.VERSION = "'$xyversion'"/' install.bat + echo "===============================================================================" + git diff + echo "===============================================================================" + echo "Does the change look alright? Press 'y' to commit" + echo "===============================================================================" + read + if [ "$REPLY" = "y" ] + then + git commit -av -m "Release $version" + fi +fi + + +[ -e "$ROCKSPEC" ] || { + echo + echo "$ROCKSPEC is missing. Please check rockspec version is correct." +} + +grep -q "LuaRocks version $version" "configure" || { + echo + echo "version in configure is incorrect. Please fix it." + exit 1 +} + +grep -q "\"$version-1\"" "$ROCKSPEC" || { + echo + echo "version in rockspec is incorrect. Please fix it." + exit 1 +} + +grep -q "program_version = \"$version\"" src/luarocks/core/cfg.lua || { + echo + echo "program_version in src/luarocks/core/cfg.lua is incorrect. Please fix it." + exit 1 +} + +grep -q "version: $version\\." appveyor.yml || { + echo + echo "version in appveyor.yml is incorrect. Please fix it." + exit 1 +} + +grep -q "LUAROCKS_VER: $version" appveyor.yml || { + echo + echo "LUAROCKS_VER in appveyor.yml is incorrect. Please fix it." + exit 1 +} + +grep -q "vars.VERSION = \"$xyversion\"" install.bat || { + echo + echo "vars.VERSION in install.bat is incorrect. Please fix it." + exit 1 +} + +fi # if ! [ "$version" = "dev" ] +#------------------------------------------------------------------------------- + +out="luarocks-$version" +rm -rf "$out" +mkdir "$out" + +git ls-files | while read i +do + if [ -f "$i" ] + then + dir=`dirname $i` + mkdir -p "$out/$dir" + cp "$i" "$out/$dir" + fi +done + +rm -rf "release-unix" "release-windows" "$out.tar.gz" "$out-win32.zip" + +mkdir "release-unix" +cp -a "$out" "release-unix" +mkdir "release-windows" +mv "$out" "release-windows/$out-win32" + +cd "release-unix/$out" +rm -rf makedist install.bat win32 .github .gitignore appveyor* .appveyor +cd .. +tar czvpf ../"$out.tar.gz" "$out" +rm -f ../"$out.tar.gz.asc" +cd .. +rm -rf "release-unix" + +cd "release-windows/$out-win32" +rm -rf makedist Makefile GNUmakefile configure .github .gitignore test appveyor* .appveyor +cd .. +zip -r ../"$out-win32.zip" "$out-win32" +rm -f ../"$out-win32.zip.asc" +cd .. +rm -rf "release-windows" + +if [ "$1" = "binary" ] +then + shift + + ./configure --lua-version=5.4 --with-lua=${LUA_DIR:-/usr} + + make binary + cd build-binary + mkdir "$out-linux-x86_64" + cp luarocks.exe "$out-linux-x86_64/luarocks" + cp luarocks-admin.exe "$out-linux-x86_64/luarocks-admin" + zip "../$out-linux-x86_64.zip" "$out-linux-x86_64"/* + cd .. + rm -f "$out-linux-x86_64.zip.asc" + + make windows-binary-32 + cd build-windows-binary-i686-w64-mingw32 + mkdir "$out-windows-32" + cp luarocks.exe "$out-windows-32/luarocks.exe" + cp luarocks-admin.exe "$out-windows-32/luarocks-admin.exe" + zip "../$out-windows-32.zip" "$out-windows-32"/* + cd .. + rm -f "$out-windows-32.zip.asc" + + make windows-binary-64 + cd build-windows-binary-x86_64-w64-mingw32 + mkdir "$out-windows-64" + cp luarocks.exe "$out-windows-64/luarocks.exe" + cp luarocks-admin.exe "$out-windows-64/luarocks-admin.exe" + zip "../$out-windows-64.zip" "$out-windows-64"/* + cd .. + rm -f "$out-windows-64.zip.asc" + +fi + +if [ "$1" = "sign" ] +then + shift + + for f in \ + $out-windows-32.zip \ + $out-windows-64.zip \ + $out-linux-x86_64.zip \ + $out-win32.zip \ + $out.tar.gz + do + [ -e "$f" -a ! -e "$f.asc" ] && gpg --armor --output "$f.asc" --detach-sign "$f" + done +fi diff --git a/lib/luarocks/internal/luarocks/src/mergerelease b/lib/luarocks/internal/luarocks/src/mergerelease new file mode 100755 index 000000000..e7c9c7683 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/mergerelease @@ -0,0 +1,33 @@ +#!/bin/sh + +[ "$1" ] || { + echo "usage.....: $0 " + echo "example...: $0 3.1.3" + echo + exit 1 +} + +v="$1" + +git show $v &> /dev/null || { + echo "There is no release branch $v" + exit 1 +} + +git show origin v$v &> /dev/null || { + echo "There is no pushed tag v$v in origin." + echo + echo "Before running this, make sure branch is tagged:" + echo " git tag -s v$v $v -m 'Release $v'" + echo " git push origin v$v" + echo + exit 1 +} + +git fetch --all +git checkout master +git diff master $v > version.diff +git merge --no-ff $v +patch -R -p1 < version.diff +git add luarocks-dev-1.rockspec +git commit -av --amend diff --git a/lib/luarocks/internal/luarocks/src/publishrelease b/lib/luarocks/internal/luarocks/src/publishrelease new file mode 100755 index 000000000..371450920 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/publishrelease @@ -0,0 +1,213 @@ +#!/usr/bin/env bash + +[ "$1" ] || { + echo "usage.....: $0 " + echo "example...: $0 3.1.1" + echo + echo "Before running this, make sure the packages were built:" + echo " makedist 3.1.1 binary sign" + echo "And the tag was merged:" + echo " mergerelease 3.1.1" + echo + exit 1 +} + +####################################### +# preliminary checks +####################################### + +v="$1" + +git checkout v$v || { + echo "Could not checkout release tag." +} + +packages=( + luarocks-$v-windows-32.zip + luarocks-$v-windows-32.zip.asc + luarocks-$v-windows-64.zip + luarocks-$v-windows-64.zip.asc + luarocks-$v-linux-x86_64.zip + luarocks-$v-linux-x86_64.zip.asc + luarocks-$v-win32.zip + luarocks-$v-win32.zip.asc + luarocks-$v.tar.gz + luarocks-$v.tar.gz.asc +) + +for f in "${packages[@]}" luarocks-$v-1.rockspec +do + [ -e "$f" ] || { + echo "Missing file $f" + exit 1 + } +done + +####################################### +# utility +####################################### + +function confirm() { + branch="$1" + + echo "****************************************" + git diff $branch + echo "****************************************" + git status + echo "****************************************" + + echo "Everything looks all right? (y/n)" + echo "(Answering y will commit and push)" + read + if ! [ "$REPLY" == "y" ] + then + git reset + git checkout . + git checkout master + exit 1 + fi +} + +####################################### +# luarocks.org +####################################### + +luarocks upload luarocks-$v-1.rockspec + +####################################### +# gh-pages +####################################### + +git checkout gh-pages +git fetch origin gh-pages +git reset --hard origin/gh-pages + +cp "${packages[@]}" releases +cd releases +git add "${packages[@]}" +gawk ' +/add new release here/ { + print "" + print "" + print "luarocks-'$v'.tar.gzPGP signature" + print "luarocks-'$v'-windows-32.zip (luarocks.exe stand-alone Windows 32-bit binary)PGP signature" + print "luarocks-'$v'-windows-64.zip (luarocks.exe stand-alone Windows 64-bit binary)PGP signature" + print "luarocks-'$v'-linux-x86_64.zip (luarocks stand-alone Linux x86_64 binary)PGP signature" + print "luarocks-'$v'-win32.zip (legacy Windows package, includes Lua 5.1)PGP signature" + done = 1 +} +// { + if (done == 1) { + done = 0 + } else { + print + } +} +' index.html > index.html.1 +mv index.html.1 index.html +git add index.html + +gawk ' +/^\[$/ { + go = 1 +} +// { + print + if (go == 1) { + go = 0 + + print "{" + print "\"'$v'\": {" + print "\"date\": \"'$(date +'%Y-%m-%d')'\"," + print "\"files\": [\"luarocks-'$v'.tar.gz\", \"luarocks-'$v'.tar.gz.asc\", \"luarocks-'$v'-win32.zip\", \"luarocks-'$v'-win32.zip.asc\", \"luarocks-'$v'-windows-32.zip\", \"luarocks-'$v'-windows-32.zip.asc\", \"luarocks-'$v'-windows-64.zip\", \"luarocks-'$v'-windows-64.zip.asc\", \"luarocks-'$v'-linux-x86_64.zip\", \"luarocks-'$v'-linux-x86_64.zip.asc\"]," + print "\"about\": []" + print "}}," + } +} +' releases.json > releases.json.1 +mv releases.json.1 releases.json +git add releases.json + +confirm gh-pages + +git commit -av -m "Release $v" +git push + +####################################### +# luarocks.org +####################################### + +git checkout v$v + +luarocks upload luarocks-$v-1.rockspec + +git checkout master + +####################################### +# luarocks-site +####################################### + +if [ -e ../luarocks-site ] +then + cd ../luarocks-site + git pull +else + cd .. + git clone ssh://git@github.com/luarocks/luarocks-site + cd luarocks-site +fi + +sed -i 's,luarocks-[0-9]*\.[0-9]*\.[0-9]*,luarocks-'$v',' static/md/home.md +git add static/md/home.md + +confirm master + +git commit static/md/home.md -m "update front page for LuaRocks $v" +git push + +####################################### +# luarocks.wiki +####################################### + +[ -e ../luarocks.wiki ] || { + cd .. + git clone ssh://git@github.com/luarocks/luarocks.wiki.git +} + +if [ -e ../luarocks.wiki ] +then + cd ../luarocks.wiki + git pull +else + cd .. + git clone ssh://git@github.com/luarocks/luarocks.wiki.git + cd luarocks.wiki +fi + +sed -i "s,Latest release: .*,Latest release: '''LuaRocks $v''' - '$(date +'%d/%b/%Y')'," Download.mediawiki + +sed -i "s,/luarocks-[0-9.]*[0-9],/luarocks-$v,g" Download.mediawiki + +gawk ' +BEGIN { + print "'\'\'\''Version '$v\'\'\'' - '$(date +'%d/%b/%Y')' - [http://luarocks.org/releases/luarocks-'$v'.tar.gz Source tarball for Unix] -" + print "[http://luarocks.org/releases/luarocks-'$v'-windows-32.zip Windows binary (32-bit)] -" + print "[http://luarocks.org/releases/luarocks-'$v'-windows-64.zip Windows binary (64-bit)] -" + print "[http://luarocks.org/releases/luarocks-'$v'-linux-x86_64.zip Linux binary (x86_64)] -" + print "[http://luarocks.github.io/luarocks/releases other files]" + print "" +} +// { + print +} +' "Release-history.mediawiki" > "Release-history.mediawiki.1" +mv "Release-history.mediawiki.1" "Release-history.mediawiki" + +git add "Download.mediawiki" +git add "Release-history.mediawiki" +git add "Installation-instructions-for-Unix.md" + +confirm master + +git commit -av -m "Release $v" +git push diff --git a/lib/luarocks/internal/luarocks/src/smoke_test.sh b/lib/luarocks/internal/luarocks/src/smoke_test.sh new file mode 100755 index 000000000..c8df28818 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/smoke_test.sh @@ -0,0 +1,113 @@ +#!/bin/sh -e + +tarball="$1" + +rm -rf smoketestdir +mkdir smoketestdir +cp "$tarball" smoketestdir +cd smoketestdir + +################################################################################ +# test installation with make install +################################################################################ + +tar zxvpf "$(basename "$tarball")" +cd "$(basename "$tarball" .tar.gz)" +./configure --prefix=foobar +make +./luarocks --verbose +./luarocks --verbose install inspect +./luarocks --verbose show inspect +./lua -e 'print(assert(require("inspect")(_G)))' +./luarocks --verbose remove inspect +make install +cd foobar +bin/luarocks --verbose +bin/luarocks --verbose install inspect +bin/luarocks --verbose show inspect +( + eval $(bin/luarocks path) + lua -e 'print(assert(require("inspect")(_G)))' +) +bin/luarocks --verbose remove inspect +cd .. +rm -rf foobar + +################################################################################ +# test installation with make bootstrap +################################################################################ + +./configure --prefix=fooboot +make bootstrap +./luarocks --verbose +./luarocks --verbose install inspect +./luarocks --verbose show inspect +./lua -e 'print(assert(require("inspect")(_G)))' +./luarocks --verbose remove inspect +cd fooboot +bin/luarocks --verbose +bin/luarocks --verbose install inspect +bin/luarocks --verbose show inspect +( + eval $(bin/luarocks path) + lua -e 'print(assert(require("inspect")(_G)))' +) +bin/luarocks --verbose remove inspect +cd .. +rm -rf fooboot + +################################################################################ +# test installation with luarocks install +################################################################################ + +./configure --prefix=foorock +make bootstrap +./luarocks make --pack-binary-rock +cd foorock +bin/luarocks install ../luarocks-*-1.all.rock +bin/luarocks --verbose +bin/luarocks --verbose install inspect +bin/luarocks --verbose show inspect +bin/luarocks install ../luarocks-*-1.all.rock --tree=../foorock2 +bin/luarocks --verbose remove inspect +cd ../foorock2 +bin/luarocks --verbose +bin/luarocks --verbose install inspect +bin/luarocks --verbose show inspect +( + eval $(bin/luarocks path) + lua -e 'print(assert(require("inspect")(_G)))' +) +bin/luarocks --verbose remove inspect +cd .. +rm -rf foorock +rm -rf foorock2 + +################################################################################ + +if [ "$2" = "binary" ] +then + make binary + make install-binary + cd foobar + bin/luarocks + bin/luarocks install inspect + bin/luarocks show inspect + ( + eval $(bin/luarocks path) + lua -e 'print(assert(require("inspect")(_G)))' + ) + cd .. + rm -rf foobar +fi + +if [ "$3" = "windows" ] +then + make windows-binary +fi + +cd .. +rm -rf smoketestdir +echo +echo "Full test ran and nothing caught fire!" +echo diff --git a/lib/luarocks/internal/luarocks/src/src/bin/luarocks b/lib/luarocks/internal/luarocks/src/src/bin/luarocks new file mode 100755 index 000000000..56caaa603 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/bin/luarocks @@ -0,0 +1,35 @@ +#!/usr/bin/env lua + +-- Load cfg first so that the loader knows it is running inside LuaRocks +local cfg = require("luarocks.core.cfg") + +local loader = require("luarocks.loader") +local cmd = require("luarocks.cmd") + +local description = "LuaRocks main command-line interface" + +local commands = { + init = "luarocks.cmd.init", + pack = "luarocks.cmd.pack", + unpack = "luarocks.cmd.unpack", + build = "luarocks.cmd.build", + install = "luarocks.cmd.install", + search = "luarocks.cmd.search", + list = "luarocks.cmd.list", + remove = "luarocks.cmd.remove", + make = "luarocks.cmd.make", + download = "luarocks.cmd.download", + path = "luarocks.cmd.path", + show = "luarocks.cmd.show", + new_version = "luarocks.cmd.new_version", + lint = "luarocks.cmd.lint", + write_rockspec = "luarocks.cmd.write_rockspec", + purge = "luarocks.cmd.purge", + doc = "luarocks.cmd.doc", + upload = "luarocks.cmd.upload", + config = "luarocks.cmd.config", + which = "luarocks.cmd.which", + test = "luarocks.cmd.test", +} + +cmd.run_command(description, commands, "luarocks.cmd.external", ...) diff --git a/lib/luarocks/internal/luarocks/src/src/bin/luarocks-admin b/lib/luarocks/internal/luarocks/src/src/bin/luarocks-admin new file mode 100755 index 000000000..4a85e45be --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/bin/luarocks-admin @@ -0,0 +1,18 @@ +#!/usr/bin/env lua + +-- Load cfg first so that luarocks.loader knows it is running inside LuaRocks +local cfg = require("luarocks.core.cfg") + +local loader = require("luarocks.loader") +local cmd = require("luarocks.cmd") + +local description = "LuaRocks repository administration interface" + +local commands = { + make_manifest = "luarocks.admin.cmd.make_manifest", + add = "luarocks.admin.cmd.add", + remove = "luarocks.admin.cmd.remove", + refresh_cache = "luarocks.admin.cmd.refresh_cache", +} + +cmd.run_command(description, commands, "luarocks.admin.cmd.external", ...) diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cache.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cache.lua new file mode 100644 index 000000000..10b273eaf --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cache.lua @@ -0,0 +1,88 @@ + +--- Module handling the LuaRocks local cache. +-- Adds a rock or rockspec to a rocks server. +local cache = {} + +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local util = require("luarocks.util") + +function cache.get_upload_server(server) + if not server then server = cfg.upload_server end + if not server then + return nil, "No server specified and no default configured with upload_server." + end + return server, cfg.upload_servers and cfg.upload_servers[server] +end + +function cache.get_server_urls(server, upload_server) + local download_url = server + local login_url = nil + if upload_server then + if upload_server.rsync then download_url = "rsync://"..upload_server.rsync + elseif upload_server.http then download_url = "http://"..upload_server.http + elseif upload_server.ftp then download_url = "ftp://"..upload_server.ftp + end + + if upload_server.ftp then login_url = "ftp://"..upload_server.ftp + elseif upload_server.sftp then login_url = "sftp://"..upload_server.sftp + end + end + return download_url, login_url +end + +function cache.split_server_url(url, user, password) + local protocol, server_path = dir.split_url(url) + if protocol == "file" then + server_path = fs.absolute_name(server_path) + elseif server_path:match("@") then + local credentials + credentials, server_path = server_path:match("([^@]*)@(.*)") + if credentials:match(":") then + user, password = credentials:match("([^:]*):(.*)") + else + user = credentials + end + end + local local_cache = cfg.local_cache .. "/" .. server_path:gsub("[\\/]", "_") + return local_cache, protocol, server_path, user, password +end + +local function download_cache(protocol, server_path, user, password) + os.remove("index.html") + -- TODO abstract away explicit 'wget' call + if protocol == "rsync" then + local srv, path = server_path:match("([^/]+)(/.+)") + return fs.execute(cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." -e ssh "..user.."@"..srv..":"..path.."/ ./") + elseif protocol == "file" then + return fs.copy_contents(server_path, ".") + else + local login_info = "" + if user then login_info = " --user="..user end + if password then login_info = login_info .. " --password="..password end + return fs.execute(cfg.variables.WGET.." --no-cache -q -m -np -nd "..protocol.."://"..server_path..login_info) + end +end + +function cache.refresh_local_cache(url, given_user, given_password) + local local_cache, protocol, server_path, user, password = cache.split_server_url(url, given_user, given_password) + + local ok, err = fs.make_dir(local_cache) + if not ok then + return nil, "Failed creating local cache dir: "..err + end + + fs.change_dir(local_cache) + + util.printout("Refreshing cache "..local_cache.."...") + + ok = download_cache(protocol, server_path, user, password) + if not ok then + return nil, "Failed downloading cache." + end + + return local_cache, protocol, server_path, user, password +end + +return cache diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/add.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/add.lua new file mode 100644 index 000000000..aa444c506 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/add.lua @@ -0,0 +1,134 @@ + +--- Module implementing the luarocks-admin "add" command. +-- Adds a rock or rockspec to a rocks server. +local add = {} + +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local writer = require("luarocks.manif.writer") +local fs = require("luarocks.fs") +local cache = require("luarocks.admin.cache") +local index = require("luarocks.admin.index") + +function add.add_to_parser(parser) + local cmd = parser:command("add", "Add a rock or rockspec to a rocks server.", util.see_also()) + + cmd:argument("rock", "A local rockspec or rock file.") + :args("+") + + cmd:option("--server", "The server to use. If not given, the default server ".. + "set in the upload_server variable from the configuration file is used instead.") + :target("add_server") + cmd:flag("--no-refresh", "Do not refresh the local cache prior to ".. + "generation of the updated manifest.") + cmd:flag("--index", "Produce an index.html file for the manifest. This ".. + "flag is automatically set if an index.html file already exists.") +end + +local function zip_manifests() + for ver in util.lua_versions() do + local file = "manifest-"..ver + local zip = file..".zip" + fs.delete(dir.path(fs.current_dir(), zip)) + fs.zip(zip, file) + end +end + +local function add_files_to_server(refresh, rockfiles, server, upload_server, do_index) + assert(type(refresh) == "boolean" or not refresh) + assert(type(rockfiles) == "table") + assert(type(server) == "string") + assert(type(upload_server) == "table" or not upload_server) + + local download_url, login_url = cache.get_server_urls(server, upload_server) + local at = fs.current_dir() + local refresh_fn = refresh and cache.refresh_local_cache or cache.split_server_url + + local local_cache, protocol, server_path, user, password = refresh_fn(download_url, cfg.upload_user, cfg.upload_password) + if not local_cache then + return nil, protocol + end + + if not login_url then + login_url = protocol.."://"..server_path + end + + local ok, err = fs.change_dir(at) + if not ok then return nil, err end + + local files = {} + for _, rockfile in ipairs(rockfiles) do + if fs.exists(rockfile) then + util.printout("Copying file "..rockfile.." to "..local_cache.."...") + local absolute = fs.absolute_name(rockfile) + fs.copy(absolute, local_cache, "read") + table.insert(files, dir.base_name(absolute)) + else + util.printerr("File "..rockfile.." not found") + end + end + if #files == 0 then + return nil, "No files found" + end + + local ok, err = fs.change_dir(local_cache) + if not ok then return nil, err end + + util.printout("Updating manifest...") + writer.make_manifest(local_cache, "one", true) + + zip_manifests() + + if fs.exists("index.html") then + do_index = true + end + + if do_index then + util.printout("Updating index.html...") + index.make_index(local_cache) + end + + local login_info = "" + if user then login_info = " -u "..user end + if password then login_info = login_info..":"..password end + if not login_url:match("/$") then + login_url = login_url .. "/" + end + + if do_index then + table.insert(files, "index.html") + end + table.insert(files, "manifest") + for ver in util.lua_versions() do + table.insert(files, "manifest-"..ver) + table.insert(files, "manifest-"..ver..".zip") + end + + -- TODO abstract away explicit 'curl' call + + local cmd + if protocol == "rsync" then + local srv, path = server_path:match("([^/]+)(/.+)") + cmd = cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." -e ssh "..local_cache.."/ "..user.."@"..srv..":"..path.."/" + elseif protocol == "file" then + return fs.copy_contents(local_cache, server_path) + elseif upload_server and upload_server.sftp then + local part1, part2 = upload_server.sftp:match("^([^/]*)/(.*)$") + cmd = cfg.variables.SCP.." "..table.concat(files, " ").." "..user.."@"..part1..":/"..part2 + else + cmd = cfg.variables.CURL.." "..login_info.." -T '{"..table.concat(files, ",").."}' "..login_url + end + + util.printout(cmd) + return fs.execute(cmd) +end + +function add.command(args) + local server, server_table = cache.get_upload_server(args.add_server or args.server) + if not server then return nil, server_table end + return add_files_to_server(not args.no_refresh, args.rock, server, server_table, args.index) +end + + +return add diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/make_manifest.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/make_manifest.lua new file mode 100644 index 000000000..18f74b5db --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/make_manifest.lua @@ -0,0 +1,50 @@ + +--- Module implementing the luarocks-admin "make_manifest" command. +-- Compile a manifest file for a repository. +local make_manifest = {} + +local writer = require("luarocks.manif.writer") +local index = require("luarocks.admin.index") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local deps = require("luarocks.deps") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") + +function make_manifest.add_to_parser(parser) + local cmd = parser:command("make_manifest", "Compile a manifest file for a repository.", util.see_also()) + + cmd:argument("repository", "Local repository pathname.") + :args("?") + + cmd:flag("--local-tree", "If given, do not write versioned versions of the manifest file.\n".. + "Use this when rebuilding the manifest of a local rocks tree.") + util.deps_mode_option(cmd) +end + +--- Driver function for "make_manifest" command. +-- @return boolean or (nil, string): True if manifest was generated, +-- or nil and an error message. +function make_manifest.command(args) + local repo = args.repository or cfg.rocks_dir + + util.printout("Making manifest for "..repo) + + if repo:match("/lib/luarocks") and not args.local_tree then + util.warning("This looks like a local rocks tree, but you did not pass --local-tree.") + end + + local ok, err = writer.make_manifest(repo, deps.get_deps_mode(args), not args.local_tree) + if ok and not args.local_tree then + util.printout("Generating index.html for "..repo) + index.make_index(repo) + end + if args.local_tree then + for luaver in util.lua_versions() do + fs.delete(dir.path(repo, "manifest-"..luaver)) + end + end + return ok, err +end + +return make_manifest diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/refresh_cache.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/refresh_cache.lua new file mode 100644 index 000000000..f8d51893f --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/refresh_cache.lua @@ -0,0 +1,31 @@ + +--- Module implementing the luarocks-admin "refresh_cache" command. +local refresh_cache = {} + +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local cache = require("luarocks.admin.cache") + +function refresh_cache.add_to_parser(parser) + local cmd = parser:command("refresh_cache", "Refresh local cache of a remote rocks server.", util.see_also()) + + cmd:option("--from", "The server to use. If not given, the default server ".. + "set in the upload_server variable from the configuration file is used instead.") + :argname("") +end + +function refresh_cache.command(args) + local server, upload_server = cache.get_upload_server(args.server) + if not server then return nil, upload_server end + local download_url = cache.get_server_urls(server, upload_server) + + local ok, err = cache.refresh_local_cache(download_url, cfg.upload_user, cfg.upload_password) + if not ok then + return nil, err + else + return true + end +end + + +return refresh_cache diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/remove.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/remove.lua new file mode 100644 index 000000000..ed7644e03 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/cmd/remove.lua @@ -0,0 +1,95 @@ + +--- Module implementing the luarocks-admin "remove" command. +-- Removes a rock or rockspec from a rocks server. +local admin_remove = {} + +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local writer = require("luarocks.manif.writer") +local fs = require("luarocks.fs") +local cache = require("luarocks.admin.cache") +local index = require("luarocks.admin.index") + +function admin_remove.add_to_parser(parser) + local cmd = parser:command("remove", "Remove a rock or rockspec from a rocks server.", util.see_also()) + + cmd:argument("rock", "A local rockspec or rock file.") + :args("+") + + cmd:option("--server", "The server to use. If not given, the default server ".. + "set in the upload_server variable from the configuration file is used instead.") + cmd:flag("--no-refresh", "Do not refresh the local cache prior to ".. + "generation of the updated manifest.") +end + +local function remove_files_from_server(refresh, rockfiles, server, upload_server) + assert(type(refresh) == "boolean" or not refresh) + assert(type(rockfiles) == "table") + assert(type(server) == "string") + assert(type(upload_server) == "table" or not upload_server) + + local download_url, login_url = cache.get_server_urls(server, upload_server) + local at = fs.current_dir() + local refresh_fn = refresh and cache.refresh_local_cache or cache.split_server_url + + local local_cache, protocol, server_path, user, password = refresh_fn(download_url, cfg.upload_user, cfg.upload_password) + if not local_cache then + return nil, protocol + end + + local ok, err = fs.change_dir(at) + if not ok then return nil, err end + + local nr_files = 0 + for _, rockfile in ipairs(rockfiles) do + local basename = dir.base_name(rockfile) + local file = dir.path(local_cache, basename) + util.printout("Removing file "..file.."...") + fs.delete(file) + if not fs.exists(file) then + nr_files = nr_files + 1 + else + util.printerr("Failed removing "..file) + end + end + if nr_files == 0 then + return nil, "No files removed." + end + + local ok, err = fs.change_dir(local_cache) + if not ok then return nil, err end + + util.printout("Updating manifest...") + writer.make_manifest(local_cache, "one", true) + util.printout("Updating index.html...") + index.make_index(local_cache) + + if protocol == "file" then + local cmd = cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." --delete "..local_cache.."/ ".. server_path.."/" + util.printout(cmd) + fs.execute(cmd) + return true + end + + if protocol ~= "rsync" then + return nil, "This command requires 'rsync', check your configuration." + end + + local srv, path = server_path:match("([^/]+)(/.+)") + local cmd = cfg.variables.RSYNC.." "..cfg.variables.RSYNCFLAGS.." --delete -e ssh "..local_cache.."/ "..user.."@"..srv..":"..path.."/" + + util.printout(cmd) + fs.execute(cmd) + + return true +end + +function admin_remove.command(args) + local server, server_table = cache.get_upload_server(args.server) + if not server then return nil, server_table end + return remove_files_from_server(not args.no_refresh, args.rock, server, server_table) +end + + +return admin_remove diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/admin/index.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/index.lua new file mode 100644 index 000000000..64c8c1eb3 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/admin/index.lua @@ -0,0 +1,185 @@ + +--- Module which builds the index.html page to be used in rocks servers. +local index = {} + +local util = require("luarocks.util") +local fs = require("luarocks.fs") +local vers = require("luarocks.core.vers") +local persist = require("luarocks.persist") +local dir = require("luarocks.dir") +local manif = require("luarocks.manif") + +local ext_url_target = ' target="_blank"' + +local index_header = [[ + + + +Available rocks + + + + +

Available rocks

+

+Lua modules available from this location for use with LuaRocks: +

+ +]] + +local index_package_begin = [[ + + + +]] + +local index_footer_begin = [[ +
+

$package - $summary
+

$detailed
+$externaldependencies +latest sources $homepage | License: $license

+
+]] + +local index_package_end = [[ +
+

+manifest file +]] +local index_manifest_ver = [[ +• Lua $VER manifest file (zip) +]] +local index_footer_end = [[ +

+ + +]] + +function index.format_external_dependencies(rockspec) + if rockspec.external_dependencies then + local deplist = {} + local listed_set = {} + local plats = nil + for name, desc in util.sortedpairs(rockspec.external_dependencies) do + if name ~= "platforms" then + table.insert(deplist, name:lower()) + listed_set[name] = true + else + plats = desc + end + end + if plats then + for plat, entries in util.sortedpairs(plats) do + for name, desc in util.sortedpairs(entries) do + if not listed_set[name] then + table.insert(deplist, name:lower() .. " (on "..plat..")") + end + end + end + end + return '

External dependencies: ' .. table.concat(deplist, ', ').. '

' + else + return "" + end +end + +function index.make_index(repo) + if not fs.is_dir(repo) then + return nil, "Cannot access repository at "..repo + end + local manifest = manif.load_manifest(repo) + local out = io.open(dir.path(repo, "index.html"), "w") + + out:write(index_header) + for package, version_list in util.sortedpairs(manifest.repository) do + local latest_rockspec = nil + local output = index_package_begin + for version, data in util.sortedpairs(version_list, vers.compare_versions) do + local versions = {} + output = output..version..': ' + table.sort(data, function(a,b) return a.arch < b.arch end) + for _, item in ipairs(data) do + local file + if item.arch == 'rockspec' then + file = ("%s-%s.rockspec"):format(package, version) + if not latest_rockspec then latest_rockspec = file end + else + file = ("%s-%s.%s.rock"):format(package, version, item.arch) + end + table.insert(versions, ''..item.arch..'') + end + output = output .. table.concat(versions, ', ') .. '
' + end + output = output .. index_package_end + if latest_rockspec then + local rockspec = persist.load_into_table(dir.path(repo, latest_rockspec)) + local descript = rockspec.description or {} + local vars = { + anchor = package, + package = rockspec.package, + original = rockspec.source.url, + summary = descript.summary or "", + detailed = descript.detailed or "", + license = descript.license or "N/A", + homepage = descript.homepage and ('| project homepage') or "", + externaldependencies = index.format_external_dependencies(rockspec) + } + vars.detailed = vars.detailed:gsub("\n\n", "

"):gsub("%s+", " ") + vars.detailed = vars.detailed:gsub("(https?://[a-zA-Z0-9%.%%-_%+%[%]=%?&/$@;:]+)", '%1') + output = output:gsub("$(%w+)", vars) + else + output = output:gsub("$anchor", package) + output = output:gsub("$package", package) + output = output:gsub("$(%w+)", "") + end + out:write(output) + end + out:write(index_footer_begin) + for ver in util.lua_versions() do + out:write((index_manifest_ver:gsub("$VER", ver))) + end + out:write(index_footer_end) + out:close() +end + +return index diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/argparse.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/argparse.lua new file mode 100644 index 000000000..2c2585ddb --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/argparse.lua @@ -0,0 +1,2103 @@ +-- The MIT License (MIT) + +-- Copyright (c) 2013 - 2018 Peter Melnichenko +-- 2019 Paul Ouellette + +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +-- the Software, and to permit persons to whom the Software is furnished to do so, +-- subject to the following conditions: + +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. + +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +-- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +-- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +-- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +-- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +local function deep_update(t1, t2) + for k, v in pairs(t2) do + if type(v) == "table" then + v = deep_update({}, v) + end + + t1[k] = v + end + + return t1 +end + +-- A property is a tuple {name, callback}. +-- properties.args is number of properties that can be set as arguments +-- when calling an object. +local function class(prototype, properties, parent) + -- Class is the metatable of its instances. + local cl = {} + cl.__index = cl + + if parent then + cl.__prototype = deep_update(deep_update({}, parent.__prototype), prototype) + else + cl.__prototype = prototype + end + + if properties then + local names = {} + + -- Create setter methods and fill set of property names. + for _, property in ipairs(properties) do + local name, callback = property[1], property[2] + + cl[name] = function(self, value) + if not callback(self, value) then + self["_" .. name] = value + end + + return self + end + + names[name] = true + end + + function cl.__call(self, ...) + -- When calling an object, if the first argument is a table, + -- interpret keys as property names, else delegate arguments + -- to corresponding setters in order. + if type((...)) == "table" then + for name, value in pairs((...)) do + if names[name] then + self[name](self, value) + end + end + else + local nargs = select("#", ...) + + for i, property in ipairs(properties) do + if i > nargs or i > properties.args then + break + end + + local arg = select(i, ...) + + if arg ~= nil then + self[property[1]](self, arg) + end + end + end + + return self + end + end + + -- If indexing class fails, fallback to its parent. + local class_metatable = {} + class_metatable.__index = parent + + function class_metatable.__call(self, ...) + -- Calling a class returns its instance. + -- Arguments are delegated to the instance. + local object = deep_update({}, self.__prototype) + setmetatable(object, self) + return object(...) + end + + return setmetatable(cl, class_metatable) +end + +local function typecheck(name, types, value) + for _, type_ in ipairs(types) do + if type(value) == type_ then + return true + end + end + + error(("bad property '%s' (%s expected, got %s)"):format(name, table.concat(types, " or "), type(value))) +end + +local function typechecked(name, ...) + local types = {...} + return {name, function(_, value) typecheck(name, types, value) end} +end + +local multiname = {"name", function(self, value) + typecheck("name", {"string"}, value) + + for alias in value:gmatch("%S+") do + self._name = self._name or alias + table.insert(self._aliases, alias) + table.insert(self._public_aliases, alias) + -- If alias contains '_', accept '-' also. + if alias:find("_", 1, true) then + table.insert(self._aliases, (alias:gsub("_", "-"))) + end + end + + -- Do not set _name as with other properties. + return true +end} + +local multiname_hidden = {"hidden_name", function(self, value) + typecheck("hidden_name", {"string"}, value) + + for alias in value:gmatch("%S+") do + table.insert(self._aliases, alias) + if alias:find("_", 1, true) then + table.insert(self._aliases, (alias:gsub("_", "-"))) + end + end + + return true +end} + +local function parse_boundaries(str) + if tonumber(str) then + return tonumber(str), tonumber(str) + end + + if str == "*" then + return 0, math.huge + end + + if str == "+" then + return 1, math.huge + end + + if str == "?" then + return 0, 1 + end + + if str:match "^%d+%-%d+$" then + local min, max = str:match "^(%d+)%-(%d+)$" + return tonumber(min), tonumber(max) + end + + if str:match "^%d+%+$" then + local min = str:match "^(%d+)%+$" + return tonumber(min), math.huge + end +end + +local function boundaries(name) + return {name, function(self, value) + typecheck(name, {"number", "string"}, value) + + local min, max = parse_boundaries(value) + + if not min then + error(("bad property '%s'"):format(name)) + end + + self["_min" .. name], self["_max" .. name] = min, max + end} +end + +local actions = {} + +local option_action = {"action", function(_, value) + typecheck("action", {"function", "string"}, value) + + if type(value) == "string" and not actions[value] then + error(("unknown action '%s'"):format(value)) + end +end} + +local option_init = {"init", function(self) + self._has_init = true +end} + +local option_default = {"default", function(self, value) + if type(value) ~= "string" then + self._init = value + self._has_init = true + return true + end +end} + +local add_help = {"add_help", function(self, value) + typecheck("add_help", {"boolean", "string", "table"}, value) + + if self._help_option_idx then + table.remove(self._options, self._help_option_idx) + self._help_option_idx = nil + end + + if value then + local help = self:flag() + :description "Show this help message and exit." + :action(function() + print(self:get_help()) + os.exit(0) + end) + + if value ~= true then + help = help(value) + end + + if not help._name then + help "-h" "--help" + end + + self._help_option_idx = #self._options + end +end} + +local Parser = class({ + _arguments = {}, + _options = {}, + _commands = {}, + _mutexes = {}, + _groups = {}, + _require_command = true, + _handle_options = true +}, { + args = 3, + typechecked("name", "string"), + typechecked("description", "string"), + typechecked("epilog", "string"), + typechecked("usage", "string"), + typechecked("help", "string"), + typechecked("require_command", "boolean"), + typechecked("handle_options", "boolean"), + typechecked("action", "function"), + typechecked("command_target", "string"), + typechecked("help_vertical_space", "number"), + typechecked("usage_margin", "number"), + typechecked("usage_max_width", "number"), + typechecked("help_usage_margin", "number"), + typechecked("help_description_margin", "number"), + typechecked("help_max_width", "number"), + add_help +}) + +local Command = class({ + _aliases = {}, + _public_aliases = {} +}, { + args = 3, + multiname, + typechecked("description", "string"), + typechecked("epilog", "string"), + multiname_hidden, + typechecked("summary", "string"), + typechecked("target", "string"), + typechecked("usage", "string"), + typechecked("help", "string"), + typechecked("require_command", "boolean"), + typechecked("handle_options", "boolean"), + typechecked("action", "function"), + typechecked("command_target", "string"), + typechecked("help_vertical_space", "number"), + typechecked("usage_margin", "number"), + typechecked("usage_max_width", "number"), + typechecked("help_usage_margin", "number"), + typechecked("help_description_margin", "number"), + typechecked("help_max_width", "number"), + typechecked("hidden", "boolean"), + add_help +}, Parser) + +local Argument = class({ + _minargs = 1, + _maxargs = 1, + _mincount = 1, + _maxcount = 1, + _defmode = "unused", + _show_default = true +}, { + args = 5, + typechecked("name", "string"), + typechecked("description", "string"), + option_default, + typechecked("convert", "function", "table"), + boundaries("args"), + typechecked("target", "string"), + typechecked("defmode", "string"), + typechecked("show_default", "boolean"), + typechecked("argname", "string", "table"), + typechecked("choices", "table"), + typechecked("hidden", "boolean"), + option_action, + option_init +}) + +local Option = class({ + _aliases = {}, + _public_aliases = {}, + _mincount = 0, + _overwrite = true +}, { + args = 6, + multiname, + typechecked("description", "string"), + option_default, + typechecked("convert", "function", "table"), + boundaries("args"), + boundaries("count"), + multiname_hidden, + typechecked("target", "string"), + typechecked("defmode", "string"), + typechecked("show_default", "boolean"), + typechecked("overwrite", "boolean"), + typechecked("argname", "string", "table"), + typechecked("choices", "table"), + typechecked("hidden", "boolean"), + option_action, + option_init +}, Argument) + +function Parser:_inherit_property(name, default) + local element = self + + while true do + local value = element["_" .. name] + + if value ~= nil then + return value + end + + if not element._parent then + return default + end + + element = element._parent + end +end + +function Argument:_get_argument_list() + local buf = {} + local i = 1 + + while i <= math.min(self._minargs, 3) do + local argname = self:_get_argname(i) + + if self._default and self._defmode:find "a" then + argname = "[" .. argname .. "]" + end + + table.insert(buf, argname) + i = i+1 + end + + while i <= math.min(self._maxargs, 3) do + table.insert(buf, "[" .. self:_get_argname(i) .. "]") + i = i+1 + + if self._maxargs == math.huge then + break + end + end + + if i < self._maxargs then + table.insert(buf, "...") + end + + return buf +end + +function Argument:_get_usage() + local usage = table.concat(self:_get_argument_list(), " ") + + if self._default and self._defmode:find "u" then + if self._maxargs > 1 or (self._minargs == 1 and not self._defmode:find "a") then + usage = "[" .. usage .. "]" + end + end + + return usage +end + +function actions.store_true(result, target) + result[target] = true +end + +function actions.store_false(result, target) + result[target] = false +end + +function actions.store(result, target, argument) + result[target] = argument +end + +function actions.count(result, target, _, overwrite) + if not overwrite then + result[target] = result[target] + 1 + end +end + +function actions.append(result, target, argument, overwrite) + result[target] = result[target] or {} + table.insert(result[target], argument) + + if overwrite then + table.remove(result[target], 1) + end +end + +function actions.concat(result, target, arguments, overwrite) + if overwrite then + error("'concat' action can't handle too many invocations") + end + + result[target] = result[target] or {} + + for _, argument in ipairs(arguments) do + table.insert(result[target], argument) + end +end + +function Argument:_get_action() + local action, init + + if self._maxcount == 1 then + if self._maxargs == 0 then + action, init = "store_true", nil + else + action, init = "store", nil + end + else + if self._maxargs == 0 then + action, init = "count", 0 + else + action, init = "append", {} + end + end + + if self._action then + action = self._action + end + + if self._has_init then + init = self._init + end + + if type(action) == "string" then + action = actions[action] + end + + return action, init +end + +-- Returns placeholder for `narg`-th argument. +function Argument:_get_argname(narg) + local argname = self._argname or self:_get_default_argname() + + if type(argname) == "table" then + return argname[narg] + else + return argname + end +end + +function Argument:_get_choices_list() + return "{" .. table.concat(self._choices, ",") .. "}" +end + +function Argument:_get_default_argname() + if self._choices then + return self:_get_choices_list() + else + return "<" .. self._name .. ">" + end +end + +function Option:_get_default_argname() + if self._choices then + return self:_get_choices_list() + else + return "<" .. self:_get_default_target() .. ">" + end +end + +-- Returns labels to be shown in the help message. +function Argument:_get_label_lines() + if self._choices then + return {self:_get_choices_list()} + else + return {self._name} + end +end + +function Option:_get_label_lines() + local argument_list = self:_get_argument_list() + + if #argument_list == 0 then + -- Don't put aliases for simple flags like `-h` on different lines. + return {table.concat(self._public_aliases, ", ")} + end + + local longest_alias_length = -1 + + for _, alias in ipairs(self._public_aliases) do + longest_alias_length = math.max(longest_alias_length, #alias) + end + + local argument_list_repr = table.concat(argument_list, " ") + local lines = {} + + for i, alias in ipairs(self._public_aliases) do + local line = (" "):rep(longest_alias_length - #alias) .. alias .. " " .. argument_list_repr + + if i ~= #self._public_aliases then + line = line .. "," + end + + table.insert(lines, line) + end + + return lines +end + +function Command:_get_label_lines() + return {table.concat(self._public_aliases, ", ")} +end + +function Argument:_get_description() + if self._default and self._show_default then + if self._description then + return ("%s (default: %s)"):format(self._description, self._default) + else + return ("default: %s"):format(self._default) + end + else + return self._description or "" + end +end + +function Command:_get_description() + return self._summary or self._description or "" +end + +function Option:_get_usage() + local usage = self:_get_argument_list() + table.insert(usage, 1, self._name) + usage = table.concat(usage, " ") + + if self._mincount == 0 or self._default then + usage = "[" .. usage .. "]" + end + + return usage +end + +function Argument:_get_default_target() + return self._name +end + +function Option:_get_default_target() + local res + + for _, alias in ipairs(self._public_aliases) do + if alias:sub(1, 1) == alias:sub(2, 2) then + res = alias:sub(3) + break + end + end + + res = res or self._name:sub(2) + return (res:gsub("-", "_")) +end + +function Option:_is_vararg() + return self._maxargs ~= self._minargs +end + +function Parser:_get_fullname(exclude_root) + local parent = self._parent + if exclude_root and not parent then + return "" + end + local buf = {self._name} + + while parent do + if not exclude_root or parent._parent then + table.insert(buf, 1, parent._name) + end + parent = parent._parent + end + + return table.concat(buf, " ") +end + +function Parser:_update_charset(charset) + charset = charset or {} + + for _, command in ipairs(self._commands) do + command:_update_charset(charset) + end + + for _, option in ipairs(self._options) do + for _, alias in ipairs(option._aliases) do + charset[alias:sub(1, 1)] = true + end + end + + return charset +end + +function Parser:argument(...) + local argument = Argument(...) + table.insert(self._arguments, argument) + return argument +end + +function Parser:option(...) + local option = Option(...) + table.insert(self._options, option) + return option +end + +function Parser:flag(...) + return self:option():args(0)(...) +end + +function Parser:command(...) + local command = Command():add_help(true)(...) + command._parent = self + table.insert(self._commands, command) + return command +end + +function Parser:mutex(...) + local elements = {...} + + for i, element in ipairs(elements) do + local mt = getmetatable(element) + assert(mt == Option or mt == Argument, ("bad argument #%d to 'mutex' (Option or Argument expected)"):format(i)) + end + + table.insert(self._mutexes, elements) + return self +end + +function Parser:group(name, ...) + assert(type(name) == "string", ("bad argument #1 to 'group' (string expected, got %s)"):format(type(name))) + + local group = {name = name, ...} + + for i, element in ipairs(group) do + local mt = getmetatable(element) + assert(mt == Option or mt == Argument or mt == Command, + ("bad argument #%d to 'group' (Option or Argument or Command expected)"):format(i + 1)) + end + + table.insert(self._groups, group) + return self +end + +local usage_welcome = "Usage: " + +function Parser:get_usage() + if self._usage then + return self._usage + end + + local usage_margin = self:_inherit_property("usage_margin", #usage_welcome) + local max_usage_width = self:_inherit_property("usage_max_width", 70) + local lines = {usage_welcome .. self:_get_fullname()} + + local function add(s) + if #lines[#lines]+1+#s <= max_usage_width then + lines[#lines] = lines[#lines] .. " " .. s + else + lines[#lines+1] = (" "):rep(usage_margin) .. s + end + end + + -- Normally options are before positional arguments in usage messages. + -- However, vararg options should be after, because they can't be reliable used + -- before a positional argument. + -- Mutexes come into play, too, and are shown as soon as possible. + -- Overall, output usages in the following order: + -- 1. Mutexes that don't have positional arguments or vararg options. + -- 2. Options that are not in any mutexes and are not vararg. + -- 3. Positional arguments - on their own or as a part of a mutex. + -- 4. Remaining mutexes. + -- 5. Remaining options. + + local elements_in_mutexes = {} + local added_elements = {} + local added_mutexes = {} + local argument_to_mutexes = {} + + local function add_mutex(mutex, main_argument) + if added_mutexes[mutex] then + return + end + + added_mutexes[mutex] = true + local buf = {} + + for _, element in ipairs(mutex) do + if not element._hidden and not added_elements[element] then + if getmetatable(element) == Option or element == main_argument then + table.insert(buf, element:_get_usage()) + added_elements[element] = true + end + end + end + + if #buf == 1 then + add(buf[1]) + elseif #buf > 1 then + add("(" .. table.concat(buf, " | ") .. ")") + end + end + + local function add_element(element) + if not element._hidden and not added_elements[element] then + add(element:_get_usage()) + added_elements[element] = true + end + end + + for _, mutex in ipairs(self._mutexes) do + local is_vararg = false + local has_argument = false + + for _, element in ipairs(mutex) do + if getmetatable(element) == Option then + if element:_is_vararg() then + is_vararg = true + end + else + has_argument = true + argument_to_mutexes[element] = argument_to_mutexes[element] or {} + table.insert(argument_to_mutexes[element], mutex) + end + + elements_in_mutexes[element] = true + end + + if not is_vararg and not has_argument then + add_mutex(mutex) + end + end + + for _, option in ipairs(self._options) do + if not elements_in_mutexes[option] and not option:_is_vararg() then + add_element(option) + end + end + + -- Add usages for positional arguments, together with one mutex containing them, if they are in a mutex. + for _, argument in ipairs(self._arguments) do + -- Pick a mutex as a part of which to show this argument, take the first one that's still available. + local mutex + + if elements_in_mutexes[argument] then + for _, argument_mutex in ipairs(argument_to_mutexes[argument]) do + if not added_mutexes[argument_mutex] then + mutex = argument_mutex + end + end + end + + if mutex then + add_mutex(mutex, argument) + else + add_element(argument) + end + end + + for _, mutex in ipairs(self._mutexes) do + add_mutex(mutex) + end + + for _, option in ipairs(self._options) do + add_element(option) + end + + if #self._commands > 0 then + if self._require_command then + add("") + else + add("[]") + end + + add("...") + end + + return table.concat(lines, "\n") +end + +local function split_lines(s) + if s == "" then + return {} + end + + local lines = {} + + if s:sub(-1) ~= "\n" then + s = s .. "\n" + end + + for line in s:gmatch("([^\n]*)\n") do + table.insert(lines, line) + end + + return lines +end + +local function autowrap_line(line, max_length) + -- Algorithm for splitting lines is simple and greedy. + local result_lines = {} + + -- Preserve original indentation of the line, put this at the beginning of each result line. + -- If the first word looks like a list marker ('*', '+', or '-'), add spaces so that starts + -- of the second and the following lines vertically align with the start of the second word. + local indentation = line:match("^ *") + + if line:find("^ *[%*%+%-]") then + indentation = indentation .. " " .. line:match("^ *[%*%+%-]( *)") + end + + -- Parts of the last line being assembled. + local line_parts = {} + + -- Length of the current line. + local line_length = 0 + + -- Index of the next character to consider. + local index = 1 + + while true do + local word_start, word_finish, word = line:find("([^ ]+)", index) + + if not word_start then + -- Ignore trailing spaces, if any. + break + end + + local preceding_spaces = line:sub(index, word_start - 1) + index = word_finish + 1 + + if (#line_parts == 0) or (line_length + #preceding_spaces + #word <= max_length) then + -- Either this is the very first word or it fits as an addition to the current line, add it. + table.insert(line_parts, preceding_spaces) -- For the very first word this adds the indentation. + table.insert(line_parts, word) + line_length = line_length + #preceding_spaces + #word + else + -- Does not fit, finish current line and put the word into a new one. + table.insert(result_lines, table.concat(line_parts)) + line_parts = {indentation, word} + line_length = #indentation + #word + end + end + + if #line_parts > 0 then + table.insert(result_lines, table.concat(line_parts)) + end + + if #result_lines == 0 then + -- Preserve empty lines. + result_lines[1] = "" + end + + return result_lines +end + +-- Automatically wraps lines within given array, +-- attempting to limit line length to `max_length`. +-- Existing line splits are preserved. +local function autowrap(lines, max_length) + local result_lines = {} + + for _, line in ipairs(lines) do + local autowrapped_lines = autowrap_line(line, max_length) + + for _, autowrapped_line in ipairs(autowrapped_lines) do + table.insert(result_lines, autowrapped_line) + end + end + + return result_lines +end + +function Parser:_get_element_help(element) + local label_lines = element:_get_label_lines() + local description_lines = split_lines(element:_get_description()) + + local result_lines = {} + + -- All label lines should have the same length (except the last one, it has no comma). + -- If too long, start description after all the label lines. + -- Otherwise, combine label and description lines. + + local usage_margin_len = self:_inherit_property("help_usage_margin", 3) + local usage_margin = (" "):rep(usage_margin_len) + local description_margin_len = self:_inherit_property("help_description_margin", 25) + local description_margin = (" "):rep(description_margin_len) + + local help_max_width = self:_inherit_property("help_max_width") + + if help_max_width then + local description_max_width = math.max(help_max_width - description_margin_len, 10) + description_lines = autowrap(description_lines, description_max_width) + end + + if #label_lines[1] >= (description_margin_len - usage_margin_len) then + for _, label_line in ipairs(label_lines) do + table.insert(result_lines, usage_margin .. label_line) + end + + for _, description_line in ipairs(description_lines) do + table.insert(result_lines, description_margin .. description_line) + end + else + for i = 1, math.max(#label_lines, #description_lines) do + local label_line = label_lines[i] + local description_line = description_lines[i] + + local line = "" + + if label_line then + line = usage_margin .. label_line + end + + if description_line and description_line ~= "" then + line = line .. (" "):rep(description_margin_len - #line) .. description_line + end + + table.insert(result_lines, line) + end + end + + return table.concat(result_lines, "\n") +end + +local function get_group_types(group) + local types = {} + + for _, element in ipairs(group) do + types[getmetatable(element)] = true + end + + return types +end + +function Parser:_add_group_help(blocks, added_elements, label, elements) + local buf = {label} + + for _, element in ipairs(elements) do + if not element._hidden and not added_elements[element] then + added_elements[element] = true + table.insert(buf, self:_get_element_help(element)) + end + end + + if #buf > 1 then + table.insert(blocks, table.concat(buf, ("\n"):rep(self:_inherit_property("help_vertical_space", 0) + 1))) + end +end + +function Parser:get_help() + if self._help then + return self._help + end + + local blocks = {self:get_usage()} + + local help_max_width = self:_inherit_property("help_max_width") + + if self._description then + local description = self._description + + if help_max_width then + description = table.concat(autowrap(split_lines(description), help_max_width), "\n") + end + + table.insert(blocks, description) + end + + -- 1. Put groups containing arguments first, then other arguments. + -- 2. Put remaining groups containing options, then other options. + -- 3. Put remaining groups containing commands, then other commands. + -- Assume that an element can't be in several groups. + local groups_by_type = { + [Argument] = {}, + [Option] = {}, + [Command] = {} + } + + for _, group in ipairs(self._groups) do + local group_types = get_group_types(group) + + for _, mt in ipairs({Argument, Option, Command}) do + if group_types[mt] then + table.insert(groups_by_type[mt], group) + break + end + end + end + + local default_groups = { + {name = "Arguments", type = Argument, elements = self._arguments}, + {name = "Options", type = Option, elements = self._options}, + {name = "Commands", type = Command, elements = self._commands} + } + + local added_elements = {} + + for _, default_group in ipairs(default_groups) do + local type_groups = groups_by_type[default_group.type] + + for _, group in ipairs(type_groups) do + self:_add_group_help(blocks, added_elements, group.name .. ":", group) + end + + local default_label = default_group.name .. ":" + + if #type_groups > 0 then + default_label = "Other " .. default_label:gsub("^.", string.lower) + end + + self:_add_group_help(blocks, added_elements, default_label, default_group.elements) + end + + if self._epilog then + local epilog = self._epilog + + if help_max_width then + epilog = table.concat(autowrap(split_lines(epilog), help_max_width), "\n") + end + + table.insert(blocks, epilog) + end + + return table.concat(blocks, "\n\n") +end + +function Parser:add_help_command(value) + if value then + assert(type(value) == "string" or type(value) == "table", + ("bad argument #1 to 'add_help_command' (string or table expected, got %s)"):format(type(value))) + end + + local help = self:command() + :description "Show help for commands." + help:argument "command" + :description "The command to show help for." + :args "?" + :action(function(_, _, cmd) + if not cmd then + print(self:get_help()) + os.exit(0) + else + for _, command in ipairs(self._commands) do + for _, alias in ipairs(command._aliases) do + if alias == cmd then + print(command:get_help()) + os.exit(0) + end + end + end + end + help:error(("unknown command '%s'"):format(cmd)) + end) + + if value then + help = help(value) + end + + if not help._name then + help "help" + end + + help._is_help_command = true + return self +end + +function Parser:_is_shell_safe() + if self._basename then + if self._basename:find("[^%w_%-%+%.]") then + return false + end + else + for _, alias in ipairs(self._aliases) do + if alias:find("[^%w_%-%+%.]") then + return false + end + end + end + for _, option in ipairs(self._options) do + for _, alias in ipairs(option._aliases) do + if alias:find("[^%w_%-%+%.]") then + return false + end + end + if option._choices then + for _, choice in ipairs(option._choices) do + if choice:find("[%s'\"]") then + return false + end + end + end + end + for _, argument in ipairs(self._arguments) do + if argument._choices then + for _, choice in ipairs(argument._choices) do + if choice:find("[%s'\"]") then + return false + end + end + end + end + for _, command in ipairs(self._commands) do + if not command:_is_shell_safe() then + return false + end + end + return true +end + +function Parser:add_complete(value) + if value then + assert(type(value) == "string" or type(value) == "table", + ("bad argument #1 to 'add_complete' (string or table expected, got %s)"):format(type(value))) + end + + local complete = self:option() + :description "Output a shell completion script for the specified shell." + :args(1) + :choices {"bash", "zsh", "fish"} + :action(function(_, _, shell) + io.write(self["get_" .. shell .. "_complete"](self)) + os.exit(0) + end) + + if value then + complete = complete(value) + end + + if not complete._name then + complete "--completion" + end + + return self +end + +function Parser:add_complete_command(value) + if value then + assert(type(value) == "string" or type(value) == "table", + ("bad argument #1 to 'add_complete_command' (string or table expected, got %s)"):format(type(value))) + end + + local complete = self:command() + :description "Output a shell completion script." + complete:argument "shell" + :description "The shell to output a completion script for." + :choices {"bash", "zsh", "fish"} + :action(function(_, _, shell) + io.write(self["get_" .. shell .. "_complete"](self)) + os.exit(0) + end) + + if value then + complete = complete(value) + end + + if not complete._name then + complete "completion" + end + + return self +end + +local function base_name(pathname) + return pathname:gsub("[/\\]*$", ""):match(".*[/\\]([^/\\]*)") or pathname +end + +local function get_short_description(element) + local short = element:_get_description():match("^(.-)%.%s") + return short or element:_get_description():match("^(.-)%.?$") +end + +function Parser:_get_options() + local options = {} + for _, option in ipairs(self._options) do + for _, alias in ipairs(option._aliases) do + table.insert(options, alias) + end + end + return table.concat(options, " ") +end + +function Parser:_get_commands() + local commands = {} + for _, command in ipairs(self._commands) do + for _, alias in ipairs(command._aliases) do + table.insert(commands, alias) + end + end + return table.concat(commands, " ") +end + +function Parser:_bash_option_args(buf, indent) + local opts = {} + for _, option in ipairs(self._options) do + if option._choices or option._minargs > 0 then + local compreply + if option._choices then + compreply = 'COMPREPLY=($(compgen -W "' .. table.concat(option._choices, " ") .. '" -- "$cur"))' + else + compreply = 'COMPREPLY=($(compgen -f -- "$cur"))' + end + table.insert(opts, (" "):rep(indent + 4) .. table.concat(option._aliases, "|") .. ")") + table.insert(opts, (" "):rep(indent + 8) .. compreply) + table.insert(opts, (" "):rep(indent + 8) .. "return 0") + table.insert(opts, (" "):rep(indent + 8) .. ";;") + end + end + + if #opts > 0 then + table.insert(buf, (" "):rep(indent) .. 'case "$prev" in') + table.insert(buf, table.concat(opts, "\n")) + table.insert(buf, (" "):rep(indent) .. "esac\n") + end +end + +function Parser:_bash_get_cmd(buf, indent) + if #self._commands == 0 then + return + end + + table.insert(buf, (" "):rep(indent) .. 'args=("${args[@]:1}")') + table.insert(buf, (" "):rep(indent) .. 'for arg in "${args[@]}"; do') + table.insert(buf, (" "):rep(indent + 4) .. 'case "$arg" in') + + for _, command in ipairs(self._commands) do + table.insert(buf, (" "):rep(indent + 8) .. table.concat(command._aliases, "|") .. ")") + if self._parent then + table.insert(buf, (" "):rep(indent + 12) .. 'cmd="$cmd ' .. command._name .. '"') + else + table.insert(buf, (" "):rep(indent + 12) .. 'cmd="' .. command._name .. '"') + end + table.insert(buf, (" "):rep(indent + 12) .. 'opts="$opts ' .. command:_get_options() .. '"') + command:_bash_get_cmd(buf, indent + 12) + table.insert(buf, (" "):rep(indent + 12) .. "break") + table.insert(buf, (" "):rep(indent + 12) .. ";;") + end + + table.insert(buf, (" "):rep(indent + 4) .. "esac") + table.insert(buf, (" "):rep(indent) .. "done") +end + +function Parser:_bash_cmd_completions(buf) + local cmd_buf = {} + if self._parent then + self:_bash_option_args(cmd_buf, 12) + end + if #self._commands > 0 then + table.insert(cmd_buf, (" "):rep(12) .. 'COMPREPLY=($(compgen -W "' .. self:_get_commands() .. '" -- "$cur"))') + elseif self._is_help_command then + table.insert(cmd_buf, (" "):rep(12) + .. 'COMPREPLY=($(compgen -W "' + .. self._parent:_get_commands() + .. '" -- "$cur"))') + end + if #cmd_buf > 0 then + table.insert(buf, (" "):rep(8) .. "'" .. self:_get_fullname(true) .. "')") + table.insert(buf, table.concat(cmd_buf, "\n")) + table.insert(buf, (" "):rep(12) .. ";;") + end + + for _, command in ipairs(self._commands) do + command:_bash_cmd_completions(buf) + end +end + +function Parser:get_bash_complete() + self._basename = base_name(self._name) + assert(self:_is_shell_safe()) + local buf = {([[ +_%s() { + local IFS=$' \t\n' + local args cur prev cmd opts arg + args=("${COMP_WORDS[@]}") + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + opts="%s" +]]):format(self._basename, self:_get_options())} + + self:_bash_option_args(buf, 4) + self:_bash_get_cmd(buf, 4) + if #self._commands > 0 then + table.insert(buf, "") + table.insert(buf, (" "):rep(4) .. 'case "$cmd" in') + self:_bash_cmd_completions(buf) + table.insert(buf, (" "):rep(4) .. "esac\n") + end + + table.insert(buf, ([=[ + if [[ "$cur" = -* ]]; then + COMPREPLY=($(compgen -W "$opts" -- "$cur")) + fi +} + +complete -F _%s -o bashdefault -o default %s +]=]):format(self._basename, self._basename)) + + return table.concat(buf, "\n") +end + +function Parser:_zsh_arguments(buf, cmd_name, indent) + if self._parent then + table.insert(buf, (" "):rep(indent) .. "options=(") + table.insert(buf, (" "):rep(indent + 2) .. "$options") + else + table.insert(buf, (" "):rep(indent) .. "local -a options=(") + end + + for _, option in ipairs(self._options) do + local line = {} + if #option._aliases > 1 then + if option._maxcount > 1 then + table.insert(line, '"*"') + end + table.insert(line, "{" .. table.concat(option._aliases, ",") .. '}"') + else + table.insert(line, '"') + if option._maxcount > 1 then + table.insert(line, "*") + end + table.insert(line, option._name) + end + if option._description then + local description = get_short_description(option):gsub('["%]:`$]', "\\%0") + table.insert(line, "[" .. description .. "]") + end + if option._maxargs == math.huge then + table.insert(line, ":*") + end + if option._choices then + table.insert(line, ": :(" .. table.concat(option._choices, " ") .. ")") + elseif option._maxargs > 0 then + table.insert(line, ": :_files") + end + table.insert(line, '"') + table.insert(buf, (" "):rep(indent + 2) .. table.concat(line)) + end + + table.insert(buf, (" "):rep(indent) .. ")") + table.insert(buf, (" "):rep(indent) .. "_arguments -s -S \\") + table.insert(buf, (" "):rep(indent + 2) .. "$options \\") + + if self._is_help_command then + table.insert(buf, (" "):rep(indent + 2) .. '": :(' .. self._parent:_get_commands() .. ')" \\') + else + for _, argument in ipairs(self._arguments) do + local spec + if argument._choices then + spec = ": :(" .. table.concat(argument._choices, " ") .. ")" + else + spec = ": :_files" + end + if argument._maxargs == math.huge then + table.insert(buf, (" "):rep(indent + 2) .. '"*' .. spec .. '" \\') + break + end + for _ = 1, argument._maxargs do + table.insert(buf, (" "):rep(indent + 2) .. '"' .. spec .. '" \\') + end + end + + if #self._commands > 0 then + table.insert(buf, (" "):rep(indent + 2) .. '": :_' .. cmd_name .. '_cmds" \\') + table.insert(buf, (" "):rep(indent + 2) .. '"*:: :->args" \\') + end + end + + table.insert(buf, (" "):rep(indent + 2) .. "&& return 0") +end + +function Parser:_zsh_cmds(buf, cmd_name) + table.insert(buf, "\n_" .. cmd_name .. "_cmds() {") + table.insert(buf, " local -a commands=(") + + for _, command in ipairs(self._commands) do + local line = {} + if #command._aliases > 1 then + table.insert(line, "{" .. table.concat(command._aliases, ",") .. '}"') + else + table.insert(line, '"' .. command._name) + end + if command._description then + table.insert(line, ":" .. get_short_description(command):gsub('["`$]', "\\%0")) + end + table.insert(buf, " " .. table.concat(line) .. '"') + end + + table.insert(buf, ' )\n _describe "command" commands\n}') +end + +function Parser:_zsh_complete_help(buf, cmds_buf, cmd_name, indent) + if #self._commands == 0 then + return + end + + self:_zsh_cmds(cmds_buf, cmd_name) + table.insert(buf, "\n" .. (" "):rep(indent) .. "case $words[1] in") + + for _, command in ipairs(self._commands) do + local name = cmd_name .. "_" .. command._name + table.insert(buf, (" "):rep(indent + 2) .. table.concat(command._aliases, "|") .. ")") + command:_zsh_arguments(buf, name, indent + 4) + command:_zsh_complete_help(buf, cmds_buf, name, indent + 4) + table.insert(buf, (" "):rep(indent + 4) .. ";;\n") + end + + table.insert(buf, (" "):rep(indent) .. "esac") +end + +function Parser:get_zsh_complete() + self._basename = base_name(self._name) + assert(self:_is_shell_safe()) + local buf = {("#compdef %s\n"):format(self._basename)} + local cmds_buf = {} + table.insert(buf, "_" .. self._basename .. "() {") + if #self._commands > 0 then + table.insert(buf, " local context state state_descr line") + table.insert(buf, " typeset -A opt_args\n") + end + self:_zsh_arguments(buf, self._basename, 2) + self:_zsh_complete_help(buf, cmds_buf, self._basename, 2) + table.insert(buf, "\n return 1") + table.insert(buf, "}") + + local result = table.concat(buf, "\n") + if #cmds_buf > 0 then + result = result .. "\n" .. table.concat(cmds_buf, "\n") + end + return result .. "\n\n_" .. self._basename .. "\n" +end + +local function fish_escape(string) + return string:gsub("[\\']", "\\%0") +end + +function Parser:_fish_get_cmd(buf, indent) + if #self._commands == 0 then + return + end + + table.insert(buf, (" "):rep(indent) .. "set -e cmdline[1]") + table.insert(buf, (" "):rep(indent) .. "for arg in $cmdline") + table.insert(buf, (" "):rep(indent + 4) .. "switch $arg") + + for _, command in ipairs(self._commands) do + table.insert(buf, (" "):rep(indent + 8) .. "case " .. table.concat(command._aliases, " ")) + table.insert(buf, (" "):rep(indent + 12) .. "set cmd $cmd " .. command._name) + command:_fish_get_cmd(buf, indent + 12) + table.insert(buf, (" "):rep(indent + 12) .. "break") + end + + table.insert(buf, (" "):rep(indent + 4) .. "end") + table.insert(buf, (" "):rep(indent) .. "end") +end + +function Parser:_fish_complete_help(buf, basename) + local prefix = "complete -c " .. basename + table.insert(buf, "") + + for _, command in ipairs(self._commands) do + local aliases = table.concat(command._aliases, " ") + local line + if self._parent then + line = ("%s -n '__fish_%s_using_command %s' -xa '%s'") + :format(prefix, basename, self:_get_fullname(true), aliases) + else + line = ("%s -n '__fish_%s_using_command' -xa '%s'"):format(prefix, basename, aliases) + end + if command._description then + line = ("%s -d '%s'"):format(line, fish_escape(get_short_description(command))) + end + table.insert(buf, line) + end + + if self._is_help_command then + local line = ("%s -n '__fish_%s_using_command %s' -xa '%s'") + :format(prefix, basename, self:_get_fullname(true), self._parent:_get_commands()) + table.insert(buf, line) + end + + for _, option in ipairs(self._options) do + local parts = {prefix} + + if self._parent then + table.insert(parts, "-n '__fish_" .. basename .. "_seen_command " .. self:_get_fullname(true) .. "'") + end + + for _, alias in ipairs(option._aliases) do + if alias:match("^%-.$") then + table.insert(parts, "-s " .. alias:sub(2)) + elseif alias:match("^%-%-.+") then + table.insert(parts, "-l " .. alias:sub(3)) + end + end + + if option._choices then + table.insert(parts, "-xa '" .. table.concat(option._choices, " ") .. "'") + elseif option._minargs > 0 then + table.insert(parts, "-r") + end + + if option._description then + table.insert(parts, "-d '" .. fish_escape(get_short_description(option)) .. "'") + end + + table.insert(buf, table.concat(parts, " ")) + end + + for _, command in ipairs(self._commands) do + command:_fish_complete_help(buf, basename) + end +end + +function Parser:get_fish_complete() + self._basename = base_name(self._name) + assert(self:_is_shell_safe()) + local buf = {} + + if #self._commands > 0 then + table.insert(buf, ([[ +function __fish_%s_print_command + set -l cmdline (commandline -poc) + set -l cmd]]):format(self._basename)) + self:_fish_get_cmd(buf, 4) + table.insert(buf, ([[ + echo "$cmd" +end + +function __fish_%s_using_command + test (__fish_%s_print_command) = "$argv" + and return 0 + or return 1 +end + +function __fish_%s_seen_command + string match -q "$argv*" (__fish_%s_print_command) + and return 0 + or return 1 +end]]):format(self._basename, self._basename, self._basename, self._basename)) + end + + self:_fish_complete_help(buf, self._basename) + return table.concat(buf, "\n") .. "\n" +end + +local function get_tip(context, wrong_name) + local context_pool = {} + local possible_name + local possible_names = {} + + for name in pairs(context) do + if type(name) == "string" then + for i = 1, #name do + possible_name = name:sub(1, i - 1) .. name:sub(i + 1) + + if not context_pool[possible_name] then + context_pool[possible_name] = {} + end + + table.insert(context_pool[possible_name], name) + end + end + end + + for i = 1, #wrong_name + 1 do + possible_name = wrong_name:sub(1, i - 1) .. wrong_name:sub(i + 1) + + if context[possible_name] then + possible_names[possible_name] = true + elseif context_pool[possible_name] then + for _, name in ipairs(context_pool[possible_name]) do + possible_names[name] = true + end + end + end + + local first = next(possible_names) + + if first then + if next(possible_names, first) then + local possible_names_arr = {} + + for name in pairs(possible_names) do + table.insert(possible_names_arr, "'" .. name .. "'") + end + + table.sort(possible_names_arr) + return "\nDid you mean one of these: " .. table.concat(possible_names_arr, " ") .. "?" + else + return "\nDid you mean '" .. first .. "'?" + end + else + return "" + end +end + +local ElementState = class({ + invocations = 0 +}) + +function ElementState:__call(state, element) + self.state = state + self.result = state.result + self.element = element + self.target = element._target or element:_get_default_target() + self.action, self.result[self.target] = element:_get_action() + return self +end + +function ElementState:error(fmt, ...) + self.state:error(fmt, ...) +end + +function ElementState:convert(argument, index) + local converter = self.element._convert + + if converter then + local ok, err + + if type(converter) == "function" then + ok, err = converter(argument) + elseif type(converter[index]) == "function" then + ok, err = converter[index](argument) + else + ok = converter[argument] + end + + if ok == nil then + self:error(err and "%s" or "malformed argument '%s'", err or argument) + end + + argument = ok + end + + return argument +end + +function ElementState:default(mode) + return self.element._defmode:find(mode) and self.element._default +end + +local function bound(noun, min, max, is_max) + local res = "" + + if min ~= max then + res = "at " .. (is_max and "most" or "least") .. " " + end + + local number = is_max and max or min + return res .. tostring(number) .. " " .. noun .. (number == 1 and "" or "s") +end + +function ElementState:set_name(alias) + self.name = ("%s '%s'"):format(alias and "option" or "argument", alias or self.element._name) +end + +function ElementState:invoke() + self.open = true + self.overwrite = false + + if self.invocations >= self.element._maxcount then + if self.element._overwrite then + self.overwrite = true + else + local num_times_repr = bound("time", self.element._mincount, self.element._maxcount, true) + self:error("%s must be used %s", self.name, num_times_repr) + end + else + self.invocations = self.invocations + 1 + end + + self.args = {} + + if self.element._maxargs <= 0 then + self:close() + end + + return self.open +end + +function ElementState:check_choices(argument) + if self.element._choices then + for _, choice in ipairs(self.element._choices) do + if argument == choice then + return + end + end + local choices_list = "'" .. table.concat(self.element._choices, "', '") .. "'" + local is_option = getmetatable(self.element) == Option + self:error("%s%s must be one of %s", is_option and "argument for " or "", self.name, choices_list) + end +end + +function ElementState:pass(argument) + self:check_choices(argument) + argument = self:convert(argument, #self.args + 1) + table.insert(self.args, argument) + + if #self.args >= self.element._maxargs then + self:close() + end + + return self.open +end + +function ElementState:complete_invocation() + while #self.args < self.element._minargs do + self:pass(self.element._default) + end +end + +function ElementState:close() + if self.open then + self.open = false + + if #self.args < self.element._minargs then + if self:default("a") then + self:complete_invocation() + else + if #self.args == 0 then + if getmetatable(self.element) == Argument then + self:error("missing %s", self.name) + elseif self.element._maxargs == 1 then + self:error("%s requires an argument", self.name) + end + end + + self:error("%s requires %s", self.name, bound("argument", self.element._minargs, self.element._maxargs)) + end + end + + local args + + if self.element._maxargs == 0 then + args = self.args[1] + elseif self.element._maxargs == 1 then + if self.element._minargs == 0 and self.element._mincount ~= self.element._maxcount then + args = self.args + else + args = self.args[1] + end + else + args = self.args + end + + self.action(self.result, self.target, args, self.overwrite) + end +end + +local ParseState = class({ + result = {}, + options = {}, + arguments = {}, + argument_i = 1, + element_to_mutexes = {}, + mutex_to_element_state = {}, + command_actions = {} +}) + +function ParseState:__call(parser, error_handler) + self.parser = parser + self.error_handler = error_handler + self.charset = parser:_update_charset() + self:switch(parser) + return self +end + +function ParseState:error(fmt, ...) + self.error_handler(self.parser, fmt:format(...)) +end + +function ParseState:switch(parser) + self.parser = parser + + if parser._action then + table.insert(self.command_actions, {action = parser._action, name = parser._name}) + end + + for _, option in ipairs(parser._options) do + option = ElementState(self, option) + table.insert(self.options, option) + + for _, alias in ipairs(option.element._aliases) do + self.options[alias] = option + end + end + + for _, mutex in ipairs(parser._mutexes) do + for _, element in ipairs(mutex) do + if not self.element_to_mutexes[element] then + self.element_to_mutexes[element] = {} + end + + table.insert(self.element_to_mutexes[element], mutex) + end + end + + for _, argument in ipairs(parser._arguments) do + argument = ElementState(self, argument) + table.insert(self.arguments, argument) + argument:set_name() + argument:invoke() + end + + self.handle_options = parser._handle_options + self.argument = self.arguments[self.argument_i] + self.commands = parser._commands + + for _, command in ipairs(self.commands) do + for _, alias in ipairs(command._aliases) do + self.commands[alias] = command + end + end +end + +function ParseState:get_option(name) + local option = self.options[name] + + if not option then + self:error("unknown option '%s'%s", name, get_tip(self.options, name)) + else + return option + end +end + +function ParseState:get_command(name) + local command = self.commands[name] + + if not command then + if #self.commands > 0 then + self:error("unknown command '%s'%s", name, get_tip(self.commands, name)) + else + self:error("too many arguments") + end + else + return command + end +end + +function ParseState:check_mutexes(element_state) + if self.element_to_mutexes[element_state.element] then + for _, mutex in ipairs(self.element_to_mutexes[element_state.element]) do + local used_element_state = self.mutex_to_element_state[mutex] + + if used_element_state and used_element_state ~= element_state then + self:error("%s can not be used together with %s", element_state.name, used_element_state.name) + else + self.mutex_to_element_state[mutex] = element_state + end + end + end +end + +function ParseState:invoke(option, name) + self:close() + option:set_name(name) + self:check_mutexes(option, name) + + if option:invoke() then + self.option = option + end +end + +function ParseState:pass(arg) + if self.option then + if not self.option:pass(arg) then + self.option = nil + end + elseif self.argument then + self:check_mutexes(self.argument) + + if not self.argument:pass(arg) then + self.argument_i = self.argument_i + 1 + self.argument = self.arguments[self.argument_i] + end + else + local command = self:get_command(arg) + self.result[command._target or command._name] = true + + if self.parser._command_target then + self.result[self.parser._command_target] = command._name + end + + self:switch(command) + end +end + +function ParseState:close() + if self.option then + self.option:close() + self.option = nil + end +end + +function ParseState:finalize() + self:close() + + for i = self.argument_i, #self.arguments do + local argument = self.arguments[i] + if #argument.args == 0 and argument:default("u") then + argument:complete_invocation() + else + argument:close() + end + end + + if self.parser._require_command and #self.commands > 0 then + self:error("a command is required") + end + + for _, option in ipairs(self.options) do + option.name = option.name or ("option '%s'"):format(option.element._name) + + if option.invocations == 0 then + if option:default("u") then + option:invoke() + option:complete_invocation() + option:close() + end + end + + local mincount = option.element._mincount + + if option.invocations < mincount then + if option:default("a") then + while option.invocations < mincount do + option:invoke() + option:close() + end + elseif option.invocations == 0 then + self:error("missing %s", option.name) + else + self:error("%s must be used %s", option.name, bound("time", mincount, option.element._maxcount)) + end + end + end + + for i = #self.command_actions, 1, -1 do + self.command_actions[i].action(self.result, self.command_actions[i].name) + end +end + +function ParseState:parse(args) + for _, arg in ipairs(args) do + local plain = true + + if self.handle_options then + local first = arg:sub(1, 1) + + if self.charset[first] then + if #arg > 1 then + plain = false + + if arg:sub(2, 2) == first then + if #arg == 2 then + if self.options[arg] then + local option = self:get_option(arg) + self:invoke(option, arg) + else + self:close() + end + + self.handle_options = false + else + local equals = arg:find "=" + if equals then + local name = arg:sub(1, equals - 1) + local option = self:get_option(name) + + if option.element._maxargs <= 0 then + self:error("option '%s' does not take arguments", name) + end + + self:invoke(option, name) + self:pass(arg:sub(equals + 1)) + else + local option = self:get_option(arg) + self:invoke(option, arg) + end + end + else + for i = 2, #arg do + local name = first .. arg:sub(i, i) + local option = self:get_option(name) + self:invoke(option, name) + + if i ~= #arg and option.element._maxargs > 0 then + self:pass(arg:sub(i + 1)) + break + end + end + end + end + end + end + + if plain then + self:pass(arg) + end + end + + self:finalize() + return self.result +end + +function Parser:error(msg) + io.stderr:write(("%s\n\nError: %s\n"):format(self:get_usage(), msg)) + os.exit(1) +end + +-- Compatibility with strict.lua and other checkers: +local default_cmdline = rawget(_G, "arg") or {} + +function Parser:_parse(args, error_handler) + return ParseState(self, error_handler):parse(args or default_cmdline) +end + +function Parser:parse(args) + return self:_parse(args, self.error) +end + +local function xpcall_error_handler(err) + if not debug then + return tostring(err) + end + return tostring(err) .. "\noriginal " .. debug.traceback("", 2):sub(2) +end + +function Parser:pparse(args) + local parse_error + + local ok, result = xpcall(function() + return self:_parse(args, function(_, err) + parse_error = err + error(err, 0) + end) + end, xpcall_error_handler) + + if ok then + return true, result + elseif not parse_error then + error(result, 0) + else + return false, parse_error + end +end + +local argparse = {} + +argparse.version = "0.7.0" + +setmetatable(argparse, {__call = function(_, ...) + return Parser(default_cmdline[0]):add_help(true)(...) +end}) + +return argparse diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/build.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/build.lua new file mode 100644 index 000000000..0b82174b0 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/build.lua @@ -0,0 +1,458 @@ + +local build = {} + +local path = require("luarocks.path") +local util = require("luarocks.util") +local fun = require("luarocks.fun") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local deps = require("luarocks.deps") +local cfg = require("luarocks.core.cfg") +local vers = require("luarocks.core.vers") +local repos = require("luarocks.repos") +local writer = require("luarocks.manif.writer") +local deplocks = require("luarocks.deplocks") + +build.opts = util.opts_table("build.opts", { + need_to_fetch = "boolean", + minimal_mode = "boolean", + deps_mode = "string", + build_only_deps = "boolean", + namespace = "string?", + branch = "string?", + verify = "boolean", + check_lua_versions = "boolean", + pin = "boolean", + no_install = "boolean" +}) + +do + --- Write to the current directory the contents of a table, + -- where each key is a file name and its value is the file content. + -- @param files table: The table of files to be written. + local function extract_from_rockspec(files) + for name, content in pairs(files) do + local fd = io.open(dir.path(fs.current_dir(), name), "w+") + fd:write(content) + fd:close() + end + end + + --- Applies patches inlined in the build.patches section + -- and extracts files inlined in the build.extra_files section + -- of a rockspec. + -- @param rockspec table: A rockspec table. + -- @return boolean or (nil, string): True if succeeded or + -- nil and an error message. + function build.apply_patches(rockspec) + assert(rockspec:type() == "rockspec") + + if not (rockspec.build.extra_files or rockspec.build.patches) then + return true + end + + local fd = io.open(fs.absolute_name(".luarocks.patches.applied"), "r") + if fd then + fd:close() + return true + end + + if rockspec.build.extra_files then + extract_from_rockspec(rockspec.build.extra_files) + end + if rockspec.build.patches then + extract_from_rockspec(rockspec.build.patches) + for patch, patchdata in util.sortedpairs(rockspec.build.patches) do + util.printout("Applying patch "..patch.."...") + local create_delete = rockspec:format_is_at_least("3.0") + local ok, err = fs.apply_patch(tostring(patch), patchdata, create_delete) + if not ok then + return nil, "Failed applying patch "..patch + end + end + end + + fd = io.open(fs.absolute_name(".luarocks.patches.applied"), "w") + if fd then + fd:close() + end + return true + end +end + +local function check_macosx_deployment_target(rockspec) + local target = rockspec.build.macosx_deployment_target + local function patch_variable(var) + if rockspec.variables[var]:match("MACOSX_DEPLOYMENT_TARGET") then + rockspec.variables[var] = (rockspec.variables[var]):gsub("MACOSX_DEPLOYMENT_TARGET=[^ ]*", "MACOSX_DEPLOYMENT_TARGET="..target) + else + rockspec.variables[var] = "env MACOSX_DEPLOYMENT_TARGET="..target.." "..rockspec.variables[var] + end + end + if cfg.is_platform("macosx") and rockspec:format_is_at_least("3.0") and target then + local version = util.popen_read("sw_vers -productVersion") + if version:match("^%d+%.%d+%.%d+$") or version:match("^%d+%.%d+$") then + if vers.compare_versions(target, version) then + return nil, ("This rock requires Mac OSX %s, and you are running %s."):format(target, version) + end + end + patch_variable("CC") + patch_variable("LD") + end + return true +end + +local function process_dependencies(rockspec, opts) + if not opts.build_only_deps then + local ok, err, errcode = deps.check_external_deps(rockspec, "build") + if err then + return nil, err, errcode + end + end + + local ok, err, errcode = deps.check_lua_incdir(rockspec.variables) + if not ok then + return nil, err, errcode + end + + if cfg.link_lua_explicitly then + local ok, err, errcode = deps.check_lua_libdir(rockspec.variables) + if not ok then + return nil, err, errcode + end + end + + if opts.deps_mode == "none" then + return true + end + + if not opts.build_only_deps then + if next(rockspec.build_dependencies) then + local ok, err, errcode = deps.fulfill_dependencies(rockspec, "build_dependencies", opts.deps_mode, opts.verify) + if err then + return nil, err, errcode + end + end + end + ok, err, errcode = deps.fulfill_dependencies(rockspec, "dependencies", opts.deps_mode, opts.verify) + if err then + return nil, err, errcode + end + return true +end + +local function fetch_and_change_to_source_dir(rockspec, opts) + if opts.minimal_mode or opts.build_only_deps then + return true + end + if opts.need_to_fetch then + if opts.branch then + rockspec.source.branch = opts.branch + end + local ok, source_dir, errcode = fetch.fetch_sources(rockspec, true) + if not ok then + return nil, source_dir, errcode + end + local err + ok, err = fs.change_dir(source_dir) + if not ok then + return nil, err + end + elseif rockspec.source.file then + local ok, err = fs.unpack_archive(rockspec.source.file) + if not ok then + return nil, err + end + end + fs.change_dir(rockspec.source.dir) + return true +end + +local function prepare_install_dirs(name, version) + local dirs = { + lua = { name = path.lua_dir(name, version), is_module_path = true, perms = "read" }, + lib = { name = path.lib_dir(name, version), is_module_path = true, perms = "exec" }, + bin = { name = path.bin_dir(name, version), is_module_path = false, perms = "exec" }, + conf = { name = path.conf_dir(name, version), is_module_path = false, perms = "read" }, + } + + for _, d in pairs(dirs) do + local ok, err = fs.make_dir(d.name) + if not ok then + return nil, err + end + end + + return dirs +end + +local function run_build_driver(rockspec, no_install) + local btype = rockspec.build.type + if btype == "none" then + return true + end + -- Temporary compatibility + if btype == "module" then + util.printout("Do not use 'module' as a build type. Use 'builtin' instead.") + btype = "builtin" + rockspec.build.type = btype + end + if cfg.accepted_build_types and not fun.contains(cfg.accepted_build_types, btype) then + return nil, "This rockspec uses the '"..btype.."' build type, which is blocked by the 'accepted_build_types' setting in your LuaRocks configuration." + end + local pok, driver = pcall(require, "luarocks.build." .. btype) + if not pok or type(driver) ~= "table" then + return nil, "Failed initializing build back-end for build type '"..btype.."': "..driver + end + local ok, err = driver.run(rockspec, no_install) + if not ok then + return nil, "Build error: " .. err + end + return true +end + +local install_files +do + --- Install files to a given location. + -- Takes a table where the array part is a list of filenames to be copied. + -- In the hash part, other keys, if is_module_path is set, are identifiers + -- in Lua module format, to indicate which subdirectory the file should be + -- copied to. For example, install_files({["foo.bar"] = "src/bar.lua"}, "boo") + -- will copy src/bar.lua to boo/foo. + -- @param files table or nil: A table containing a list of files to copy in + -- the format described above. If nil is passed, this function is a no-op. + -- Directories should be delimited by forward slashes as in internet URLs. + -- @param location string: The base directory files should be copied to. + -- @param is_module_path boolean: True if string keys in files should be + -- interpreted as dotted module paths. + -- @param perms string ("read" or "exec"): Permissions of the newly created + -- files installed. + -- Directories are always created with the default permissions. + -- @return boolean or (nil, string): True if succeeded or + -- nil and an error message. + local function install_to(files, location, is_module_path, perms) + assert(type(files) == "table" or not files) + assert(type(location) == "string") + if not files then + return true + end + for k, file in pairs(files) do + local dest = location + local filename = dir.base_name(file) + if type(k) == "string" then + local modname = k + if is_module_path then + dest = dir.path(location, path.module_to_path(modname)) + local ok, err = fs.make_dir(dest) + if not ok then return nil, err end + if filename:match("%.lua$") then + local basename = modname:match("([^.]+)$") + filename = basename..".lua" + end + else + dest = dir.path(location, dir.dir_name(modname)) + local ok, err = fs.make_dir(dest) + if not ok then return nil, err end + filename = dir.base_name(modname) + end + else + local ok, err = fs.make_dir(dest) + if not ok then return nil, err end + end + local ok = fs.copy(dir.path(file), dir.path(dest, filename), perms) + if not ok then + return nil, "Failed copying "..file + end + end + return true + end + + local function install_default_docs(name, version) + local patterns = { "readme", "license", "copying", ".*%.md" } + local dest = dir.path(path.install_dir(name, version), "doc") + local has_dir = false + for file in fs.dir() do + for _, pattern in ipairs(patterns) do + if file:lower():match("^"..pattern) then + if not has_dir then + fs.make_dir(dest) + has_dir = true + end + fs.copy(file, dest, "read") + break + end + end + end + end + + install_files = function(rockspec, dirs) + local name, version = rockspec.name, rockspec.version + + if rockspec.build.install then + for k, d in pairs(dirs) do + local ok, err = install_to(rockspec.build.install[k], d.name, d.is_module_path, d.perms) + if not ok then return nil, err end + end + end + + local copy_directories = rockspec.build.copy_directories + local copying_default = false + if not copy_directories then + copy_directories = {"doc"} + copying_default = true + end + + local any_docs = false + for _, copy_dir in pairs(copy_directories) do + if fs.is_dir(copy_dir) then + local dest = dir.path(path.install_dir(name, version), copy_dir) + fs.make_dir(dest) + fs.copy_contents(copy_dir, dest) + any_docs = true + else + if not copying_default then + return nil, "Directory '"..copy_dir.."' not found" + end + end + end + if not any_docs then + install_default_docs(name, version) + end + + return true + end +end + +local function write_rock_dir_files(rockspec, opts) + local name, version = rockspec.name, rockspec.version + + fs.copy(rockspec.local_abs_filename, path.rockspec_file(name, version), "read") + + local deplock_file = deplocks.get_abs_filename(rockspec.name) + if deplock_file then + fs.copy(deplock_file, dir.path(path.install_dir(name, version), "luarocks.lock"), "read") + end + + local ok, err = writer.make_rock_manifest(name, version) + if not ok then return nil, err end + + ok, err = writer.make_namespace_file(name, version, opts.namespace) + if not ok then return nil, err end + + return true +end + +--- Build and install a rock given a rockspec. +-- @param opts table: build options table +-- @return (string, string) or (nil, string, [string]): Name and version of +-- installed rock if succeeded or nil and an error message followed by an error code. +function build.build_rockspec(rockspec, opts) + assert(rockspec:type() == "rockspec") + assert(opts:type() == "build.opts") + + if not rockspec.build then + if rockspec:format_is_at_least("3.0") then + rockspec.build = { + type = "builtin" + } + else + return nil, "Rockspec error: build table not specified" + end + end + + if not rockspec.build.type then + if rockspec:format_is_at_least("3.0") then + rockspec.build.type = "builtin" + else + return nil, "Rockspec error: build type not specified" + end + end + + local ok, err = fetch_and_change_to_source_dir(rockspec, opts) + if not ok then return nil, err end + + if opts.pin then + deplocks.init(rockspec.name, ".") + end + + ok, err = process_dependencies(rockspec, opts) + if not ok then return nil, err end + + local name, version = rockspec.name, rockspec.version + if opts.build_only_deps then + if opts.pin then + deplocks.write_file() + end + return name, version + end + + local dirs, err + local rollback + if not opts.no_install then + if repos.is_installed(name, version) then + repos.delete_version(name, version, opts.deps_mode) + end + + dirs, err = prepare_install_dirs(name, version) + if not dirs then return nil, err end + + rollback = util.schedule_function(function() + fs.delete(path.install_dir(name, version)) + fs.remove_dir_if_empty(path.versions_dir(name)) + end) + end + + ok, err = build.apply_patches(rockspec) + if not ok then return nil, err end + + ok, err = check_macosx_deployment_target(rockspec) + if not ok then return nil, err end + + ok, err = run_build_driver(rockspec, opts.no_install) + if not ok then return nil, err end + + if opts.no_install then + fs.pop_dir() + if opts.need_to_fetch then + fs.pop_dir() + end + return name, version + end + + ok, err = install_files(rockspec, dirs) + if not ok then return nil, err end + + for _, d in pairs(dirs) do + fs.remove_dir_if_empty(d.name) + end + + fs.pop_dir() + if opts.need_to_fetch then + fs.pop_dir() + end + + if opts.pin then + deplocks.write_file() + end + + ok, err = write_rock_dir_files(rockspec, opts) + if not ok then return nil, err end + + ok, err = repos.deploy_files(name, version, repos.should_wrap_bin_scripts(rockspec), opts.deps_mode) + if not ok then return nil, err end + + util.remove_scheduled_function(rollback) + rollback = util.schedule_function(function() + repos.delete_version(name, version, opts.deps_mode) + end) + + ok, err = repos.run_hook(rockspec, "post_install") + if not ok then return nil, err end + + util.announce_install(rockspec) + util.remove_scheduled_function(rollback) + return name, version +end + +return build diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/build/builtin.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/build/builtin.lua new file mode 100644 index 000000000..cd4d44867 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/build/builtin.lua @@ -0,0 +1,379 @@ + +--- A builtin build system: back-end to provide a portable way of building C-based Lua modules. +local builtin = {} + +local unpack = unpack or table.unpack + +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") + +function builtin.autodetect_external_dependencies(build) + if not build or not build.modules then + return nil + end + local extdeps = {} + local any = false + for _, data in pairs(build.modules) do + if type(data) == "table" and data.libraries then + local libraries = data.libraries + if type(libraries) == "string" then + libraries = { libraries } + end + local incdirs = {} + local libdirs = {} + for _, lib in ipairs(libraries) do + local upper = lib:upper():gsub("%+", "P"):gsub("[^%w]", "_") + any = true + extdeps[upper] = { library = lib } + table.insert(incdirs, "$(" .. upper .. "_INCDIR)") + table.insert(libdirs, "$(" .. upper .. "_LIBDIR)") + end + if not data.incdirs then + data.incdirs = incdirs + end + if not data.libdirs then + data.libdirs = libdirs + end + end + end + return any and extdeps or nil +end + +local function autoextract_libs(external_dependencies, variables) + if not external_dependencies then + return nil, nil, nil + end + local libs = {} + local incdirs = {} + local libdirs = {} + for name, data in pairs(external_dependencies) do + if data.library then + table.insert(libs, data.library) + table.insert(incdirs, variables[name .. "_INCDIR"]) + table.insert(libdirs, variables[name .. "_LIBDIR"]) + end + end + return libs, incdirs, libdirs +end + +do + local function get_cmod_name(file) + local fd = io.open(dir.path(fs.current_dir(), file), "r") + if not fd then return nil end + local data = fd:read("*a") + fd:close() + return (data:match("int%s+luaopen_([a-zA-Z0-9_]+)")) + end + + local skiplist = { + ["spec"] = true, + [".luarocks"] = true, + ["lua_modules"] = true, + ["test.lua"] = true, + ["tests.lua"] = true, + } + + function builtin.autodetect_modules(libs, incdirs, libdirs) + local modules = {} + local install + local copy_directories + + local prefix = "" + for _, parent in ipairs({"src", "lua", "lib"}) do + if fs.is_dir(parent) then + fs.change_dir(parent) + prefix = parent.."/" + break + end + end + + for _, file in ipairs(fs.find()) do + local base = file:match("^([^\\/]*)") + if not skiplist[base] then + local luamod = file:match("(.*)%.lua$") + if luamod then + modules[path.path_to_module(file)] = prefix..file + else + local cmod = file:match("(.*)%.c$") + if cmod then + local modname = get_cmod_name(file) or path.path_to_module(file:gsub("%.c$", ".lua")) + modules[modname] = { + sources = prefix..file, + libraries = libs, + incdirs = incdirs, + libdirs = libdirs, + } + end + end + end + end + + if prefix ~= "" then + fs.pop_dir() + end + + local bindir = (fs.is_dir("src/bin") and "src/bin") + or (fs.is_dir("bin") and "bin") + if bindir then + install = { bin = {} } + for _, file in ipairs(fs.list_dir(bindir)) do + table.insert(install.bin, dir.path(bindir, file)) + end + end + + for _, directory in ipairs({ "doc", "docs", "samples", "tests" }) do + if fs.is_dir(directory) then + if not copy_directories then + copy_directories = {} + end + table.insert(copy_directories, directory) + end + end + + return modules, install, copy_directories + end +end + +--- Run a command displaying its execution on standard output. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +local function execute(...) + io.stdout:write(table.concat({...}, " ").."\n") + return fs.execute(...) +end + +--- Driver function for the builtin build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors occurred, +-- nil and an error message otherwise. +function builtin.run(rockspec, no_install) + assert(rockspec:type() == "rockspec") + local compile_object, compile_library, compile_static_library + + local build = rockspec.build + local variables = rockspec.variables + local checked_lua_h = false + + for _, var in ipairs{ "CC", "CFLAGS", "LDFLAGS" } do + variables[var] = variables[var] or os.getenv(var) or "" + end + + local function add_flags(extras, flag, flags) + if flags then + if type(flags) ~= "table" then + flags = { tostring(flags) } + end + util.variable_substitutions(flags, variables) + for _, v in ipairs(flags) do + table.insert(extras, flag:format(v)) + end + end + end + + if cfg.is_platform("mingw32") then + compile_object = function(object, source, defines, incdirs) + local extras = {} + add_flags(extras, "-D%s", defines) + add_flags(extras, "-I%s", incdirs) + return execute(variables.CC.." "..variables.CFLAGS, "-c", "-o", object, "-I"..variables.LUA_INCDIR, source, unpack(extras)) + end + compile_library = function(library, objects, libraries, libdirs, name) + local extras = { unpack(objects) } + add_flags(extras, "-L%s", libdirs) + add_flags(extras, "-l%s", libraries) + extras[#extras+1] = dir.path(variables.LUA_LIBDIR, variables.LUALIB) + + if variables.CC == "clang" or variables.CC == "clang-cl" then + local exported_name = name:gsub("%.", "_") + exported_name = exported_name:match('^[^%-]+%-(.+)$') or exported_name + extras[#extras+1] = string.format("-Wl,-export:luaopen_%s", exported_name) + else + extras[#extras+1] = "-l" .. (variables.MSVCRT or "m") + end + + local ok = execute(variables.LD.." "..variables.LDFLAGS.." "..variables.LIBFLAG, "-o", library, unpack(extras)) + return ok + end + --[[ TODO disable static libs until we fix the conflict in the manifest, which will take extending the manifest format. + compile_static_library = function(library, objects, libraries, libdirs, name) + local ok = execute(variables.AR, "rc", library, unpack(objects)) + if ok then + ok = execute(variables.RANLIB, library) + end + return ok + end + ]] + elseif cfg.is_platform("win32") then + compile_object = function(object, source, defines, incdirs) + local extras = {} + add_flags(extras, "-D%s", defines) + add_flags(extras, "-I%s", incdirs) + return execute(variables.CC.." "..variables.CFLAGS, "-c", "-Fo"..object, "-I"..variables.LUA_INCDIR, source, unpack(extras)) + end + compile_library = function(library, objects, libraries, libdirs, name) + local extras = { unpack(objects) } + add_flags(extras, "-libpath:%s", libdirs) + add_flags(extras, "%s.lib", libraries) + local basename = dir.base_name(library):gsub(".[^.]*$", "") + local deffile = basename .. ".def" + local def = io.open(dir.path(fs.current_dir(), deffile), "w+") + local exported_name = name:gsub("%.", "_") + exported_name = exported_name:match('^[^%-]+%-(.+)$') or exported_name + def:write("EXPORTS\n") + def:write("luaopen_"..exported_name.."\n") + def:close() + local ok = execute(variables.LD, "-dll", "-def:"..deffile, "-out:"..library, dir.path(variables.LUA_LIBDIR, variables.LUALIB), unpack(extras)) + local basedir = "" + if name:find("%.") ~= nil then + basedir = name:gsub("%.%w+$", "\\") + basedir = basedir:gsub("%.", "\\") + end + local manifestfile = basedir .. basename..".dll.manifest" + + if ok and fs.exists(manifestfile) then + ok = execute(variables.MT, "-manifest", manifestfile, "-outputresource:"..basedir..basename..".dll;2") + end + return ok + end + --[[ TODO disable static libs until we fix the conflict in the manifest, which will take extending the manifest format. + compile_static_library = function(library, objects, libraries, libdirs, name) + local ok = execute(variables.AR, "-out:"..library, unpack(objects)) + return ok + end + ]] + else + compile_object = function(object, source, defines, incdirs) + local extras = {} + add_flags(extras, "-D%s", defines) + add_flags(extras, "-I%s", incdirs) + return execute(variables.CC.." "..variables.CFLAGS, "-I"..variables.LUA_INCDIR, "-c", source, "-o", object, unpack(extras)) + end + compile_library = function (library, objects, libraries, libdirs) + local extras = { unpack(objects) } + add_flags(extras, "-L%s", libdirs) + if cfg.gcc_rpath then + add_flags(extras, "-Wl,-rpath,%s", libdirs) + end + add_flags(extras, "-l%s", libraries) + if cfg.link_lua_explicitly then + extras[#extras+1] = "-L"..variables.LUA_LIBDIR + extras[#extras+1] = "-llua" + end + return execute(variables.LD.." "..variables.LDFLAGS.." "..variables.LIBFLAG, "-o", library, unpack(extras)) + end + compile_static_library = function(library, objects, libraries, libdirs, name) -- luacheck: ignore 211 + local ok = execute(variables.AR, "rc", library, unpack(objects)) + if ok then + ok = execute(variables.RANLIB, library) + end + return ok + end + end + + local ok, err + local lua_modules = {} + local lib_modules = {} + local luadir = path.lua_dir(rockspec.name, rockspec.version) + local libdir = path.lib_dir(rockspec.name, rockspec.version) + + if not build.modules then + if rockspec:format_is_at_least("3.0") then + local libs, incdirs, libdirs = autoextract_libs(rockspec.external_dependencies, rockspec.variables) + local install, copy_directories + build.modules, install, copy_directories = builtin.autodetect_modules(libs, incdirs, libdirs) + build.install = build.install or install + build.copy_directories = build.copy_directories or copy_directories + else + return nil, "Missing build.modules table" + end + end + for name, info in pairs(build.modules) do + local moddir = path.module_to_path(name) + if type(info) == "string" then + local ext = info:match("%.([^.]+)$") + if ext == "lua" then + local filename = dir.base_name(info) + if filename == "init.lua" and not name:match("%.init$") then + moddir = path.module_to_path(name..".init") + else + local basename = name:match("([^.]+)$") + filename = basename..".lua" + end + local dest = dir.path(luadir, moddir, filename) + lua_modules[info] = dest + else + info = {info} + end + end + if type(info) == "table" then + if not checked_lua_h then + local lua_incdir, lua_h = variables.LUA_INCDIR, "lua.h" + if not fs.exists(dir.path(lua_incdir, lua_h)) then + return nil, "Lua header file "..lua_h.." not found (looked in "..lua_incdir.."). \n" .. + "You need to install the Lua development package for your system." + end + checked_lua_h = true + end + local objects = {} + local sources = info.sources + if info[1] then sources = info end + if type(sources) == "string" then sources = {sources} end + for _, source in ipairs(sources) do + local object = source:gsub("%.[^.]*$", "."..cfg.obj_extension) + if not object then + object = source.."."..cfg.obj_extension + end + ok = compile_object(object, source, info.defines, info.incdirs) + if not ok then + return nil, "Failed compiling object "..object + end + table.insert(objects, object) + end + local module_name = name:match("([^.]*)$").."."..util.matchquote(cfg.lib_extension) + if moddir ~= "" then + module_name = dir.path(moddir, module_name) + ok, err = fs.make_dir(moddir) + if not ok then return nil, err end + end + lib_modules[module_name] = dir.path(libdir, module_name) + ok = compile_library(module_name, objects, info.libraries, info.libdirs, name) + if not ok then + return nil, "Failed compiling module "..module_name + end + --[[ TODO disable static libs until we fix the conflict in the manifest, which will take extending the manifest format. + module_name = name:match("([^.]*)$").."."..util.matchquote(cfg.static_lib_extension) + if moddir ~= "" then + module_name = dir.path(moddir, module_name) + end + lib_modules[module_name] = dir.path(libdir, module_name) + ok = compile_static_library(module_name, objects, info.libraries, info.libdirs, name) + if not ok then + return nil, "Failed compiling static library "..module_name + end + ]] + end + end + if not no_install then + for _, mods in ipairs({{ tbl = lua_modules, perms = "read" }, { tbl = lib_modules, perms = "exec" }}) do + for name, dest in pairs(mods.tbl) do + fs.make_dir(dir.dir_name(dest)) + ok, err = fs.copy(name, dest, mods.perms) + if not ok then + return nil, "Failed installing "..name.." in "..dest..": "..err + end + end + end + if fs.is_dir("lua") then + ok, err = fs.copy_contents("lua", luadir) + if not ok then + return nil, "Failed copying contents of 'lua' directory: "..err + end + end + end + return true +end + +return builtin diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/build/cmake.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/build/cmake.lua new file mode 100644 index 000000000..b7a4786ef --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/build/cmake.lua @@ -0,0 +1,78 @@ + +--- Build back-end for CMake-based modules. +local cmake = {} + +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") + +--- Driver function for the "cmake" build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors occurred, +-- nil and an error message otherwise. +function cmake.run(rockspec, no_install) + assert(rockspec:type() == "rockspec") + local build = rockspec.build + local variables = build.variables or {} + + -- Pass Env variables + variables.CMAKE_MODULE_PATH=os.getenv("CMAKE_MODULE_PATH") + variables.CMAKE_LIBRARY_PATH=os.getenv("CMAKE_LIBRARY_PATH") + variables.CMAKE_INCLUDE_PATH=os.getenv("CMAKE_INCLUDE_PATH") + + util.variable_substitutions(variables, rockspec.variables) + + local ok, err_msg = fs.is_tool_available(rockspec.variables.CMAKE, "CMake") + if not ok then + return nil, err_msg + end + + -- If inline cmake is present create CMakeLists.txt from it. + if type(build.cmake) == "string" then + local cmake_handler = assert(io.open(fs.current_dir().."/CMakeLists.txt", "w")) + cmake_handler:write(build.cmake) + cmake_handler:close() + end + + -- Execute cmake with variables. + local args = "" + + -- Try to pick the best generator. With msvc and x64, CMake does not select it by default so we need to be explicit. + if cfg.cmake_generator then + args = args .. ' -G"'..cfg.cmake_generator.. '"' + elseif cfg.is_platform("windows") and cfg.target_cpu:match("x86_64$") then + args = args .. " -DCMAKE_GENERATOR_PLATFORM=x64" + end + + for k,v in pairs(variables) do + args = args .. ' -D' ..k.. '="' ..tostring(v).. '"' + end + + if not fs.execute_string(rockspec.variables.CMAKE.." -H. -Bbuild.luarocks "..args) then + return nil, "Failed cmake." + end + + local do_build, do_install + if rockspec:format_is_at_least("3.0") then + do_build = (build.build_pass == nil) and true or build.build_pass + do_install = (build.install_pass == nil) and true or build.install_pass + else + do_build = true + do_install = true + end + + if do_build then + if not fs.execute_string(rockspec.variables.CMAKE.." --build build.luarocks --config Release") then + return nil, "Failed building." + end + end + if do_install and not no_install then + if not fs.execute_string(rockspec.variables.CMAKE.." --build build.luarocks --target install --config Release") then + return nil, "Failed installing." + end + end + + return true +end + +return cmake diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/build/command.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/build/command.lua new file mode 100644 index 000000000..b0c4aa791 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/build/command.lua @@ -0,0 +1,41 @@ + +--- Build back-end for raw listing of commands in rockspec files. +local command = {} + +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") + +--- Driver function for the "command" build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors occurred, +-- nil and an error message otherwise. +function command.run(rockspec, not_install) + assert(rockspec:type() == "rockspec") + + local build = rockspec.build + + util.variable_substitutions(build, rockspec.variables) + + local env = { + CC = cfg.variables.CC, + --LD = cfg.variables.LD, + --CFLAGS = cfg.variables.CFLAGS, + } + + if build.build_command then + util.printout(build.build_command) + if not fs.execute_env(env, build.build_command) then + return nil, "Failed building." + end + end + if build.install_command and not not_install then + util.printout(build.install_command) + if not fs.execute_env(env, build.install_command) then + return nil, "Failed installing." + end + end + return true +end + +return command diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/build/make.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/build/make.lua new file mode 100644 index 000000000..4345ddffb --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/build/make.lua @@ -0,0 +1,98 @@ + +--- Build back-end for using Makefile-based packages. +local make = {} + +local unpack = unpack or table.unpack + +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") + +--- Call "make" with given target and variables +-- @param make_cmd string: the make command to be used (typically +-- configured through variables.MAKE in the config files, or +-- the appropriate platform-specific default). +-- @param pass boolean: If true, run make; if false, do nothing. +-- @param target string: The make target; an empty string indicates +-- the default target. +-- @param variables table: A table containing string-string key-value +-- pairs representing variable assignments to be passed to make. +-- @return boolean: false if any errors occurred, true otherwise. +local function make_pass(make_cmd, pass, target, variables) + assert(type(pass) == "boolean") + assert(type(target) == "string") + assert(type(variables) == "table") + + local assignments = {} + for k,v in pairs(variables) do + table.insert(assignments, k.."="..v) + end + if pass then + return fs.execute(make_cmd.." "..target, unpack(assignments)) + else + return true + end +end + +--- Driver function for the "make" build back-end. +-- @param rockspec table: the loaded rockspec. +-- @return boolean or (nil, string): true if no errors occurred, +-- nil and an error message otherwise. +function make.run(rockspec, not_install) + assert(rockspec:type() == "rockspec") + + local build = rockspec.build + + if build.build_pass == nil then build.build_pass = true end + if build.install_pass == nil then build.install_pass = true end + build.build_variables = build.build_variables or {} + build.install_variables = build.install_variables or {} + build.build_target = build.build_target or "" + build.install_target = build.install_target or "install" + local makefile = build.makefile or cfg.makefile + if makefile then + -- Assumes all make's accept -f. True for POSIX make, GNU make and Microsoft nmake. + build.build_target = "-f "..makefile.." "..build.build_target + build.install_target = "-f "..makefile.." "..build.install_target + end + + if build.variables then + for var, val in pairs(build.variables) do + build.build_variables[var] = val + build.install_variables[var] = val + end + end + + util.warn_if_not_used(build.build_variables, { CFLAGS=true }, "variable %s was not passed in build_variables") + + util.variable_substitutions(build.build_variables, rockspec.variables) + util.variable_substitutions(build.install_variables, rockspec.variables) + + local auto_variables = { "CC" } + + for _, variable in pairs(auto_variables) do + if not build.build_variables[variable] then + build.build_variables[variable] = rockspec.variables[variable] + end + if not build.install_variables[variable] then + build.install_variables[variable] = rockspec.variables[variable] + end + end + + -- backwards compatibility + local make_cmd = cfg.make or rockspec.variables.MAKE + + local ok = make_pass(make_cmd, build.build_pass, build.build_target, build.build_variables) + if not ok then + return nil, "Failed building." + end + if not not_install then + ok = make_pass(make_cmd, build.install_pass, build.install_target, build.install_variables) + if not ok then + return nil, "Failed installing." + end + end + return true +end + +return make diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd.lua new file mode 100644 index 000000000..c5ac2df78 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd.lua @@ -0,0 +1,661 @@ + +--- Functions for command-line scripts. +local cmd = {} + +local manif = require("luarocks.manif") +local util = require("luarocks.util") +local path = require("luarocks.path") +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local fun = require("luarocks.fun") +local fs = require("luarocks.fs") +local argparse = require("luarocks.argparse") + +local unpack = table.unpack or unpack +local pack = table.pack or function(...) return { n = select("#", ...), ... } end + +local hc_ok, hardcoded = pcall(require, "luarocks.core.hardcoded") +if not hc_ok then + hardcoded = {} +end + +local program = util.this_program("luarocks") + +cmd.errorcodes = { + OK = 0, + UNSPECIFIED = 1, + PERMISSIONDENIED = 2, + CONFIGFILE = 3, + CRASH = 99 +} + +local function check_popen() + local popen_ok, popen_result = pcall(io.popen, "") + if popen_ok then + if popen_result then + popen_result:close() + end + else + io.stderr:write("Your version of Lua does not support io.popen,\n") + io.stderr:write("which is required by LuaRocks. Please check your Lua installation.\n") + os.exit(cmd.errorcodes.UNSPECIFIED) + end +end + +local process_tree_args +do + local function replace_tree(args, root, tree) + root = dir.normalize(root) + args.tree = root + path.use_tree(tree or root) + end + + local function strip_trailing_slashes() + if type(cfg.root_dir) == "string" then + cfg.root_dir = cfg.root_dir:gsub("/+$", "") + else + cfg.root_dir.root = cfg.root_dir.root:gsub("/+$", "") + end + cfg.rocks_dir = cfg.rocks_dir:gsub("/+$", "") + cfg.deploy_bin_dir = cfg.deploy_bin_dir:gsub("/+$", "") + cfg.deploy_lua_dir = cfg.deploy_lua_dir:gsub("/+$", "") + cfg.deploy_lib_dir = cfg.deploy_lib_dir:gsub("/+$", "") + end + + process_tree_args = function(args, project_dir) + + if args.global then + cfg.local_by_default = false + end + + if args.tree then + local named = false + for _, tree in ipairs(cfg.rocks_trees) do + if type(tree) == "table" and args.tree == tree.name then + if not tree.root then + return nil, "Configuration error: tree '"..tree.name.."' has no 'root' field." + end + replace_tree(args, tree.root, tree) + named = true + break + end + end + if not named then + local root_dir = fs.absolute_name(args.tree) + replace_tree(args, root_dir) + end + elseif args["local"] then + if fs.is_superuser() then + return nil, "The --local flag is meant for operating in a user's home directory.\n".. + "You are running as a superuser, which is intended for system-wide operation.\n".. + "To force using the superuser's home, use --tree explicitly." + else + replace_tree(args, cfg.home_tree) + end + elseif args.project_tree then + local tree = args.project_tree + table.insert(cfg.rocks_trees, 1, { name = "project", root = tree } ) + manif.load_rocks_tree_manifests() + path.use_tree(tree) + elseif cfg.local_by_default then + if cfg.home_tree then + replace_tree(args, cfg.home_tree) + end + elseif project_dir then + local project_tree = project_dir .. "/lua_modules" + table.insert(cfg.rocks_trees, 1, { name = "project", root = project_tree } ) + manif.load_rocks_tree_manifests() + path.use_tree(project_tree) + else + local trees = cfg.rocks_trees + path.use_tree(trees[#trees]) + end + + strip_trailing_slashes() + + cfg.variables.ROCKS_TREE = cfg.rocks_dir + cfg.variables.SCRIPTS_DIR = cfg.deploy_bin_dir + + return true + end +end + +local function process_server_args(args) + if args.server then + local user_servers = {} + for server in string.gmatch(args.server, "%S+") do + local protocol, pathname = dir.split_url(server) + table.insert(user_servers, protocol.."://"..pathname) + end + cfg.rocks_servers = fun.concat(user_servers, cfg.rocks_servers) + end + + if args.dev then + local append_dev = function(s) return dir.path(s, "dev") end + local dev_servers = fun.traverse(cfg.rocks_servers, append_dev) + cfg.rocks_servers = fun.concat(dev_servers, cfg.rocks_servers) + end + + if args.only_server then + if args.dev then + return nil, "--only-server cannot be used with --dev" + end + if args.server then + return nil, "--only-server cannot be used with --server" + end + cfg.rocks_servers = { args.only_server } + end + + return true +end + +local function error_handler(err) + if not debug then + return err + end + local mode = "Arch.: " .. (cfg and cfg.arch or "unknown") + if package.config:sub(1, 1) == "\\" then + if cfg and cfg.fs_use_modules then + mode = mode .. " (fs_use_modules = true)" + end + end + if cfg and cfg.is_binary then + mode = mode .. " (binary)" + end + return debug.traceback("LuaRocks "..cfg.program_version.. + " bug (please report at https://github.com/luarocks/luarocks/issues).\n".. + mode.."\n"..err, 2) +end + +--- Display an error message and exit. +-- @param message string: The error message. +-- @param exitcode number: the exitcode to use +local function die(message, exitcode) + assert(type(message) == "string", "bad error, expected string, got: " .. type(message)) + assert(exitcode == nil or type(exitcode) == "number", "bad error, expected number, got: " .. type(exitcode) .. " - " .. tostring(exitcode)) + util.printerr("\nError: "..message) + + local ok, err = xpcall(util.run_scheduled_functions, error_handler) + if not ok then + util.printerr("\nError: "..err) + exitcode = cmd.errorcodes.CRASH + end + + os.exit(exitcode or cmd.errorcodes.UNSPECIFIED) +end + +local function search_lua_in_path(lua_version, verbose) + local path_sep = (package.config:sub(1, 1) == "\\" and ";" or ":") + local all_tried = {} + for bindir in (os.getenv("PATH") or ""):gmatch("[^"..path_sep.."]+") do + local parentdir = dir.path((bindir:gsub("[\\/][^\\/]+[\\/]?$", ""))) + local detected, tried = util.find_lua(parentdir, lua_version) + if detected then + return detected + else + table.insert(all_tried, tried) + end + bindir = dir.path(bindir) + detected = util.find_lua(bindir, lua_version) + if detected then + return detected + else + table.insert(all_tried, tried) + end + end + return nil, "Could not find " .. + (lua_version and "Lua " .. lua_version or "Lua") .. + " in PATH." .. + (verbose and " Tried:\n" .. table.concat(all_tried, "\n") or "") +end + +local init_config +do + local detect_config_via_args + do + local function find_project_dir(project_tree) + if project_tree then + return project_tree:gsub("[/\\][^/\\]+$", ""), true + else + local try = "." + for _ = 1, 10 do -- FIXME detect when root dir was hit instead + if util.exists(try .. "/.luarocks") and util.exists(try .. "/lua_modules") then + return try, false + elseif util.exists(try .. "/.luarocks-no-project") then + break + end + try = try .. "/.." + end + end + return nil + end + + local function find_default_lua_version(args, project_dir) + if hardcoded.FORCE_CONFIG then + return nil + end + + local dirs = {} + if project_dir then + table.insert(dirs, dir.path(project_dir, ".luarocks")) + end + if cfg.homeconfdir then + table.insert(dirs, cfg.homeconfdir) + end + table.insert(dirs, cfg.sysconfdir) + for _, d in ipairs(dirs) do + local f = dir.path(d, "default-lua-version.lua") + local mod, err = loadfile(f, "t") + if mod then + local pok, ver = pcall(mod) + if pok and type(ver) == "string" and ver:match("%d+.%d+") then + if args.verbose then + util.printout("Defaulting to Lua " .. ver .. " based on " .. f .. " ...") + end + return ver + end + end + end + return nil + end + + local function find_version_from_config(dirname) + return fun.find(util.lua_versions("descending"), function(v) + if util.exists(dir.path(dirname, ".luarocks", "config-"..v..".lua")) then + return v + end + end) + end + + local function detect_lua_via_args(args, project_dir) + local lua_version = args.lua_version + or find_default_lua_version(args, project_dir) + or (project_dir and find_version_from_config(project_dir)) + + if args.lua_dir then + local detected, err = util.find_lua(args.lua_dir, lua_version) + if not detected then + local suggestion = (not args.lua_version) + and "\nYou may want to specify a different Lua version with --lua-version\n" + or "" + die(err .. suggestion) + end + return detected + end + + if lua_version then + local detected = search_lua_in_path(lua_version) + if detected then + return detected + end + return { + lua_version = lua_version, + } + end + + return {} + end + + detect_config_via_args = function(args) + local project_dir, given + if not args.no_project then + project_dir, given = find_project_dir(args.project_tree) + end + + local detected = detect_lua_via_args(args, project_dir) + if args.lua_version then + detected.given_lua_version = args.lua_version + end + if args.lua_dir then + detected.given_lua_dir = args.lua_dir + end + if given then + detected.given_project_dir = project_dir + end + detected.project_dir = project_dir + return detected + end + end + + init_config = function(args) + local detected = detect_config_via_args(args) + + local ok, err = cfg.init(detected, util.warning) + if not ok then + return nil, err + end + + return (detected.lua_dir ~= nil) + end +end + +local variables_help = [[ +Variables: + Variables from the "variables" table of the configuration file can be + overridden with VAR=VALUE assignments. + +]] + +local function get_status(status) + return status and "ok" or "not found" +end + +local function use_to_fix_location(key) + local buf = " ****************************************\n" + buf = buf .. " Use the command\n\n" + buf = buf .. " luarocks config " .. key .. "

\n\n" + buf = buf .. " to fix the location\n" + buf = buf .. " ****************************************\n" + return buf +end + +local function get_config_text(cfg) -- luacheck: ignore 431 + local deps = require("luarocks.deps") + + local libdir_ok = deps.check_lua_libdir(cfg.variables) + local incdir_ok = deps.check_lua_incdir(cfg.variables) + local bindir_ok = cfg.variables.LUA_BINDIR and fs.exists(cfg.variables.LUA_BINDIR) + local luadir_ok = cfg.variables.LUA_DIR and fs.exists(cfg.variables.LUA_DIR) + local lua_ok = cfg.variables.LUA and fs.exists(cfg.variables.LUA) + + local buf = "Configuration:\n" + buf = buf.." Lua:\n" + buf = buf.." Version : "..cfg.lua_version.."\n" + if cfg.luajit_version then + buf = buf.." LuaJIT : "..cfg.luajit_version.."\n" + end + buf = buf.." Interpreter: "..(cfg.variables.LUA or "").." ("..get_status(lua_ok)..")\n" + buf = buf.." LUA_DIR : "..(cfg.variables.LUA_DIR or "").." ("..get_status(luadir_ok)..")\n" + if not lua_ok then + buf = buf .. use_to_fix_location("lua_dir") + end + buf = buf.." LUA_BINDIR : "..(cfg.variables.LUA_BINDIR or "").." ("..get_status(bindir_ok)..")\n" + buf = buf.." LUA_INCDIR : "..(cfg.variables.LUA_INCDIR or "").." ("..get_status(incdir_ok)..")\n" + if lua_ok and not incdir_ok then + buf = buf .. use_to_fix_location("variables.LUA_INCDIR") + end + buf = buf.." LUA_LIBDIR : "..(cfg.variables.LUA_LIBDIR or "").." ("..get_status(libdir_ok)..")\n" + if lua_ok and not libdir_ok then + buf = buf .. use_to_fix_location("variables.LUA_LIBDIR") + end + + buf = buf.."\n Configuration files:\n" + local conf = cfg.config_files + buf = buf.." System : "..fs.absolute_name(conf.system.file).." ("..get_status(conf.system.found)..")\n" + if conf.user.file then + buf = buf.." User : "..fs.absolute_name(conf.user.file).." ("..get_status(conf.user.found)..")\n" + else + buf = buf.." User : disabled in this LuaRocks installation.\n" + end + if conf.project then + buf = buf.." Project : "..fs.absolute_name(conf.project.file).." ("..get_status(conf.project.found)..")\n" + end + buf = buf.."\n Rocks trees in use: \n" + for _, tree in ipairs(cfg.rocks_trees) do + if type(tree) == "string" then + buf = buf.." "..fs.absolute_name(tree) + else + local name = tree.name and " (\""..tree.name.."\")" or "" + buf = buf.." "..fs.absolute_name(tree.root)..name + end + buf = buf .. "\n" + end + + return buf +end + +local function get_parser(description, cmd_modules) + local basename = dir.base_name(program) + local parser = argparse( + basename, "LuaRocks "..cfg.program_version..", the Lua package manager\n\n".. + program.." - "..description, variables_help.."Run '"..basename.. + "' without any arguments to see the configuration.") + :help_max_width(80) + :add_help_command() + :command_target("command") + :require_command(false) + + parser:flag("--version", "Show version info and exit.") + :action(function() + util.printout(program.." "..cfg.program_version) + util.printout(description) + util.printout() + os.exit(cmd.errorcodes.OK) + end) + parser:flag("--dev", "Enable the sub-repositories in rocks servers for ".. + "rockspecs of in-development versions.") + parser:option("--server", "Fetch rocks/rockspecs from this servers ".. + "(takes priority over config file).") + :hidden_name("--from") + :argname("<'server1 server2 server3'>") + parser:option("--only-server", "Fetch rocks/rockspecs from this server only ".. + "(overrides any entries in the config file).") + :argname("") + :hidden_name("--only-from") + parser:option("--only-sources", "Restrict downloads to paths matching the given URL.") + :argname("") + :hidden_name("--only-sources-from") + parser:option("--namespace", "Specify the rocks server namespace to use.") + :convert(string.lower) + parser:option("--lua-dir", "Which Lua installation to use.") + :argname("") + parser:option("--lua-version", "Which Lua version to use.") + :argname("") + :convert(function(s) return (s:match("^%d+%.%d+$")) end) + parser:option("--tree", "Which tree to operate on.") + :hidden_name("--to") + parser:flag("--local", "Use the tree in the user's home directory.\n".. + "To enable it, see '"..program.." help path'.") + parser:flag("--global", "Use the system tree when `local_by_default` is `true`.") + parser:flag("--no-project", "Do not use project tree even if running from a project folder.") + parser:flag("--verbose", "Display verbose output of commands executed.") + parser:option("--timeout", "Timeout on network operations, in seconds.\n".. + "0 means no timeout (wait forever). Default is ".. + tostring(cfg.connection_timeout)..".") + :argname("") + :convert(tonumber) + + -- Used internally to force the use of a particular project tree + parser:option("--project-tree"):hidden(true) + + for _, module in util.sortedpairs(cmd_modules) do + module.add_to_parser(parser) + end + + return parser +end + +--- Main command-line processor. +-- Parses input arguments and calls the appropriate driver function +-- to execute the action requested on the command-line, forwarding +-- to it any additional arguments passed by the user. +-- @param description string: Short summary description of the program. +-- @param commands table: contains the loaded modules representing commands. +-- @param external_namespace string: where to look for external commands. +-- @param ... string: Arguments given on the command-line. +function cmd.run_command(description, commands, external_namespace, ...) + + check_popen() + + -- Preliminary initialization + cfg.init() + + fs.init() + + for _, module_name in ipairs(fs.modules(external_namespace)) do + if not commands[module_name] then + commands[module_name] = external_namespace.."."..module_name + end + end + + local cmd_modules = {} + for name, module in pairs(commands) do + local pok, mod = pcall(require, module) + if pok and type(mod) == "table" then + local original_command = mod.command + if original_command then + if not mod.add_to_parser then + mod.add_to_parser = function(parser) + parser:command(name, mod.help, util.see_also()) + :summary(mod.help_summary) + :handle_options(false) + :argument("input") + :args("*") + end + + mod.command = function(args) + return original_command(args, unpack(args.input)) + end + end + cmd_modules[name] = mod + else + util.warning("command module " .. module .. " does not implement command(), skipping") + end + else + util.warning("failed to load command module " .. module) + end + end + + local function process_cmdline_vars(...) + local args = pack(...) + local cmdline_vars = {} + local last = args.n + for i = 1, args.n do + if args[i] == "--" then + last = i - 1 + break + end + end + for i = last, 1, -1 do + local arg = args[i] + if arg:match("^[^-][^=]*=") then + local var, val = arg:match("^([A-Z_][A-Z0-9_]*)=(.*)") + if val then + cmdline_vars[var] = val + table.remove(args, i) + else + die("Invalid assignment: "..arg) + end + end + end + + return args, cmdline_vars + end + + local args, cmdline_vars = process_cmdline_vars(...) + local parser = get_parser(description, cmd_modules) + args = parser:parse(args) + + -- Compatibility for old flag + if args.nodeps then + args.deps_mode = "none" + end + + if args.timeout then -- setting it in the config file will kick-in earlier in the process + cfg.connection_timeout = args.timeout + end + + if args.command == "config" then + if args.key == "lua_version" and args.value then + args.lua_version = args.value + elseif args.key == "lua_dir" and args.value then + args.lua_dir = args.value + end + end + + ----------------------------------------------------------------------------- + local lua_found, err = init_config(args) + if err then + die(err) + end + ----------------------------------------------------------------------------- + + -- Now that the config is fully loaded, reinitialize fs using the full + -- feature set. + fs.init() + if cfg.variables.FORCE_HARDCODED and cfg.variables.LUA_INTERPRETER then + lua_found = true + end + + -- if the Lua interpreter wasn't explicitly found before cfg.init, + -- try again now. + local tried + if not lua_found then + if cfg.variables.LUA_DIR then + lua_found, tried = util.find_lua(cfg.variables.LUA_DIR, cfg.lua_version, args.verbose) + else + lua_found, tried = search_lua_in_path(cfg.lua_version, args.verbose) + end + end + + if not lua_found and args.command ~= "config" and args.command ~= "help" then + util.warning(tried .. + "\nModules may not install with the correct configurations. " .. + "You may want to configure the path prefix to your build " .. + "of Lua " .. cfg.lua_version .. " using\n\n" .. + " luarocks config --local lua_dir \n") + end + cfg.lua_found = lua_found + + if cfg.project_dir then + cfg.project_dir = fs.absolute_name(cfg.project_dir) + end + + if args.verbose then + cfg.verbose = true + fs.verbose() + end + + if (not fs.current_dir()) or fs.current_dir() == "" then + die("Current directory does not exist. Please run LuaRocks from an existing directory.") + end + + local ok, err = process_tree_args(args, cfg.project_dir) + if not ok then + die(err) + end + + ok, err = process_server_args(args) + if not ok then + die(err) + end + + if args.only_sources then + cfg.only_sources_from = args.only_sources + end + + for k, v in pairs(cmdline_vars) do + cfg.variables[k] = v + end + + -- if running as superuser, use system cache dir + if fs.is_superuser() then + cfg.local_cache = dir.path(fs.system_cache_dir(), "luarocks") + end + + if args.no_manifest then + cfg.no_manifest = true + end + + if not args.command then + parser:epilog(variables_help..get_config_text(cfg)) + util.printout() + util.printout(parser:get_help()) + util.printout() + os.exit(cmd.errorcodes.OK) + end + + local cmd_mod = cmd_modules[args.command] + local call_ok, ok, err, exitcode = xpcall(function() + return cmd_mod.command(args) + end, error_handler) + + if not call_ok then + die(ok, cmd.errorcodes.CRASH) + elseif not ok then + die(err, exitcode) + end + util.run_scheduled_functions() +end + +return cmd diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/build.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/build.lua new file mode 100644 index 000000000..56f0e7576 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/build.lua @@ -0,0 +1,183 @@ + +--- Module implementing the LuaRocks "build" command. +-- Builds a rock, compiling its C parts if any. +local cmd_build = {} + +local pack = require("luarocks.pack") +local path = require("luarocks.path") +local util = require("luarocks.util") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local deps = require("luarocks.deps") +local remove = require("luarocks.remove") +local cfg = require("luarocks.core.cfg") +local build = require("luarocks.build") +local writer = require("luarocks.manif.writer") +local search = require("luarocks.search") +local make = require("luarocks.cmd.make") +local cmd = require("luarocks.cmd") + +function cmd_build.add_to_parser(parser) + local cmd = parser:command("build", "Build and install a rock, compiling its C parts if any.\n".. -- luacheck: ignore 431 + "If the sources contain a luarocks.lock file, uses it as an authoritative source for ".. + "exact version of dependencies.\n".. + "If no arguments are given, behaves as luarocks make.", util.see_also()) + :summary("Build/compile a rock.") + + cmd:argument("rock", "A rockspec file, a source rock file, or the name of ".. + "a rock to be fetched from a repository.") + :args("?") + :action(util.namespaced_name_action) + cmd:argument("version", "Rock version.") + :args("?") + + cmd:flag("--only-deps --deps-only", "Install only the dependencies of the rock.") + cmd:option("--branch", "Override the `source.branch` field in the loaded ".. + "rockspec. Allows to specify a different branch to fetch. Particularly ".. + 'for "dev" rocks.') + :argname("") + cmd:flag("--pin", "Create a luarocks.lock file listing the exact ".. + "versions of each dependency found for this rock (recursively), ".. + "and store it in the rock's directory. ".. + "Ignores any existing luarocks.lock file in the rock's sources.") + make.cmd_options(cmd) +end + +--- Build and install a rock. +-- @param rock_filename string: local or remote filename of a rock. +-- @param opts table: build options +-- @return boolean or (nil, string, [string]): True if build was successful, +-- or false and an error message and an optional error code. +local function build_rock(rock_filename, opts) + assert(type(rock_filename) == "string") + assert(opts:type() == "build.opts") + + local ok, err, errcode + + local unpack_dir + unpack_dir, err, errcode = fetch.fetch_and_unpack_rock(rock_filename, nil, opts.verify) + if not unpack_dir then + return nil, err, errcode + end + + local rockspec_filename = path.rockspec_name_from_rock(rock_filename) + + ok, err = fs.change_dir(unpack_dir) + if not ok then return nil, err end + + local rockspec + rockspec, err, errcode = fetch.load_rockspec(rockspec_filename) + if not rockspec then + return nil, err, errcode + end + + ok, err, errcode = build.build_rockspec(rockspec, opts) + + fs.pop_dir() + return ok, err, errcode +end + +local function do_build(name, namespace, version, opts) + assert(type(name) == "string") + assert(type(namespace) == "string" or not namespace) + assert(version == nil or type(version) == "string") + assert(opts:type() == "build.opts") + + local url, err + if name:match("%.rockspec$") or name:match("%.rock$") then + url = name + else + url, err = search.find_src_or_rockspec(name, namespace, version, opts.check_lua_versions) + if not url then + return nil, err + end + end + + if url:match("%.rockspec$") then + local rockspec, err = fetch.load_rockspec(url, nil, opts.verify) + if not rockspec then + return nil, err + end + return build.build_rockspec(rockspec, opts) + end + + if url:match("%.src%.rock$") then + opts.need_to_fetch = false + end + + return build_rock(url, opts) +end + +--- Driver function for "build" command. +-- If a package name is given, forwards the request to "search" and, +-- if returned a result, installs the matching rock. +-- When passing a package name, a version number may also be given. +-- @return boolean or (nil, string, exitcode): True if build was successful; nil and an +-- error message otherwise. exitcode is optionally returned. +function cmd_build.command(args) + if not args.rock then + return make.command(args) + end + + local opts = build.opts({ + need_to_fetch = true, + minimal_mode = false, + deps_mode = deps.get_deps_mode(args), + build_only_deps = not not (args.only_deps and not args.pack_binary_rock), + namespace = args.namespace, + branch = args.branch, + verify = not not args.verify, + check_lua_versions = not not args.check_lua_versions, + pin = not not args.pin, + no_install = false + }) + + if args.sign and not args.pack_binary_rock then + return nil, "In the build command, --sign is meant to be used only with --pack-binary-rock" + end + + if args.pack_binary_rock then + return pack.pack_binary_rock(args.rock, args.namespace, args.version, args.sign, function() + local name, version = do_build(args.rock, args.namespace, args.version, opts) + if name and args.no_doc then + util.remove_doc_dir(name, version) + end + return name, version + end) + end + + local ok, err = fs.check_command_permissions(args) + if not ok then + return nil, err, cmd.errorcodes.PERMISSIONDENIED + end + + local name, version = do_build(args.rock, args.namespace, args.version, opts) + if not name then + return nil, version + end + + if args.no_doc then + util.remove_doc_dir(name, version) + end + + if opts.build_only_deps then + util.printout("Stopping after installing dependencies for " ..name.." "..version) + util.printout() + else + if (not args.keep) and not cfg.keep_other_versions then + local ok, err, warn = remove.remove_other_versions(name, version, args.force, args.force_fast) + if not ok then + return nil, err + elseif warn then + util.printerr(err) + end + end + end + + if opts.deps_mode ~= "none" then + writer.check_dependencies(nil, deps.get_deps_mode(args)) + end + return name, version +end + +return cmd_build diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/config.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/config.lua new file mode 100644 index 000000000..6a73c7ff1 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/config.lua @@ -0,0 +1,326 @@ +--- Module implementing the LuaRocks "config" command. +-- Queries information about the LuaRocks configuration. +local config_cmd = {} + +local persist = require("luarocks.persist") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local deps = require("luarocks.deps") +local dir = require("luarocks.dir") +local fs = require("luarocks.fs") + +function config_cmd.add_to_parser(parser) + local cmd = parser:command("config", [[ +Query information about the LuaRocks configuration. + +* When given a configuration key, it prints the value of that key according to + the currently active configuration (taking into account all config files and + any command-line flags passed) + + Examples: + luarocks config lua_interpreter + luarocks config variables.LUA_INCDIR + luarocks config lua_version + +* When given a configuration key and a value, it overwrites the config file (see + the --scope option below to determine which) and replaces the value of the + given key with the given value. + + * `lua_dir` is a special key as it checks for a valid Lua installation + (equivalent to --lua-dir) and sets several keys at once. + * `lua_version` is a special key as it changes the default Lua version + used by LuaRocks commands (equivalent to passing --lua-version). + + Examples: + luarocks config variables.OPENSSL_DIR /usr/local/openssl + luarocks config lua_dir /usr/local + luarocks config lua_version 5.3 + +* When given a configuration key and --unset, it overwrites the config file (see + the --scope option below to determine which) and deletes that key from the + file. + + Example: luarocks config variables.OPENSSL_DIR --unset + +* When given no arguments, it prints the entire currently active configuration, + resulting from reading the config files from all scopes. + + Example: luarocks config]], util.see_also([[ + https://github.com/luarocks/luarocks/wiki/Config-file-format + for detailed information on the LuaRocks config file format. +]])) + :summary("Query information about the LuaRocks configuration.") + + cmd:argument("key", "The configuration key.") + :args("?") + cmd:argument("value", "The configuration value.") + :args("?") + + cmd:option("--scope", "The scope indicates which config file should be rewritten.\n".. + '* Using a wrapper created with `luarocks init`, the default is "project".\n'.. + '* Using --local (or when `local_by_default` is `true`), the default is "user".\n'.. + '* Otherwise, the default is "system".') + :choices({"system", "user", "project"}) + cmd:flag("--unset", "Delete the key from the configuration file.") + cmd:flag("--json", "Output as JSON.") + + -- Deprecated flags + cmd:flag("--lua-incdir"):hidden(true) + cmd:flag("--lua-libdir"):hidden(true) + cmd:flag("--lua-ver"):hidden(true) + cmd:flag("--system-config"):hidden(true) + cmd:flag("--user-config"):hidden(true) + cmd:flag("--rock-trees"):hidden(true) +end + +local function config_file(conf) + print(dir.normalize(conf.file)) + if conf.found then + return true + else + return nil, "file not found" + end +end + +local cfg_skip = { + errorcodes = true, + flags = true, + platforms = true, + root_dir = true, + upload_servers = true, +} + +local function should_skip(k, v) + return type(v) == "function" or cfg_skip[k] +end + +local function cleanup(tbl) + local copy = {} + for k, v in pairs(tbl) do + if not should_skip(k, v) then + copy[k] = v + end + end + return copy +end + +local function traverse_varstring(var, tbl, fn, missing_parent) + local k, r = var:match("^%[([0-9]+)%]%.(.*)$") + if k then + k = tonumber(k) + else + k, r = var:match("^([^.[]+)%.(.*)$") + if not k then + k, r = var:match("^([^[]+)(%[.*)$") + end + end + + if k then + if not tbl[k] and missing_parent then + missing_parent(tbl, k) + end + + if tbl[k] then + return traverse_varstring(r, tbl[k], fn, missing_parent) + else + return nil, "Unknown entry " .. k + end + end + + local i = var:match("^%[([0-9]+)%]$") + if i then + var = tonumber(i) + end + + return fn(tbl, var) +end + +local function print_json(value) + local json_ok, json = util.require_json() + if not json_ok then + return nil, "A JSON library is required for this command. "..json + end + + print(json.encode(value)) + return true +end + +local function print_entry(var, tbl, is_json) + return traverse_varstring(var, tbl, function(t, k) + if not t[k] then + return nil, "Unknown entry " .. k + end + local val = t[k] + + if not should_skip(var, val) then + if is_json then + return print_json(val) + elseif type(val) == "string" then + print(val) + else + persist.write_value(io.stdout, val) + end + end + return true + end) +end + +local function infer_type(var) + local typ + traverse_varstring(var, cfg, function(t, k) + if t[k] ~= nil then + typ = type(t[k]) + end + end) + return typ +end + +local function write_entries(keys, scope, do_unset) + if scope == "project" and not cfg.config_files.project then + return nil, "Current directory is not part of a project. You may want to run `luarocks init`." + end + + local tbl, err = persist.load_config_file_if_basic(cfg.config_files[scope].file, cfg) + if not tbl then + return nil, err + end + + for var, val in util.sortedpairs(keys) do + traverse_varstring(var, tbl, function(t, k) + if do_unset then + t[k] = nil + else + local typ = infer_type(var) + local v + if typ == "number" and tonumber(val) then + v = tonumber(val) + elseif typ == "boolean" and val == "true" then + v = true + elseif typ == "boolean" and val == "false" then + v = false + else + v = val + end + t[k] = v + keys[var] = v + end + return true + end, function(p, k) + p[k] = {} + end) + end + + local ok, err = persist.save_from_table(cfg.config_files[scope].file, tbl) + if ok then + print(do_unset and "Removed" or "Wrote") + for var, val in util.sortedpairs(keys) do + if do_unset then + print(("\t%s"):format(var)) + else + print(("\t%s = %q"):format(var, val)) + end + end + print(do_unset and "from" or "to") + print("\t" .. cfg.config_files[scope].file) + return true + else + return nil, err + end +end + +local function get_scope(args) + return args.scope + or (args["local"] and "user") + or (args.project_tree and "project") + or (cfg.local_by_default and "user") + or (fs.is_writable(cfg.config_files["system"].file and "system")) + or "user" +end + +--- Driver function for "config" command. +-- @return boolean: True if succeeded, nil on errors. +function config_cmd.command(args) + deps.check_lua_incdir(cfg.variables, args.lua_version or cfg.lua_version) + deps.check_lua_libdir(cfg.variables, args.lua_version or cfg.lua_version) + + -- deprecated flags + if args.lua_incdir then + print(cfg.variables.LUA_INCDIR) + return true + end + if args.lua_libdir then + print(cfg.variables.LUA_LIBDIR) + return true + end + if args.lua_ver then + print(cfg.lua_version) + return true + end + if args.system_config then + return config_file(cfg.config_files.system) + end + if args.user_config then + return config_file(cfg.config_files.user) + end + if args.rock_trees then + for _, tree in ipairs(cfg.rocks_trees) do + if type(tree) == "string" then + util.printout(dir.normalize(tree)) + else + local name = tree.name and "\t"..tree.name or "" + util.printout(dir.normalize(tree.root)..name) + end + end + return true + end + + if args.key == "lua_version" and args.value then + local scope = get_scope(args) + if scope == "project" and not cfg.config_files.project then + return nil, "Current directory is not part of a project. You may want to run `luarocks init`." + end + + local prefix = dir.dir_name(cfg.config_files[scope].file) + local ok, err = persist.save_default_lua_version(prefix, args.value) + if not ok then + return nil, "could not set default Lua version: " .. err + end + print("Lua version will default to " .. args.value .. " in " .. prefix) + end + + if args.key == "lua_dir" and args.value then + local scope = get_scope(args) + local keys = { + ["variables.LUA_DIR"] = cfg.variables.LUA_DIR, + ["variables.LUA_BINDIR"] = cfg.variables.LUA_BINDIR, + ["variables.LUA_INCDIR"] = cfg.variables.LUA_INCDIR, + ["variables.LUA_LIBDIR"] = cfg.variables.LUA_LIBDIR, + ["lua_interpreter"] = cfg.lua_interpreter, + } + if args.lua_version then + local prefix = dir.dir_name(cfg.config_files[scope].file) + persist.save_default_lua_version(prefix, args.lua_version) + end + return write_entries(keys, scope, args.unset) + end + + if args.key then + if args.value or args.unset then + local scope = get_scope(args) + return write_entries({ [args.key] = args.value or args.unset }, scope, args.unset) + else + return print_entry(args.key, cfg, args.json) + end + end + + local cleancfg = cleanup(cfg) + + if args.json then + return print_json(cleancfg) + else + print(persist.save_from_table_to_string(cleancfg)) + return true + end +end + +return config_cmd diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/doc.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/doc.lua new file mode 100644 index 000000000..ae4712302 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/doc.lua @@ -0,0 +1,153 @@ + +--- Module implementing the LuaRocks "doc" command. +-- Shows documentation for an installed rock. +local doc = {} + +local util = require("luarocks.util") +local queries = require("luarocks.queries") +local search = require("luarocks.search") +local path = require("luarocks.path") +local dir = require("luarocks.dir") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local download = require("luarocks.download") + +function doc.add_to_parser(parser) + local cmd = parser:command("doc", "Show documentation for an installed rock.\n\n".. + "Without any flags, tries to load the documentation using a series of heuristics.\n".. + "With flags, return only the desired information.", util.see_also([[ + For more information about a rock, see the 'show' command. +]])) + :summary("Show documentation for an installed rock.") + + cmd:argument("rock", "Name of the rock.") + :action(util.namespaced_name_action) + cmd:argument("version", "Version of the rock.") + :args("?") + + cmd:flag("--home", "Open the home page of project.") + cmd:flag("--list", "List documentation files only.") + cmd:flag("--porcelain", "Produce machine-friendly output.") +end + +local function show_homepage(homepage, name, namespace, version) + if not homepage then + return nil, "No 'homepage' field in rockspec for "..util.format_rock_name(name, namespace, version) + end + util.printout("Opening "..homepage.." ...") + fs.browser(homepage) + return true +end + +local function try_to_open_homepage(name, namespace, version) + local temp_dir, err = fs.make_temp_dir("doc-"..name.."-"..(version or "")) + if not temp_dir then + return nil, "Failed creating temporary directory: "..err + end + util.schedule_function(fs.delete, temp_dir) + local ok, err = fs.change_dir(temp_dir) + if not ok then return nil, err end + local filename, err = download.download("rockspec", name, namespace, version) + if not filename then return nil, err end + local rockspec, err = fetch.load_local_rockspec(filename) + if not rockspec then return nil, err end + fs.pop_dir() + local descript = rockspec.description or {} + return show_homepage(descript.homepage, name, namespace, version) +end + +--- Driver function for "doc" command. +-- @return boolean: True if succeeded, nil on errors. +function doc.command(args) + local query = queries.new(args.rock, args.namespace, args.version) + local iname, iversion, repo = search.pick_installed_rock(query, args.tree) + if not iname then + local rock = util.format_rock_name(args.rock, args.namespace, args.version) + util.printout(rock.." is not installed. Looking for it in the rocks servers...") + return try_to_open_homepage(args.rock, args.namespace, args.version) + end + local name, version = iname, iversion + + local rockspec, err = fetch.load_local_rockspec(path.rockspec_file(name, version, repo)) + if not rockspec then return nil,err end + local descript = rockspec.description or {} + + if args.home then + return show_homepage(descript.homepage, name, args.namespace, version) + end + + local directory = path.install_dir(name, version, repo) + + local docdir + local directories = { "doc", "docs" } + for _, d in ipairs(directories) do + local dirname = dir.path(directory, d) + if fs.is_dir(dirname) then + docdir = dirname + break + end + end + if not docdir then + if descript.homepage and not args.list then + util.printout("Local documentation directory not found -- opening "..descript.homepage.." ...") + fs.browser(descript.homepage) + return true + end + return nil, "Documentation directory not found for "..name.." "..version + end + + docdir = dir.normalize(docdir):gsub("/+", "/") + local files = fs.find(docdir) + local htmlpatt = "%.html?$" + local extensions = { htmlpatt, "%.md$", "%.txt$", "%.textile$", "" } + local basenames = { "index", "readme", "manual" } + + local porcelain = args.porcelain + if #files > 0 then + util.title("Documentation files for "..name.." "..version, porcelain) + if porcelain then + for _, file in ipairs(files) do + util.printout(docdir.."/"..file) + end + else + util.printout(docdir.."/") + for _, file in ipairs(files) do + util.printout("\t"..file) + end + end + end + + if args.list then + return true + end + + for _, extension in ipairs(extensions) do + for _, basename in ipairs(basenames) do + local filename = basename..extension + local found + for _, file in ipairs(files) do + if file:lower():match(filename) and ((not found) or #file < #found) then + found = file + end + end + if found then + local pathname = dir.path(docdir, found) + util.printout() + util.printout("Opening "..pathname.." ...") + util.printout() + local ok = fs.browser(pathname) + if not ok and not pathname:match(htmlpatt) then + local fd = io.open(pathname, "r") + util.printout(fd:read("*a")) + fd:close() + end + return true + end + end + end + + return true +end + + +return doc diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/download.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/download.lua new file mode 100644 index 000000000..eae824395 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/download.lua @@ -0,0 +1,51 @@ + +--- Module implementing the luarocks "download" command. +-- Download a rock from the repository. +local cmd_download = {} + +local util = require("luarocks.util") +local download = require("luarocks.download") + +function cmd_download.add_to_parser(parser) + local cmd = parser:command("download", "Download a specific rock file from a rocks server.", util.see_also()) + + cmd:argument("name", "Name of the rock.") + :args("?") + :action(util.namespaced_name_action) + cmd:argument("version", "Version of the rock.") + :args("?") + + cmd:flag("--all", "Download all files if there are multiple matches.") + cmd:mutex( + cmd:flag("--source", "Download .src.rock if available."), + cmd:flag("--rockspec", "Download .rockspec if available."), + cmd:option("--arch", "Download rock for a specific architecture.")) + cmd:flag("--check-lua-versions", "If the rock can't be found, check repository ".. + "and report if it is available for another Lua version.") +end + +--- Driver function for the "download" command. +-- @return boolean or (nil, string): true if successful or nil followed +-- by an error message. +function cmd_download.command(args) + if not args.name and not args.all then + return nil, "Argument missing. "..util.see_help("download") + end + + args.name = args.name or "" + + local arch + + if args.source then + arch = "src" + elseif args.rockspec then + arch = "rockspec" + elseif args.arch then + arch = args.arch + end + + local dl, err = download.download(arch, args.name, args.namespace, args.version, args.all, args.check_lua_versions) + return dl and true, err +end + +return cmd_download diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/init.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/init.lua new file mode 100644 index 000000000..8bccb23f0 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/init.lua @@ -0,0 +1,176 @@ + +local init = {} + +local cfg = require("luarocks.core.cfg") +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local deps = require("luarocks.deps") +local dir = require("luarocks.dir") +local util = require("luarocks.util") +local persist = require("luarocks.persist") +local write_rockspec = require("luarocks.cmd.write_rockspec") + +function init.add_to_parser(parser) + local cmd = parser:command("init", "Initialize a directory for a Lua project using LuaRocks.", util.see_also()) + + cmd:argument("name", "The project name.") + :args("?") + cmd:argument("version", "An optional project version.") + :args("?") + cmd:flag("--reset", "Delete .luarocks/config-5.x.lua and ./lua and generate new ones.") + + cmd:group("Options for specifying rockspec data", write_rockspec.cmd_options(cmd)) +end + +local function write_gitignore(entries) + local gitignore = "" + local fd = io.open(".gitignore", "r") + if fd then + gitignore = fd:read("*a") + fd:close() + gitignore = "\n" .. gitignore .. "\n" + end + + fd = io.open(".gitignore", gitignore and "a" or "w") + for _, entry in ipairs(entries) do + entry = "/" .. entry + if not gitignore:find("\n"..entry.."\n", 1, true) then + fd:write(entry.."\n") + end + end + fd:close() +end + +--- Driver function for "init" command. +-- @return boolean: True if succeeded, nil on errors. +function init.command(args) + + local pwd = fs.current_dir() + + if not args.name then + args.name = dir.base_name(pwd) + if args.name == "/" then + return nil, "When running from the root directory, please specify the argument" + end + end + + util.title("Initializing project '" .. args.name .. "' for Lua " .. cfg.lua_version .. " ...") + + util.printout("Checking your Lua installation ...") + if not cfg.lua_found then + return nil, "Lua installation is not found." + end + local ok, err = deps.check_lua_incdir(cfg.variables) + if not ok then + return nil, err + end + + local has_rockspec = false + for file in fs.dir() do + if file:match("%.rockspec$") then + has_rockspec = true + break + end + end + + if not has_rockspec then + args.version = args.version or "dev" + args.location = pwd + local ok, err = write_rockspec.command(args) + if not ok then + util.printerr(err) + end + end + + local ext = cfg.wrapper_suffix + local luarocks_wrapper = "luarocks" .. ext + local lua_wrapper = "lua" .. ext + + util.printout("Adding entries to .gitignore ...") + write_gitignore({ luarocks_wrapper, lua_wrapper, "lua_modules", ".luarocks" }) + + util.printout("Preparing ./.luarocks/ ...") + fs.make_dir(".luarocks") + local config_file = ".luarocks/config-" .. cfg.lua_version .. ".lua" + + if args.reset then + fs.delete(lua_wrapper) + fs.delete(config_file) + end + + local config_tbl, err = persist.load_config_file_if_basic(config_file, cfg) + if config_tbl then + local globals = { + "lua_interpreter", + } + for _, v in ipairs(globals) do + if cfg[v] then + config_tbl[v] = cfg[v] + end + end + + local varnames = { + "LUA_DIR", + "LUA_INCDIR", + "LUA_LIBDIR", + "LUA_BINDIR", + "LUA_INTERPRETER", + } + for _, varname in ipairs(varnames) do + if cfg.variables[varname] then + config_tbl.variables = config_tbl.variables or {} + config_tbl.variables[varname] = cfg.variables[varname] + end + end + local ok, err = persist.save_from_table(config_file, config_tbl) + if ok then + util.printout("Wrote " .. config_file) + else + util.printout("Failed writing " .. config_file .. ": " .. err) + end + else + util.printout("Will not attempt to overwrite " .. config_file) + end + + ok, err = persist.save_default_lua_version(".luarocks", cfg.lua_version) + if not ok then + util.printout("Failed setting default Lua version: " .. err) + end + + util.printout("Preparing ./lua_modules/ ...") + + fs.make_dir("lua_modules/lib/luarocks/rocks-" .. cfg.lua_version) + local tree = dir.path(pwd, "lua_modules") + + luarocks_wrapper = dir.path(".", luarocks_wrapper) + if not fs.exists(luarocks_wrapper) then + util.printout("Preparing " .. luarocks_wrapper .. " ...") + fs.wrap_script(arg[0], luarocks_wrapper, "none", nil, nil, "--project-tree", tree) + else + util.printout(luarocks_wrapper .. " already exists. Not overwriting it!") + end + + lua_wrapper = dir.path(".", lua_wrapper) + local write_lua_wrapper = true + if fs.exists(lua_wrapper) then + if not util.lua_is_wrapper(lua_wrapper) then + util.printout(lua_wrapper .. " already exists and does not look like a wrapper script. Not overwriting.") + write_lua_wrapper = false + end + end + + if write_lua_wrapper then + local interp = dir.path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter) + if util.check_lua_version(interp, cfg.lua_version) then + util.printout("Preparing " .. lua_wrapper .. " for version " .. cfg.lua_version .. "...") + path.use_tree(tree) + fs.wrap_script(nil, lua_wrapper, "all") + else + util.warning("No Lua interpreter detected for version " .. cfg.lua_version .. ". Not creating " .. lua_wrapper) + end + end + + return true +end + +return init diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/install.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/install.lua new file mode 100644 index 000000000..c2e947b20 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/install.lua @@ -0,0 +1,258 @@ +--- Module implementing the LuaRocks "install" command. +-- Installs binary rocks. +local install = {} + +local path = require("luarocks.path") +local repos = require("luarocks.repos") +local fetch = require("luarocks.fetch") +local util = require("luarocks.util") +local fs = require("luarocks.fs") +local deps = require("luarocks.deps") +local writer = require("luarocks.manif.writer") +local remove = require("luarocks.remove") +local search = require("luarocks.search") +local queries = require("luarocks.queries") +local cfg = require("luarocks.core.cfg") +local cmd = require("luarocks.cmd") + +function install.add_to_parser(parser) + local cmd = parser:command("install", "Install a rock.", util.see_also()) -- luacheck: ignore 431 + + cmd:argument("rock", "The name of a rock to be fetched from a repository ".. + "or a filename of a locally available rock.") + :action(util.namespaced_name_action) + cmd:argument("version", "Version of the rock.") + :args("?") + + cmd:flag("--keep", "Do not remove previously installed versions of the ".. + "rock after building a new one. This behavior can be made permanent by ".. + "setting keep_other_versions=true in the configuration file.") + cmd:flag("--force", "If --keep is not specified, force removal of ".. + "previously installed versions if it would break dependencies.") + cmd:flag("--force-fast", "Like --force, but performs a forced removal ".. + "without reporting dependency issues.") + cmd:flag("--only-deps --deps-only", "Install only the dependencies of the rock.") + cmd:flag("--no-doc", "Install the rock without its documentation.") + cmd:flag("--verify", "Verify signature of the rockspec or src.rock being ".. + "built. If the rockspec or src.rock is being downloaded, LuaRocks will ".. + "attempt to download the signature as well. Otherwise, the signature ".. + "file should be already available locally in the same directory.\n".. + "You need the signer’s public key in your local keyring for this ".. + "option to work properly.") + cmd:flag("--check-lua-versions", "If the rock can't be found, check repository ".. + "and report if it is available for another Lua version.") + util.deps_mode_option(cmd) + cmd:flag("--no-manifest", "Skip creating/updating the manifest") + cmd:flag("--pin", "If the installed rock is a Lua module, create a ".. + "luarocks.lock file listing the exact versions of each dependency found for ".. + "this rock (recursively), and store it in the rock's directory. ".. + "Ignores any existing luarocks.lock file in the rock's sources.") + -- luarocks build options + parser:flag("--pack-binary-rock"):hidden(true) + parser:option("--branch"):hidden(true) + parser:flag("--sign"):hidden(true) +end + +install.opts = util.opts_table("install.opts", { + namespace = "string?", + keep = "boolean", + force = "boolean", + force_fast = "boolean", + no_doc = "boolean", + deps_mode = "string", + verify = "boolean", +}) + +--- Install a binary rock. +-- @param rock_file string: local or remote filename of a rock. +-- @param opts table: installation options +-- @return (string, string) or (nil, string, [string]): Name and version of +-- installed rock if succeeded or nil and an error message followed by an error code. +function install.install_binary_rock(rock_file, opts) + assert(type(rock_file) == "string") + assert(opts:type() == "install.opts") + + local namespace = opts.namespace + local deps_mode = opts.deps_mode + + local name, version, arch = path.parse_name(rock_file) + if not name then + return nil, "Filename "..rock_file.." does not match format 'name-version-revision.arch.rock'." + end + + if arch ~= "all" and arch ~= cfg.arch then + return nil, "Incompatible architecture "..arch, "arch" + end + if repos.is_installed(name, version) then + repos.delete_version(name, version, opts.deps_mode) + end + + local install_dir = path.install_dir(name, version) + + local rollback = util.schedule_function(function() + fs.delete(install_dir) + fs.remove_dir_if_empty(path.versions_dir(name)) + end) + + local ok, err, errcode = fetch.fetch_and_unpack_rock(rock_file, install_dir, opts.verify) + if not ok then return nil, err, errcode end + + local rockspec, err = fetch.load_rockspec(path.rockspec_file(name, version)) + if err then + return nil, "Failed loading rockspec for installed package: "..err, errcode + end + + if opts.deps_mode ~= "none" then + ok, err, errcode = deps.check_external_deps(rockspec, "install") + if err then return nil, err, errcode end + end + + -- For compatibility with .rock files built with LuaRocks 1 + if not fs.exists(path.rock_manifest_file(name, version)) then + ok, err = writer.make_rock_manifest(name, version) + if err then return nil, err end + end + + if namespace then + ok, err = writer.make_namespace_file(name, version, namespace) + if err then return nil, err end + end + + if deps_mode ~= "none" then + ok, err, errcode = deps.fulfill_dependencies(rockspec, "dependencies", deps_mode, opts.verify, install_dir) + if err then return nil, err, errcode end + end + + ok, err = repos.deploy_files(name, version, repos.should_wrap_bin_scripts(rockspec), deps_mode) + if err then return nil, err end + + util.remove_scheduled_function(rollback) + rollback = util.schedule_function(function() + repos.delete_version(name, version, deps_mode) + end) + + ok, err = repos.run_hook(rockspec, "post_install") + if err then return nil, err end + + util.announce_install(rockspec) + util.remove_scheduled_function(rollback) + return name, version +end + +--- Installs the dependencies of a binary rock. +-- @param rock_file string: local or remote filename of a rock. +-- @param opts table: installation options +-- @return (string, string) or (nil, string, [string]): Name and version of +-- the rock whose dependencies were installed if succeeded or nil and an error message +-- followed by an error code. +function install.install_binary_rock_deps(rock_file, opts) + assert(type(rock_file) == "string") + assert(opts:type() == "install.opts") + + local name, version, arch = path.parse_name(rock_file) + if not name then + return nil, "Filename "..rock_file.." does not match format 'name-version-revision.arch.rock'." + end + + if arch ~= "all" and arch ~= cfg.arch then + return nil, "Incompatible architecture "..arch, "arch" + end + + local install_dir = path.install_dir(name, version) + + local ok, err, errcode = fetch.fetch_and_unpack_rock(rock_file, install_dir, opts.verify) + if not ok then return nil, err, errcode end + + local rockspec, err = fetch.load_rockspec(path.rockspec_file(name, version)) + if err then + return nil, "Failed loading rockspec for installed package: "..err, errcode + end + + ok, err, errcode = deps.fulfill_dependencies(rockspec, "dependencies", opts.deps_mode, opts.verify, install_dir) + if err then return nil, err, errcode end + + util.printout() + util.printout("Successfully installed dependencies for " ..name.." "..version) + + return name, version +end + +local function install_rock_file_deps(filename, opts) + assert(opts:type() == "install.opts") + + local name, version = install.install_binary_rock_deps(filename, opts) + if not name then return nil, version end + + writer.check_dependencies(nil, opts.deps_mode) + return name, version +end + +local function install_rock_file(filename, opts) + assert(type(filename) == "string") + assert(opts:type() == "install.opts") + + local name, version = install.install_binary_rock(filename, opts) + if not name then return nil, version end + + if opts.no_doc then + util.remove_doc_dir(name, version) + end + + if (not opts.keep) and not cfg.keep_other_versions then + local ok, err, warn = remove.remove_other_versions(name, version, opts.force, opts.force_fast) + if not ok then + return nil, err + elseif warn then + util.printerr(err) + end + end + + writer.check_dependencies(nil, opts.deps_mode) + return name, version +end + +--- Driver function for the "install" command. +-- If an URL or pathname to a binary rock is given, fetches and installs it. +-- If a rockspec or a source rock is given, forwards the request to the "build" +-- command. +-- If a package name is given, forwards the request to "search" and, +-- if returned a result, installs the matching rock. +-- @return boolean or (nil, string, exitcode): True if installation was +-- successful, nil and an error message otherwise. exitcode is optionally returned. +function install.command(args) + local ok, err = fs.check_command_permissions(args) + if not ok then return nil, err, cmd.errorcodes.PERMISSIONDENIED end + + if args.rock:match("%.rockspec$") or args.rock:match("%.src%.rock$") then + local build = require("luarocks.cmd.build") + return build.command(args) + elseif args.rock:match("%.rock$") then + local deps_mode = deps.get_deps_mode(args) + local opts = install.opts({ + namespace = args.namespace, + keep = not not args.keep, + force = not not args.force, + force_fast = not not args.force_fast, + no_doc = not not args.no_doc, + deps_mode = deps_mode, + verify = not not args.verify, + }) + if args.only_deps then + return install_rock_file_deps(args.rock, opts) + else + return install_rock_file(args.rock, opts) + end + else + local url, err = search.find_rock_checking_lua_versions( + queries.new(args.rock, args.namespace, args.version), + args.check_lua_versions) + if not url then + return nil, err + end + util.printout("Installing "..url) + args.rock = url + return install.command(args) + end +end + +return install diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/lint.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/lint.lua new file mode 100644 index 000000000..47a3da906 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/lint.lua @@ -0,0 +1,50 @@ + +--- Module implementing the LuaRocks "lint" command. +-- Utility function that checks syntax of the rockspec. +local lint = {} + +local util = require("luarocks.util") +local download = require("luarocks.download") +local fetch = require("luarocks.fetch") + +function lint.add_to_parser(parser) + local cmd = parser:command("lint", "Check syntax of a rockspec.\n\n".. + "Returns success if the text of the rockspec is syntactically correct, else failure.", + util.see_also()) + :summary("Check syntax of a rockspec.") + + cmd:argument("rockspec", "The rockspec to check.") +end + +function lint.command(args) + + local filename = args.rockspec + if not filename:match(".rockspec$") then + local err + filename, err = download.download("rockspec", filename:lower()) + if not filename then + return nil, err + end + end + + local rs, err = fetch.load_local_rockspec(filename) + if not rs then + return nil, "Failed loading rockspec: "..err + end + + local ok = true + + -- This should have been done in the type checker, + -- but it would break compatibility of other commands. + -- Making 'lint' alone be stricter shouldn't be a problem, + -- because extra-strict checks is what lint-type commands + -- are all about. + if not rs.description.license then + util.printerr("Rockspec has no license field.") + ok = false + end + + return ok, ok or filename.." failed consistency checks." +end + +return lint diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/list.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/list.lua new file mode 100644 index 000000000..7b2682f64 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/list.lua @@ -0,0 +1,96 @@ + +--- Module implementing the LuaRocks "list" command. +-- Lists currently installed rocks. +local list = {} + +local search = require("luarocks.search") +local queries = require("luarocks.queries") +local vers = require("luarocks.core.vers") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local path = require("luarocks.path") + +function list.add_to_parser(parser) + local cmd = parser:command("list", "List currently installed rocks.", util.see_also()) + + cmd:argument("filter", "A substring of a rock name to filter by.") + :args("?") + cmd:argument("version", "Rock version to filter by.") + :args("?") + + cmd:flag("--outdated", "List only rocks for which there is a higher ".. + "version available in the rocks server.") + cmd:flag("--porcelain", "Produce machine-friendly output.") +end + +local function check_outdated(trees, query) + local results_installed = {} + for _, tree in ipairs(trees) do + search.local_manifest_search(results_installed, path.rocks_dir(tree), query) + end + local outdated = {} + for name, versions in util.sortedpairs(results_installed) do + versions = util.keys(versions) + table.sort(versions, vers.compare_versions) + local latest_installed = versions[1] + + local query_available = queries.new(name:lower()) + local results_available, err = search.search_repos(query_available) + + if results_available[name] then + local available_versions = util.keys(results_available[name]) + table.sort(available_versions, vers.compare_versions) + local latest_available = available_versions[1] + local latest_available_repo = results_available[name][latest_available][1].repo + + if vers.compare_versions(latest_available, latest_installed) then + table.insert(outdated, { name = name, installed = latest_installed, available = latest_available, repo = latest_available_repo }) + end + end + end + return outdated +end + +local function list_outdated(trees, query, porcelain) + util.title("Outdated rocks:", porcelain) + local outdated = check_outdated(trees, query) + for _, item in ipairs(outdated) do + if porcelain then + util.printout(item.name, item.installed, item.available, item.repo) + else + util.printout(item.name) + util.printout(" "..item.installed.." < "..item.available.." at "..item.repo) + util.printout() + end + end + return true +end + +--- Driver function for "list" command. +-- @return boolean: True if succeeded, nil on errors. +function list.command(args) + local query = queries.new(args.filter and args.filter:lower() or "", args.namespace, args.version, true) + local trees = cfg.rocks_trees + local title = "Rocks installed for Lua "..cfg.lua_version + if args.tree then + trees = { args.tree } + title = title .. " in " .. args.tree + end + + if args.outdated then + return list_outdated(trees, query, args.porcelain) + end + + local results = {} + for _, tree in ipairs(trees) do + local ok, err, errcode = search.local_manifest_search(results, path.rocks_dir(tree), query) + if not ok and errcode ~= "open" then + util.warning(err) + end + end + util.title(title, args.porcelain) + search.print_result_tree(results, args.porcelain) + return true +end + +return list diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/make.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/make.lua new file mode 100644 index 000000000..5c67c6736 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/make.lua @@ -0,0 +1,166 @@ + +--- Module implementing the LuaRocks "make" command. +-- Builds sources in the current directory, but unlike "build", +-- it does not fetch sources, etc., assuming everything is +-- available in the current directory. +local make = {} + +local build = require("luarocks.build") +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") +local fetch = require("luarocks.fetch") +local pack = require("luarocks.pack") +local remove = require("luarocks.remove") +local deps = require("luarocks.deps") +local writer = require("luarocks.manif.writer") +local cmd = require("luarocks.cmd") + +function make.cmd_options(parser) + parser:flag("--no-install", "Do not install the rock.") + parser:flag("--no-doc", "Install the rock without its documentation.") + parser:flag("--pack-binary-rock", "Do not install rock. Instead, produce a ".. + ".rock file with the contents of compilation in the current directory.") + parser:flag("--keep", "Do not remove previously installed versions of the ".. + "rock after building a new one. This behavior can be made permanent by ".. + "setting keep_other_versions=true in the configuration file.") + parser:flag("--force", "If --keep is not specified, force removal of ".. + "previously installed versions if it would break dependencies.") + parser:flag("--force-fast", "Like --force, but performs a forced removal ".. + "without reporting dependency issues.") + parser:flag("--verify", "Verify signature of the rockspec or src.rock being ".. + "built. If the rockspec or src.rock is being downloaded, LuaRocks will ".. + "attempt to download the signature as well. Otherwise, the signature ".. + "file should be already available locally in the same directory.\n".. + "You need the signer’s public key in your local keyring for this ".. + "option to work properly.") + parser:flag("--sign", "To be used with --pack-binary-rock. Also produce a ".. + "signature file for the generated .rock file.") + parser:flag("--check-lua-versions", "If the rock can't be found, check repository ".. + "and report if it is available for another Lua version.") + parser:flag("--pin", "Pin the exact dependencies used for the rockspec".. + "being built into a luarocks.lock file in the current directory.") + parser:flag("--no-manifest", "Skip creating/updating the manifest") + parser:flag("--only-deps --deps-only", "Install only the dependencies of the rock.") + parser:option("--chdir", "Specify a source directory of the rock."):argname("") + util.deps_mode_option(parser) +end + +function make.add_to_parser(parser) + -- luacheck: push ignore 431 + local cmd = parser:command("make", [[ +Builds sources in the current directory, but unlike "build", it does not fetch +sources, etc., assuming everything is available in the current directory. If no +argument is given, it looks for a rockspec in the current directory and in +"rockspec/" and "rockspecs/" subdirectories, picking the rockspec with newest +version or without version name. If rockspecs for different rocks are found or +there are several rockspecs without version, you must specify which to use, +through the command-line. + +This command is useful as a tool for debugging rockspecs. +To install rocks, you'll normally want to use the "install" and "build" +commands. See the help on those for details. + +If the current directory contains a luarocks.lock file, it is used as the +authoritative source for exact version of dependencies. The --pin flag +overrides and recreates this file scanning dependency based on ranges. +]], util.see_also()) + :summary("Compile package in current directory using a rockspec.") + -- luacheck: pop + + cmd:argument("rockspec", "Rockspec for the rock to build.") + :args("?") + + make.cmd_options(cmd) +end + +--- Driver function for "make" command. +-- @return boolean or (nil, string, exitcode): True if build was successful; nil and an +-- error message otherwise. exitcode is optionally returned. +function make.command(args) + local rockspec_filename = args.rockspec + + if args.chdir then + local ok, err = fs.change_dir(args.chdir) + if not ok then + return nil, err + end + end + if not rockspec_filename then + local err + rockspec_filename, err = util.get_default_rockspec() + if not rockspec_filename then + return nil, err + end + end + if not rockspec_filename:match("rockspec$") then + return nil, "Invalid argument: 'make' takes a rockspec as a parameter. "..util.see_help("make") + end + + local rockspec, err, errcode = fetch.load_rockspec(rockspec_filename) + if not rockspec then + return nil, err + end + + local name, namespace = util.split_namespace(rockspec.name) + namespace = namespace or args.namespace + + local opts = build.opts({ + need_to_fetch = false, + minimal_mode = true, + deps_mode = deps.get_deps_mode(args), + build_only_deps = not not (args.only_deps and not args.pack_binary_rock), + namespace = namespace, + branch = args.branch, + verify = not not args.verify, + check_lua_versions = not not args.check_lua_versions, + pin = not not args.pin, + no_install = not not args.no_install + }) + + if args.sign and not args.pack_binary_rock then + return nil, "In the make command, --sign is meant to be used only with --pack-binary-rock" + end + + if args.no_install then + return build.build_rockspec(rockspec, opts) + elseif args.pack_binary_rock then + return pack.pack_binary_rock(name, namespace, rockspec.version, args.sign, function() + local name, version = build.build_rockspec(rockspec, opts) -- luacheck: ignore 431 + if name and args.no_doc then + util.remove_doc_dir(name, version) + end + return name, version + end) + else + local ok, err = fs.check_command_permissions(args) + if not ok then return nil, err, cmd.errorcodes.PERMISSIONDENIED end + ok, err = build.build_rockspec(rockspec, opts) + if not ok then return nil, err end + local name, version = ok, err -- luacheck: ignore 421 + + if opts.build_only_deps then + util.printout("Stopping after installing dependencies for " ..name.." "..version) + util.printout() + return name, version + end + + if args.no_doc then + util.remove_doc_dir(name, version) + end + + if (not args.keep) and not cfg.keep_other_versions then + local ok, err, warn = remove.remove_other_versions(name, version, args.force, args.force_fast) + if not ok then + return nil, err + elseif warn then + util.printerr(warn) + end + end + + writer.check_dependencies(nil, deps.get_deps_mode(args)) + return name, version + end +end + +return make diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/new_version.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/new_version.lua new file mode 100644 index 000000000..49479910c --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/new_version.lua @@ -0,0 +1,228 @@ + +--- Module implementing the LuaRocks "new_version" command. +-- Utility function that writes a new rockspec, updating data from a previous one. +local new_version = {} + +local util = require("luarocks.util") +local download = require("luarocks.download") +local fetch = require("luarocks.fetch") +local persist = require("luarocks.persist") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local type_rockspec = require("luarocks.type.rockspec") + +function new_version.add_to_parser(parser) + local cmd = parser:command("new_version", [[ +This is a utility function that writes a new rockspec, updating data from a +previous one. + +If a package name is given, it downloads the latest rockspec from the default +server. If a rockspec is given, it uses it instead. If no argument is given, it +looks for a rockspec same way 'luarocks make' does. + +If the version number is not given and tag is passed using --tag, it is used as +the version, with 'v' removed from beginning. Otherwise, it only increments the +revision number of the given (or downloaded) rockspec. + +If a URL is given, it replaces the one from the old rockspec with the given URL. +If a URL is not given and a new version is given, it tries to guess the new URL +by replacing occurrences of the version number in the URL or tag; if the guessed +URL is invalid, the old URL is restored. It also tries to download the new URL +to determine the new MD5 checksum. + +If a tag is given, it replaces the one from the old rockspec. If there is an old +tag but no new one passed, it is guessed in the same way URL is. + +If a directory is not given, it defaults to the current directory. + +WARNING: it writes the new rockspec to the given directory, overwriting the file +if it already exists.]], util.see_also()) + :summary("Auto-write a rockspec for a new version of a rock.") + + cmd:argument("rock", "Package name or rockspec.") + :args("?") + cmd:argument("new_version", "New version of the rock.") + :args("?") + cmd:argument("new_url", "New URL of the rock.") + :args("?") + + cmd:option("--dir", "Output directory for the new rockspec.") + cmd:option("--tag", "New SCM tag.") +end + + +local function try_replace(tbl, field, old, new) + if not tbl[field] then + return false + end + local old_field = tbl[field] + local new_field = tbl[field]:gsub(old, new) + if new_field ~= old_field then + util.printout("Guessing new '"..field.."' field as "..new_field) + tbl[field] = new_field + return true + end + return false +end + +-- Try to download source file using URL from a rockspec. +-- If it specified MD5, update it. +-- @return (true, false) if MD5 was not specified or it stayed same, +-- (true, true) if MD5 changed, (nil, string) on error. +local function check_url_and_update_md5(out_rs, invalid_is_error) + local file, temp_dir = fetch.fetch_url_at_temp_dir(out_rs.source.url, "luarocks-new-version-"..out_rs.package) + if not file then + if invalid_is_error then + return nil, "invalid URL - "..temp_dir + end + util.warning("invalid URL - "..temp_dir) + return true, false + end + + local inferred_dir, found_dir = fetch.find_base_dir(file, temp_dir, out_rs.source.url, out_rs.source.dir) + if not inferred_dir then + return nil, found_dir + end + + if found_dir and found_dir ~= inferred_dir then + out_rs.source.dir = found_dir + end + + if file then + if out_rs.source.md5 then + util.printout("File successfully downloaded. Updating MD5 checksum...") + local new_md5, err = fs.get_md5(file) + if not new_md5 then + return nil, err + end + local old_md5 = out_rs.source.md5 + out_rs.source.md5 = new_md5 + return true, new_md5 ~= old_md5 + else + util.printout("File successfully downloaded.") + return true, false + end + end +end + +local function update_source_section(out_rs, url, tag, old_ver, new_ver) + if tag then + out_rs.source.tag = tag + end + if url then + out_rs.source.url = url + return check_url_and_update_md5(out_rs) + end + if new_ver == old_ver then + return true + end + if out_rs.source.dir then + try_replace(out_rs.source, "dir", old_ver, new_ver) + end + if out_rs.source.file then + try_replace(out_rs.source, "file", old_ver, new_ver) + end + + local old_url = out_rs.source.url + if try_replace(out_rs.source, "url", old_ver, new_ver) then + local ok, md5_changed = check_url_and_update_md5(out_rs, true) + if ok then + return ok, md5_changed + end + out_rs.source.url = old_url + end + if tag or try_replace(out_rs.source, "tag", old_ver, new_ver) then + return true + end + -- Couldn't replace anything significant, use the old URL. + local ok, md5_changed = check_url_and_update_md5(out_rs) + if not ok then + return nil, md5_changed + end + if md5_changed then + util.warning("URL is the same, but MD5 has changed. Old rockspec is broken.") + end + return true +end + +function new_version.command(args) + if not args.rock then + local err + args.rock, err = util.get_default_rockspec() + if not args.rock then + return nil, err + end + end + + local filename, err + if args.rock:match("rockspec$") then + filename, err = fetch.fetch_url(args.rock) + if not filename then + return nil, err + end + else + filename, err = download.download("rockspec", args.rock:lower()) + if not filename then + return nil, err + end + end + + local valid_rs, err = fetch.load_rockspec(filename) + if not valid_rs then + return nil, err + end + + local old_ver, old_rev = valid_rs.version:match("(.*)%-(%d+)$") + local new_ver, new_rev + + if args.tag and not args.new_version then + args.new_version = args.tag:gsub("^v", "") + end + + local out_dir + if args.dir then + out_dir = dir.normalize(args.dir) + end + + if args.new_version then + new_ver, new_rev = args.new_version:match("(.*)%-(%d+)$") + new_rev = tonumber(new_rev) + if not new_rev then + new_ver = args.new_version + new_rev = 1 + end + else + new_ver = old_ver + new_rev = tonumber(old_rev) + 1 + end + local new_rockver = new_ver:gsub("-", "") + + local out_rs, err = persist.load_into_table(filename) + local out_name = out_rs.package:lower() + out_rs.version = new_rockver.."-"..new_rev + + local ok, err = update_source_section(out_rs, args.new_url, args.tag, old_ver, new_ver) + if not ok then return nil, err end + + if out_rs.build and out_rs.build.type == "module" then + out_rs.build.type = "builtin" + end + + local out_filename = out_name.."-"..new_rockver.."-"..new_rev..".rockspec" + if out_dir then + out_filename = dir.path(out_dir, out_filename) + fs.make_dir(out_dir) + end + persist.save_from_table(out_filename, out_rs, type_rockspec.order) + + util.printout("Wrote "..out_filename) + + local valid_out_rs, err = fetch.load_local_rockspec(out_filename) + if not valid_out_rs then + return nil, "Failed loading generated rockspec: "..err + end + + return true +end + +return new_version diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/pack.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/pack.lua new file mode 100644 index 000000000..29a43e7b0 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/pack.lua @@ -0,0 +1,36 @@ + +--- Module implementing the LuaRocks "pack" command. +-- Creates a rock, packing sources or binaries. +local cmd_pack = {} + +local util = require("luarocks.util") +local pack = require("luarocks.pack") +local queries = require("luarocks.queries") + +function cmd_pack.add_to_parser(parser) + local cmd = parser:command("pack", "Create a rock, packing sources or binaries.", util.see_also()) + + cmd:argument("rock", "A rockspec file, for creating a source rock, or the ".. + "name of an installed package, for creating a binary rock.") + :action(util.namespaced_name_action) + cmd:argument("version", "A version may be given if the first argument is a rock name.") + :args("?") + + cmd:flag("--sign", "Produce a signature file as well.") +end + +--- Driver function for the "pack" command. +-- @return boolean or (nil, string): true if successful or nil followed +-- by an error message. +function cmd_pack.command(args) + local file, err + if args.rock:match(".*%.rockspec") then + file, err = pack.pack_source_rock(args.rock) + else + local query = queries.new(args.rock, args.namespace, args.version) + file, err = pack.pack_installed_rock(query, args.tree) + end + return pack.report_and_sign_local_file(file, err, args.sign) +end + +return cmd_pack diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/path.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/path.lua new file mode 100644 index 000000000..9b6fee71a --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/path.lua @@ -0,0 +1,70 @@ + +--- @module luarocks.path_cmd +-- Driver for the `luarocks path` command. +local path_cmd = {} + +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") +local fs = require("luarocks.fs") + +function path_cmd.add_to_parser(parser) + local cmd = parser:command("path", [[ +Returns the package path currently configured for this installation +of LuaRocks, formatted as shell commands to update LUA_PATH and LUA_CPATH. + +On Unix systems, you may run: + eval `luarocks path` +And on Windows: + luarocks path > "%temp%\_lrp.bat" && call "%temp%\_lrp.bat" && del "%temp%\_lrp.bat"]], + util.see_also()) + :summary("Return the currently configured package path.") + + cmd:flag("--no-bin", "Do not export the PATH variable.") + cmd:flag("--append", "Appends the paths to the existing paths. Default is ".. + "to prefix the LR paths to the existing paths.") + cmd:flag("--lr-path", "Exports the Lua path (not formatted as shell command).") + cmd:flag("--lr-cpath", "Exports the Lua cpath (not formatted as shell command).") + cmd:flag("--lr-bin", "Exports the system path (not formatted as shell command).") + cmd:flag("--bin"):hidden(true) +end + +--- Driver function for "path" command. +-- @return boolean This function always succeeds. +function path_cmd.command(args) + local lr_path, lr_cpath, lr_bin = cfg.package_paths(args.tree) + local path_sep = cfg.export_path_separator + + if args.lr_path then + util.printout(util.cleanup_path(lr_path, ';', cfg.lua_version, true)) + return true + elseif args.lr_cpath then + util.printout(util.cleanup_path(lr_cpath, ';', cfg.lua_version, true)) + return true + elseif args.lr_bin then + util.printout(util.cleanup_path(lr_bin, path_sep, nil, true)) + return true + end + + local clean_path = util.cleanup_path(os.getenv("PATH") or "", path_sep, nil, true) + + if args.append then + lr_path = package.path .. ";" .. lr_path + lr_cpath = package.cpath .. ";" .. lr_cpath + lr_bin = clean_path .. path_sep .. lr_bin + else + lr_path = lr_path.. ";" .. package.path + lr_cpath = lr_cpath .. ";" .. package.cpath + lr_bin = lr_bin .. path_sep .. clean_path + end + + local lpath_var, lcpath_var = util.lua_path_variables() + + util.printout(fs.export_cmd(lpath_var, util.cleanup_path(lr_path, ';', cfg.lua_version, args.append))) + util.printout(fs.export_cmd(lcpath_var, util.cleanup_path(lr_cpath, ';', cfg.lua_version, args.append))) + if not args.no_bin then + util.printout(fs.export_cmd("PATH", util.cleanup_path(lr_bin, path_sep, nil, args.append))) + end + return true +end + +return path_cmd diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/purge.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/purge.lua new file mode 100644 index 000000000..c300e286e --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/purge.lua @@ -0,0 +1,84 @@ + +--- Module implementing the LuaRocks "purge" command. +-- Remove all rocks from a given tree. +local purge = {} + +local util = require("luarocks.util") +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local search = require("luarocks.search") +local vers = require("luarocks.core.vers") +local repos = require("luarocks.repos") +local writer = require("luarocks.manif.writer") +local cfg = require("luarocks.core.cfg") +local remove = require("luarocks.remove") +local queries = require("luarocks.queries") +local cmd = require("luarocks.cmd") + +function purge.add_to_parser(parser) + -- luacheck: push ignore 431 + local cmd = parser:command("purge", [[ +This command removes rocks en masse from a given tree. +By default, it removes all rocks from a tree. + +The --tree option is mandatory: luarocks purge does not assume a default tree.]], + util.see_also()) + :summary("Remove all installed rocks from a tree.") + -- luacheck: pop + + cmd:flag("--old-versions", "Keep the highest-numbered version of each ".. + "rock and remove the other ones. By default it only removes old ".. + "versions if they are not needed as dependencies. This can be ".. + "overridden with the flag --force.") + cmd:flag("--force", "If --old-versions is specified, force removal of ".. + "previously installed versions if it would break dependencies.") + cmd:flag("--force-fast", "Like --force, but performs a forced removal ".. + "without reporting dependency issues.") +end + +function purge.command(args) + local tree = args.tree + + if type(tree) ~= "string" then + return nil, "The --tree argument is mandatory. "..util.see_help("purge") + end + + local results = {} + if not fs.is_dir(tree) then + return nil, "Directory not found: "..tree + end + + local ok, err = fs.check_command_permissions(args) + if not ok then return nil, err, cmd.errorcodes.PERMISSIONDENIED end + + search.local_manifest_search(results, path.rocks_dir(tree), queries.all()) + + local sort = function(a,b) return vers.compare_versions(b,a) end + if args.old_versions then + sort = vers.compare_versions + end + + for package, versions in util.sortedpairs(results) do + for version, _ in util.sortedpairs(versions, sort) do + if args.old_versions then + util.printout("Keeping "..package.." "..version.."...") + local ok, err, warn = remove.remove_other_versions(package, version, args.force, args.force_fast) + if not ok then + util.printerr(err) + elseif warn then + util.printerr(err) + end + break + else + util.printout("Removing "..package.." "..version.."...") + local ok, err = repos.delete_version(package, version, "none", true) + if not ok then + util.printerr(err) + end + end + end + end + return writer.make_manifest(cfg.rocks_dir, "one") +end + +return purge diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/remove.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/remove.lua new file mode 100644 index 000000000..6bb69ad63 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/remove.lua @@ -0,0 +1,75 @@ + +--- Module implementing the LuaRocks "remove" command. +-- Uninstalls rocks. +local cmd_remove = {} + +local remove = require("luarocks.remove") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") +local fs = require("luarocks.fs") +local search = require("luarocks.search") +local path = require("luarocks.path") +local deps = require("luarocks.deps") +local writer = require("luarocks.manif.writer") +local queries = require("luarocks.queries") +local cmd = require("luarocks.cmd") + +function cmd_remove.add_to_parser(parser) + -- luacheck: push ignore 431 + local cmd = parser:command("remove", [[ +Uninstall a rock. + +If a version is not given, try to remove all versions at once. +Will only perform the removal if it does not break dependencies. +To override this check and force the removal, use --force or --force-fast.]], + util.see_also()) + :summary("Uninstall a rock.") + -- luacheck: pop + + cmd:argument("rock", "Name of the rock to be uninstalled.") + :action(util.namespaced_name_action) + cmd:argument("version", "Version of the rock to uninstall.") + :args("?") + + cmd:flag("--force", "Force removal if it would break dependencies.") + cmd:flag("--force-fast", "Perform a forced removal without reporting dependency issues.") + util.deps_mode_option(cmd) +end + +--- Driver function for the "remove" command. +-- @return boolean or (nil, string, exitcode): True if removal was +-- successful, nil and an error message otherwise. exitcode is optionally returned. +function cmd_remove.command(args) + local name = args.rock + local deps_mode = deps.get_deps_mode(args) + + local ok, err = fs.check_command_permissions(args) + if not ok then return nil, err, cmd.errorcodes.PERMISSIONDENIED end + + local rock_type = name:match("%.(rock)$") or name:match("%.(rockspec)$") + local version = args.version + local filename = name + if rock_type then + name, version = path.parse_name(filename) + if not name then return nil, "Invalid "..rock_type.." filename: "..filename end + end + + name = name:lower() + + local results = {} + search.local_manifest_search(results, cfg.rocks_dir, queries.new(name, args.namespace, version)) + if not results[name] then + local rock = util.format_rock_name(name, args.namespace, version) + return nil, "Could not find rock '"..rock.."' in "..path.rocks_tree_to_string(cfg.root_dir) + end + + ok, err = remove.remove_search_results(results, name, deps_mode, args.force, args.force_fast) + if not ok then + return nil, err + end + + writer.check_dependencies(nil, deps.get_deps_mode(args)) + return true +end + +return cmd_remove diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/search.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/search.lua new file mode 100644 index 000000000..6cab6d800 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/search.lua @@ -0,0 +1,84 @@ + +--- Module implementing the LuaRocks "search" command. +-- Queries LuaRocks servers. +local cmd_search = {} + +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local search = require("luarocks.search") +local queries = require("luarocks.queries") +local results = require("luarocks.results") + +function cmd_search.add_to_parser(parser) + local cmd = parser:command("search", "Query the LuaRocks servers.", util.see_also()) + + cmd:argument("name", "Name of the rock to search for.") + :args("?") + :action(util.namespaced_name_action) + cmd:argument("version", "Rock version to search for.") + :args("?") + + cmd:flag("--source", "Return only rockspecs and source rocks, to be used ".. + 'with the "build" command.') + cmd:flag("--binary", "Return only pure Lua and binary rocks (rocks that ".. + 'can be used with the "install" command without requiring a C toolchain).') + cmd:flag("--all", "List all contents of the server that are suitable to ".. + "this platform, do not filter by name.") + cmd:flag("--porcelain", "Return a machine readable format.") +end + +--- Splits a list of search results into two lists, one for "source" results +-- to be used with the "build" command, and one for "binary" results to be +-- used with the "install" command. +-- @param result_tree table: A search results table. +-- @return (table, table): Two tables, one for source and one for binary +-- results. +local function split_source_and_binary_results(result_tree) + local sources, binaries = {}, {} + for name, versions in pairs(result_tree) do + for version, repositories in pairs(versions) do + for _, repo in ipairs(repositories) do + local where = sources + if repo.arch == "all" or repo.arch == cfg.arch then + where = binaries + end + local entry = results.new(name, version, repo.repo, repo.arch) + search.store_result(where, entry) + end + end + end + return sources, binaries +end + +--- Driver function for "search" command. +-- @return boolean or (nil, string): True if build was successful; nil and an +-- error message otherwise. +function cmd_search.command(args) + local name = args.name + + if args.all then + name, args.version = "", nil + end + + if not args.name and not args.all then + return nil, "Enter name and version or use --all. "..util.see_help("search") + end + + local query = queries.new(name, args.namespace, args.version, true) + local result_tree, err = search.search_repos(query) + local porcelain = args.porcelain + local full_name = util.format_rock_name(name, args.namespace, args.version) + util.title(full_name .. " - Search results for Lua "..cfg.lua_version..":", porcelain, "=") + local sources, binaries = split_source_and_binary_results(result_tree) + if next(sources) and not args.binary then + util.title("Rockspecs and source rocks:", porcelain) + search.print_result_tree(sources, porcelain) + end + if next(binaries) and not args.source then + util.title("Binary and pure-Lua rocks:", porcelain) + search.print_result_tree(binaries, porcelain) + end + return true +end + +return cmd_search diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/show.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/show.lua new file mode 100644 index 000000000..d93459fd2 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/show.lua @@ -0,0 +1,315 @@ +--- Module implementing the LuaRocks "show" command. +-- Shows information about an installed rock. +local show = {} + +local queries = require("luarocks.queries") +local search = require("luarocks.search") +local dir = require("luarocks.core.dir") +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local path = require("luarocks.path") +local fetch = require("luarocks.fetch") +local manif = require("luarocks.manif") +local repos = require("luarocks.repos") + +function show.add_to_parser(parser) + local cmd = parser:command("show", [[ +Show information about an installed rock. + +Without any flags, show all module information. +With flags, return only the desired information.]], util.see_also()) + :summary("Show information about an installed rock.") + + cmd:argument("rock", "Name of an installed rock.") + :action(util.namespaced_name_action) + cmd:argument("version", "Rock version.") + :args("?") + + cmd:flag("--home", "Show home page of project.") + cmd:flag("--modules", "Show all modules provided by the package as used by require().") + cmd:flag("--deps", "Show packages the package depends on.") + cmd:flag("--build-deps", "Show build-only dependencies for the package.") + cmd:flag("--test-deps", "Show dependencies for testing the package.") + cmd:flag("--rockspec", "Show the full path of the rockspec file.") + cmd:flag("--mversion", "Show the package version.") + cmd:flag("--rock-tree", "Show local tree where rock is installed.") + cmd:flag("--rock-namespace", "Show rock namespace.") + cmd:flag("--rock-dir", "Show data directory of the installed rock.") + cmd:flag("--rock-license", "Show rock license.") + cmd:flag("--issues", "Show URL for project's issue tracker.") + cmd:flag("--labels", "List the labels of the rock.") + cmd:flag("--porcelain", "Produce machine-friendly output.") +end + +local friendly_template = [[ + : +?namespace:${namespace}/${package} ${version} - ${summary} +!namespace:${package} ${version} - ${summary} + : +*detailed :${detailed} +?detailed : +?license :License: \t${license} +?homepage :Homepage: \t${homepage} +?issues :Issues: \t${issues} +?labels :Labels: \t${labels} +?location :Installed in: \t${location} +?commands : +?commands :Commands: +*commands :\t${name} (${file}) +?modules : +?modules :Modules: +*modules :\t${name} (${file}) +?bdeps : +?bdeps :Has build dependency on: +*bdeps :\t${name} (${label}) +?tdeps : +?tdeps :Tests depend on: +*tdeps :\t${name} (${label}) +?deps : +?deps :Depends on: +*deps :\t${name} (${label}) +?ideps : +?ideps :Indirectly pulling: +*ideps :\t${name} (${label}) + : +]] + +local porcelain_template = [[ +?namespace:namespace\t${namespace} +?package :package\t${package} +?version :version\t${version} +?summary :summary\t${summary} +*detailed :detailed\t${detailed} +?license :license\t${license} +?homepage :homepage\t${homepage} +?issues :issues\t${issues} +?labels :labels\t${labels} +?location :location\t${location} +*commands :command\t${name}\t${file} +*modules :module\t${name}\t${file} +*bdeps :build_dependency\t${name}\t${label} +*tdeps :test_dependency\t${name}\t${label} +*deps :dependency\t${name}\t${label} +*ideps :indirect_dependency\t${name}\t${label} +]] + +local function keys_as_string(t, sep) + local keys = util.keys(t) + table.sort(keys) + return table.concat(keys, sep or " ") +end + +local function word_wrap(line) + local width = tonumber(os.getenv("COLUMNS")) or 80 + if width > 80 then width = 80 end + if #line > width then + local brk = width + while brk > 0 and line:sub(brk, brk) ~= " " do + brk = brk - 1 + end + if brk > 0 then + return line:sub(1, brk-1) .. "\n" .. word_wrap(line:sub(brk+1)) + end + end + return line +end + +local function format_text(text) + text = text:gsub("^%s*",""):gsub("%s$", ""):gsub("\n[ \t]+","\n"):gsub("([^\n])\n([^\n])","%1 %2") + local paragraphs = util.split_string(text, "\n\n") + for n, line in ipairs(paragraphs) do + paragraphs[n] = word_wrap(line) + end + return (table.concat(paragraphs, "\n\n"):gsub("%s$", "")) +end + +local function installed_rock_label(dep, tree) + local installed, version + local rocks_provided = util.get_rocks_provided() + if rocks_provided[dep.name] then + installed, version = true, rocks_provided[dep.name] + else + installed, version = search.pick_installed_rock(dep, tree) + end + return installed and "using "..version or "missing" +end + +local function render(template, data) + local out = {} + for cmd, var, line in template:gmatch("(.)([a-z]*)%s*:([^\n]*)\n") do + line = line:gsub("\\t", "\t") + local d = data[var] + if cmd == " " then + table.insert(out, line) + elseif cmd == "?" or cmd == "*" or cmd == "!" then + if (cmd == "!" and d == nil) + or (cmd ~= "!" and (type(d) == "string" + or (type(d) == "table" and next(d)))) then + local n = cmd == "*" and #d or 1 + for i = 1, n do + local tbl = cmd == "*" and d[i] or data + if type(tbl) == "string" then + tbl = tbl:gsub("%%", "%%%%") + end + table.insert(out, (line:gsub("${([a-z]+)}", tbl))) + end + end + end + end + return table.concat(out, "\n") +end + +local function adjust_path(name, version, basedir, pathname, suffix) + pathname = dir.path(basedir, pathname) + local vpathname = path.versioned_name(pathname, basedir, name, version) + return (fs.exists(vpathname) + and vpathname + or pathname) .. (suffix or "") +end + +local function modules_to_list(name, version, repo) + local ret = {} + local rock_manifest = manif.load_rock_manifest(name, version, repo) + + local lua_dir = path.deploy_lua_dir(repo) + local lib_dir = path.deploy_lib_dir(repo) + repos.recurse_rock_manifest_entry(rock_manifest.lua, function(pathname) + table.insert(ret, { + name = path.path_to_module(pathname), + file = adjust_path(name, version, lua_dir, pathname), + }) + end) + repos.recurse_rock_manifest_entry(rock_manifest.lib, function(pathname) + table.insert(ret, { + name = path.path_to_module(pathname), + file = adjust_path(name, version, lib_dir, pathname), + }) + end) + table.sort(ret, function(a, b) + if a.name == b.name then + return a.file < b.file + end + return a.name < b.name + end) + return ret +end + +local function commands_to_list(name, version, repo) + local ret = {} + local rock_manifest = manif.load_rock_manifest(name, version, repo) + + local bin_dir = path.deploy_bin_dir(repo) + repos.recurse_rock_manifest_entry(rock_manifest.bin, function(pathname) + pathname = adjust_path(name, version, bin_dir, pathname) + table.insert(ret, { + name = pathname, + file = adjust_path(name, version, bin_dir, pathname, cfg.wrapper_suffix), + }) + end) + table.sort(ret, function(a, b) + if a.name == b.name then + return a.file < b.file + end + return a.name < b.name + end) + return ret +end + +local function deps_to_list(dependencies, tree) + local ret = {} + for _, dep in ipairs(dependencies or {}) do + table.insert(ret, { name = tostring(dep), label = installed_rock_label(dep, tree) }) + end + return ret +end + +local function indirect_deps(mdeps, rdeps, tree) + local ret = {} + local direct_deps = {} + for _, dep in ipairs(rdeps) do + direct_deps[dep] = true + end + for dep_name in util.sortedpairs(mdeps or {}) do + if not direct_deps[dep_name] then + table.insert(ret, { name = tostring(dep_name), label = installed_rock_label(queries.new(dep_name), tree) }) + end + end + return ret +end + +local function show_rock(template, namespace, name, version, rockspec, repo, minfo, tree) + local desc = rockspec.description or {} + local data = { + namespace = namespace, + package = rockspec.package, + version = rockspec.version, + summary = desc.summary or "", + detailed = desc.detailed and util.split_string(format_text(desc.detailed), "\n"), + license = desc.license, + homepage = desc.homepage, + issues = desc.issues_url, + labels = desc.labels and table.concat(desc.labels, ", "), + location = path.rocks_tree_to_string(repo), + commands = commands_to_list(name, version, repo), + modules = modules_to_list(name, version, repo), + bdeps = deps_to_list(rockspec.build_dependencies, tree), + tdeps = deps_to_list(rockspec.test_dependencies, tree), + deps = deps_to_list(rockspec.dependencies, tree), + ideps = indirect_deps(minfo.dependencies, rockspec.dependencies, tree), + } + util.printout(render(template, data)) +end + +--- Driver function for "show" command. +-- @return boolean: True if succeeded, nil on errors. +function show.command(args) + local query = queries.new(args.rock, args.namespace, args.version, true) + + local name, version, repo, repo_url = search.pick_installed_rock(query, args.tree) + if not name then + return nil, version + end + local tree = path.rocks_tree_to_string(repo) + local directory = path.install_dir(name, version, repo) + local namespace = path.read_namespace(name, version, tree) + local rockspec_file = path.rockspec_file(name, version, repo) + local rockspec, err = fetch.load_local_rockspec(rockspec_file) + if not rockspec then return nil,err end + + local descript = rockspec.description or {} + local manifest, err = manif.load_manifest(repo_url) + if not manifest then return nil,err end + local minfo = manifest.repository[name][version][1] + + if args.rock_tree then util.printout(tree) + elseif args.rock_namespace then util.printout(namespace) + elseif args.rock_dir then util.printout(directory) + elseif args.home then util.printout(descript.homepage) + elseif args.rock_license then util.printout(descript.license) + elseif args.issues then util.printout(descript.issues_url) + elseif args.labels then util.printout(descript.labels and table.concat(descript.labels, "\n")) + elseif args.modules then util.printout(keys_as_string(minfo.modules, "\n")) + elseif args.deps then + for _, dep in ipairs(rockspec.dependencies) do + util.printout(tostring(dep)) + end + elseif args.build_deps then + for _, dep in ipairs(rockspec.build_dependencies) do + util.printout(tostring(dep)) + end + elseif args.test_deps then + for _, dep in ipairs(rockspec.test_dependencies) do + util.printout(tostring(dep)) + end + elseif args.rockspec then util.printout(rockspec_file) + elseif args.mversion then util.printout(version) + elseif args.porcelain then + show_rock(porcelain_template, namespace, name, version, rockspec, repo, minfo, args.tree) + else + show_rock(friendly_template, namespace, name, version, rockspec, repo, minfo, args.tree) + end + return true +end + +return show diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/test.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/test.lua new file mode 100644 index 000000000..b353bd80b --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/test.lua @@ -0,0 +1,48 @@ + +--- Module implementing the LuaRocks "test" command. +-- Tests a rock, compiling its C parts if any. +local cmd_test = {} + +local util = require("luarocks.util") +local test = require("luarocks.test") + +function cmd_test.add_to_parser(parser) + local cmd = parser:command("test", [[ +Run the test suite for the Lua project in the current directory. + +If the first argument is a rockspec, it will use it to determine the parameters +for running tests; otherwise, it will attempt to detect the rockspec. + +Any additional arguments are forwarded to the test suite. +To make sure that test suite flags are not interpreted as LuaRocks flags, use -- +to separate LuaRocks arguments from test suite arguments.]], + util.see_also()) + :summary("Run the test suite in the current directory.") + + cmd:argument("rockspec", "Project rockspec.") + :args("?") + cmd:argument("args", "Test suite arguments.") + :args("*") + cmd:flag("--prepare", "Only install dependencies needed for testing only, but do not run the test") + + cmd:option("--test-type", "Specify the test suite type manually if it was ".. + "not specified in the rockspec and it could not be auto-detected.") + :argname("") +end + +function cmd_test.command(args) + if args.rockspec and args.rockspec:match("rockspec$") then + return test.run_test_suite(args.rockspec, args.test_type, args.args, args.prepare) + end + + table.insert(args.args, 1, args.rockspec) + + local rockspec, err = util.get_default_rockspec() + if not rockspec then + return nil, err + end + + return test.run_test_suite(rockspec, args.test_type, args.args, args.prepare) +end + +return cmd_test diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/unpack.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/unpack.lua new file mode 100644 index 000000000..94da2c9f8 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/unpack.lua @@ -0,0 +1,168 @@ + +--- Module implementing the LuaRocks "unpack" command. +-- Unpack the contents of a rock. +local unpack = {} + +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local build = require("luarocks.build") +local dir = require("luarocks.dir") +local search = require("luarocks.search") + +function unpack.add_to_parser(parser) + local cmd = parser:command("unpack", [[ +Unpacks the contents of a rock in a newly created directory. +Argument may be a rock file, or the name of a rock in a rocks server. +In the latter case, the rock version may be given as a second argument.]], + util.see_also()) + :summary("Unpack the contents of a rock.") + + cmd:argument("rock", "A rock file or the name of a rock.") + :action(util.namespaced_name_action) + cmd:argument("version", "Rock version.") + :args("?") + + cmd:flag("--force", "Unpack files even if the output directory already exists.") + cmd:flag("--check-lua-versions", "If the rock can't be found, check repository ".. + "and report if it is available for another Lua version.") +end + +--- Load a rockspec file to the given directory, fetches the source +-- files specified in the rockspec, and unpack them inside the directory. +-- @param rockspec_file string: The URL for a rockspec file. +-- @param dir_name string: The directory where to store and unpack files. +-- @return table or (nil, string): the loaded rockspec table or +-- nil and an error message. +local function unpack_rockspec(rockspec_file, dir_name) + assert(type(rockspec_file) == "string") + assert(type(dir_name) == "string") + + local rockspec, err = fetch.load_rockspec(rockspec_file) + if not rockspec then + return nil, "Failed loading rockspec "..rockspec_file..": "..err + end + local ok, err = fs.change_dir(dir_name) + if not ok then return nil, err end + local ok, sources_dir = fetch.fetch_sources(rockspec, true, ".") + if not ok then + return nil, sources_dir + end + ok, err = fs.change_dir(sources_dir) + if not ok then return nil, err end + ok, err = build.apply_patches(rockspec) + fs.pop_dir() + if not ok then return nil, err end + return rockspec +end + +--- Load a .rock file to the given directory and unpack it inside it. +-- @param rock_file string: The URL for a .rock file. +-- @param dir_name string: The directory where to unpack. +-- @param kind string: the kind of rock file, as in the second-level +-- extension in the rock filename (eg. "src", "all", "linux-x86") +-- @return table or (nil, string): the loaded rockspec table or +-- nil and an error message. +local function unpack_rock(rock_file, dir_name, kind) + assert(type(rock_file) == "string") + assert(type(dir_name) == "string") + + local ok, err, errcode = fetch.fetch_and_unpack_rock(rock_file, dir_name) + if not ok then + return nil, err, errcode + end + ok, err = fs.change_dir(dir_name) + if not ok then return nil, err end + local rockspec_file = dir_name..".rockspec" + local rockspec, err = fetch.load_rockspec(rockspec_file) + if not rockspec then + return nil, "Failed loading rockspec "..rockspec_file..": "..err + end + if kind == "src" then + if rockspec.source.file then + local ok, err = fs.unpack_archive(rockspec.source.file) + if not ok then + return nil, err + end + ok, err = fs.change_dir(rockspec.source.dir) + if not ok then return nil, err end + ok, err = build.apply_patches(rockspec) + fs.pop_dir() + if not ok then return nil, err end + end + end + return rockspec +end + +--- Create a directory and perform the necessary actions so that +-- the sources for the rock and its rockspec are unpacked inside it, +-- laid out properly so that the 'make' command is able to build the module. +-- @param file string: A rockspec or .rock URL. +-- @return boolean or (nil, string): true if successful or nil followed +-- by an error message. +local function run_unpacker(file, force) + assert(type(file) == "string") + + local base_name = dir.base_name(file) + local dir_name, kind, extension = base_name:match("(.*)%.([^.]+)%.(rock)$") + if not extension then + dir_name, extension = base_name:match("(.*)%.(rockspec)$") + kind = "rockspec" + end + if not extension then + return nil, file.." does not seem to be a valid filename." + end + + local exists = fs.exists(dir_name) + if exists and not force then + return nil, "Directory "..dir_name.." already exists." + end + if not exists then + local ok, err = fs.make_dir(dir_name) + if not ok then return nil, err end + end + local rollback = util.schedule_function(fs.delete, fs.absolute_name(dir_name)) + + local rockspec, err + if extension == "rock" then + rockspec, err = unpack_rock(file, dir_name, kind) + elseif extension == "rockspec" then + rockspec, err = unpack_rockspec(file, dir_name) + end + if not rockspec then + return nil, err + end + if kind == "src" or kind == "rockspec" then + if rockspec.source.dir ~= "." then + local ok = fs.copy(rockspec.local_abs_filename, rockspec.source.dir, "read") + if not ok then + return nil, "Failed copying unpacked rockspec into unpacked source directory." + end + end + util.printout() + util.printout("Done. You may now enter directory ") + util.printout(dir.path(dir_name, rockspec.source.dir)) + util.printout("and type 'luarocks make' to build.") + end + util.remove_scheduled_function(rollback) + return true +end + +--- Driver function for the "unpack" command. +-- @return boolean or (nil, string): true if successful or nil followed +-- by an error message. +function unpack.command(args) + local url, err + if args.rock:match(".*%.rock") or args.rock:match(".*%.rockspec") then + url = args.rock + else + url, err = search.find_src_or_rockspec(args.rock, args.namespace, args.version, args.check_lua_versions) + if not url then + return nil, err + end + end + + return run_unpacker(url, args.force) +end + +return unpack diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/upload.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/upload.lua new file mode 100644 index 000000000..6b84e452f --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/upload.lua @@ -0,0 +1,128 @@ + +local upload = {} + +local signing = require("luarocks.signing") +local util = require("luarocks.util") +local fetch = require("luarocks.fetch") +local pack = require("luarocks.pack") +local cfg = require("luarocks.core.cfg") +local Api = require("luarocks.upload.api") + +function upload.add_to_parser(parser) + local cmd = parser:command("upload", "Pack a source rock file (.src.rock extension) ".. + "and upload it and the rockspec to the public rocks repository.", util.see_also()) + :summary("Upload a rockspec to the public rocks repository.") + + cmd:argument("rockspec", "Rockspec for the rock to upload.") + cmd:argument("src-rock", "A corresponding .src.rock file; if not given it will be generated.") + :args("?") + + cmd:flag("--skip-pack", "Do not pack and send source rock.") + cmd:option("--api-key", "Pass an API key. It will be stored for subsequent uses.") + :argname("") + cmd:option("--temp-key", "Use the given a temporary API key in this ".. + "invocation only. It will not be stored.") + :argname("") + cmd:flag("--force", "Replace existing rockspec if the same revision of a ".. + "module already exists. This should be used only in case of upload ".. + "mistakes: when updating a rockspec, increment the revision number ".. + "instead.") + cmd:flag("--sign", "Upload a signature file alongside each file as well.") + cmd:flag("--debug"):hidden(true) +end + +local function is_dev_version(version) + return version:match("^dev") or version:match("^scm") +end + +function upload.command(args) + local api, err = Api.new(args) + if not api then + return nil, err + end + if cfg.verbose then + api.debug = true + end + + local rockspec, err, errcode = fetch.load_rockspec(args.rockspec) + if err then + return nil, err, errcode + end + + util.printout("Sending " .. tostring(args.rockspec) .. " ...") + local res, err = api:method("check_rockspec", { + package = rockspec.package, + version = rockspec.version + }) + if not res then return nil, err end + + if not res.module then + util.printout("Will create new module (" .. tostring(rockspec.package) .. ")") + end + if res.version and not args.force then + return nil, "Revision "..rockspec.version.." already exists on the server. "..util.see_help("upload") + end + + local sigfname + local rock_sigfname + + if args.sign then + sigfname, err = signing.sign_file(args.rockspec) + if err then + return nil, "Failed signing rockspec: " .. err + end + util.printout("Signed rockspec: "..sigfname) + end + + local rock_fname + if args.src_rock then + rock_fname = args.src_rock + elseif not args.skip_pack and not is_dev_version(rockspec.version) then + util.printout("Packing " .. tostring(rockspec.package)) + rock_fname, err = pack.pack_source_rock(args.rockspec) + if not rock_fname then + return nil, err + end + end + + if rock_fname and args.sign then + rock_sigfname, err = signing.sign_file(rock_fname) + if err then + return nil, "Failed signing rock: " .. err + end + util.printout("Signed packed rock: "..rock_sigfname) + end + + local multipart = require("luarocks.upload.multipart") + + res, err = api:method("upload", nil, { + rockspec_file = multipart.new_file(args.rockspec), + rockspec_sig = sigfname and multipart.new_file(sigfname), + }) + if not res then return nil, err end + + if res.is_new and #res.manifests == 0 then + util.printerr("Warning: module not added to root manifest due to name taken.") + end + + local module_url = res.module_url + + if rock_fname then + if (not res.version) or (not res.version.id) then + return nil, "Invalid response from server." + end + util.printout(("Sending " .. tostring(rock_fname) .. " ...")) + res, err = api:method("upload_rock/" .. ("%d"):format(res.version.id), nil, { + rock_file = multipart.new_file(rock_fname), + rock_sig = rock_sigfname and multipart.new_file(rock_sigfname), + }) + if not res then return nil, err end + end + + util.printout() + util.printout("Done: " .. tostring(module_url)) + util.printout() + return true +end + +return upload diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/which.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/which.lua new file mode 100644 index 000000000..f50a43c33 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/which.lua @@ -0,0 +1,40 @@ + +--- @module luarocks.which_cmd +-- Driver for the `luarocks which` command. +local which_cmd = {} + +local loader = require("luarocks.loader") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") + +function which_cmd.add_to_parser(parser) + local cmd = parser:command("which", 'Given a module name like "foo.bar", '.. + "output which file would be loaded to resolve that module by ".. + 'luarocks.loader, like "/usr/local/lua/'..cfg.lua_version..'/foo/bar.lua".', + util.see_also()) + :summary("Tell which file corresponds to a given module name.") + + cmd:argument("modname", "Module name.") +end + +--- Driver function for "which" command. +-- @return boolean This function terminates the interpreter. +function which_cmd.command(args) + local pathname, rock_name, rock_version, where = loader.which(args.modname, "lp") + + if pathname then + util.printout(pathname) + if where == "l" then + util.printout("(provided by " .. tostring(rock_name) .. " " .. tostring(rock_version) .. ")") + else + local key = rock_name + util.printout("(found directly via package." .. key.. " -- not installed as a rock?)") + end + return true + end + + return nil, "Module '" .. args.modname .. "' not found." +end + +return which_cmd + diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/write_rockspec.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/write_rockspec.lua new file mode 100644 index 000000000..871cdd44c --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/cmd/write_rockspec.lua @@ -0,0 +1,408 @@ + +local write_rockspec = {} + +local builtin = require("luarocks.build.builtin") +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local persist = require("luarocks.persist") +local rockspecs = require("luarocks.rockspecs") +local type_rockspec = require("luarocks.type.rockspec") +local util = require("luarocks.util") + +local lua_versions = { + "5.1", + "5.2", + "5.3", + "5.4", + "5.1,5.2", + "5.2,5.3", + "5.3,5.4", + "5.1,5.2,5.3", + "5.2,5.3,5.4", + "5.1,5.2,5.3,5.4" +} + +function write_rockspec.cmd_options(parser) + return parser:option("--output", "Write the rockspec with the given filename.\n".. + "If not given, a file is written in the current directory with a ".. + "filename based on given name and version.") + :argname(""), + parser:option("--license", 'A license string, such as "MIT/X11" or "GNU GPL v3".') + :argname(""), + parser:option("--summary", "A short one-line description summary.") + :argname(""), + parser:option("--detailed", "A longer description string.") + :argname(""), + parser:option("--homepage", "Project homepage.") + :argname(""), + parser:option("--lua-versions", 'Supported Lua versions. Accepted values are: "'.. + table.concat(lua_versions, '", "')..'".') + :argname("") + :choices(lua_versions), + parser:option("--rockspec-format", 'Rockspec format version, such as "1.0" or "1.1".') + :argname(""), + parser:option("--tag", "Tag to use. Will attempt to extract version number from it."), + parser:option("--lib", "A comma-separated list of libraries that C files need to link to.") + :argname("") +end + +function write_rockspec.add_to_parser(parser) + local cmd = parser:command("write_rockspec", [[ +This command writes an initial version of a rockspec file, +based on a name, a version, and a location (an URL or a local path). +If only two arguments are given, the first one is considered the name and the +second one is the location. +If only one argument is given, it must be the location. +If no arguments are given, current directory is used as the location. +LuaRocks will attempt to infer name and version if not given, +using 'dev' as a fallback default version. + +Note that the generated file is a _starting point_ for writing a +rockspec, and is not guaranteed to be complete or correct. ]], util.see_also()) + :summary("Write a template for a rockspec file.") + + cmd:argument("name", "Name of the rock.") + :args("?") + cmd:argument("version", "Rock version.") + :args("?") + cmd:argument("location", "URL or path to the rock sources.") + :args("?") + + write_rockspec.cmd_options(cmd) +end + +local function open_file(name) + return io.open(dir.path(fs.current_dir(), name), "r") +end + +local function fetch_url(rockspec) + local file, temp_dir, err_code, err_file, err_temp_dir = fetch.fetch_sources(rockspec, false) + if err_code == "source.dir" then + file, temp_dir = err_file, err_temp_dir + elseif not file then + util.warning("Could not fetch sources - "..temp_dir) + return false + end + util.printout("File successfully downloaded. Making checksum and checking base dir...") + if dir.is_basic_protocol(rockspec.source.protocol) then + rockspec.source.md5 = fs.get_md5(file) + end + local inferred_dir, found_dir = fetch.find_base_dir(file, temp_dir, rockspec.source.url) + return true, found_dir or inferred_dir, temp_dir +end + +local lua_version_dep = { + ["5.1"] = "lua ~> 5.1", + ["5.2"] = "lua ~> 5.2", + ["5.3"] = "lua ~> 5.3", + ["5.4"] = "lua ~> 5.4", + ["5.1,5.2"] = "lua >= 5.1, < 5.3", + ["5.2,5.3"] = "lua >= 5.2, < 5.4", + ["5.3,5.4"] = "lua >= 5.3, < 5.5", + ["5.1,5.2,5.3"] = "lua >= 5.1, < 5.4", + ["5.2,5.3,5.4"] = "lua >= 5.2, < 5.5", + ["5.1,5.2,5.3,5.4"] = "lua >= 5.1, < 5.5", +} + +local simple_scm_protocols = { + git = true, + ["git+http"] = true, + ["git+https"] = true, + ["git+ssh"] = true, + hg = true, + ["hg+http"] = true, + ["hg+https"] = true, + ["hg+ssh"] = true, +} + +local detect_url +do + local function detect_url_from_command(program, args, directory) + local command = fs.Q(cfg.variables[program:upper()]).. " "..args + local pipe = io.popen(fs.command_at(directory, fs.quiet_stderr(command))) + if not pipe then return nil end + local url = pipe:read("*a"):match("^([^\r\n]+)") + pipe:close() + if not url then return nil end + if url:match("^[^@:/]+@[^@:/]+:.*$") then + local u, h, p = url:match("^([^@]+)@([^:]+):(.*)$") + url = program.."+ssh://"..u.."@"..h.."/"..p + elseif not util.starts_with(url, program.."://") then + url = program.."+"..url + end + + if simple_scm_protocols[dir.split_url(url)] then + return url + end + end + + local function detect_scm_url(directory) + return detect_url_from_command("git", "config --get remote.origin.url", directory) or + detect_url_from_command("hg", "paths default", directory) + end + + detect_url = function(url_or_dir) + if url_or_dir:match("://") then + return url_or_dir + else + return detect_scm_url(url_or_dir) or "*** please add URL for source tarball, zip or repository here ***" + end + end +end + +local function detect_homepage(url, homepage) + if homepage then + return homepage + end + local url_protocol, url_path = dir.split_url(url) + + if simple_scm_protocols[url_protocol] then + for _, domain in ipairs({"github.com", "bitbucket.org", "gitlab.com"}) do + if util.starts_with(url_path, domain) then + return "https://"..url_path:gsub("%.git$", "") + end + end + end + + return "*** please enter a project homepage ***" +end + +local function detect_description() + local fd = open_file("README.md") or open_file("README") + if not fd then return end + local data = fd:read("*a") + fd:close() + local paragraph = data:match("\n\n([^%[].-)\n\n") + if not paragraph then paragraph = data:match("\n\n(.*)") end + local summary, detailed + if paragraph then + detailed = paragraph + + if #paragraph < 80 then + summary = paragraph:gsub("\n", "") + else + summary = paragraph:gsub("\n", " "):match("([^.]*%.) ") + end + end + return summary, detailed +end + +local licenses = { + [78656] = "MIT", + [49311] = "ISC", +} + +local function detect_license(data) + local strip_copyright = (data:gsub("^Copyright [^\n]*\n", "")) + local sum = 0 + for i = 1, #strip_copyright do + local num = string.byte(strip_copyright:sub(i,i)) + if num > 32 and num <= 128 then + sum = sum + num + end + end + return licenses[sum] +end + +local function check_license() + local fd = open_file("COPYING") or open_file("LICENSE") or open_file("MIT-LICENSE.txt") + if not fd then return nil end + local data = fd:read("*a") + fd:close() + local license = detect_license(data) + if license then + return license, data + end + return nil, data +end + +local function fill_as_builtin(rockspec, libs) + rockspec.build.type = "builtin" + + local incdirs, libdirs + if libs then + incdirs, libdirs = {}, {} + for _, lib in ipairs(libs) do + local upper = lib:upper() + incdirs[#incdirs+1] = "$("..upper.."_INCDIR)" + libdirs[#libdirs+1] = "$("..upper.."_LIBDIR)" + end + end + + rockspec.build.modules, rockspec.build.install, rockspec.build.copy_directories = builtin.autodetect_modules(libs, incdirs, libdirs) +end + +local function rockspec_cleanup(rockspec) + rockspec.source.file = nil + rockspec.source.protocol = nil + rockspec.source.identifier = nil + rockspec.source.dir = nil + rockspec.source.dir_set = nil + rockspec.source.pathname = nil + rockspec.variables = nil + rockspec.name = nil + rockspec.format_is_at_least = nil + rockspec.local_abs_filename = nil + rockspec.rocks_provided = nil + for _, list in ipairs({"dependencies", "build_dependencies", "test_dependencies"}) do + if rockspec[list] and not next(rockspec[list]) then + rockspec[list] = nil + end + end + for _, list in ipairs({"dependencies", "build_dependencies", "test_dependencies"}) do + if rockspec[list] then + for i, entry in ipairs(rockspec[list]) do + rockspec[list][i] = tostring(entry) + end + end + end +end + +function write_rockspec.command(args) + local name, version = args.name, args.version + local location = args.location + + if not name then + location = "." + elseif not version then + location = name + name = nil + elseif not location then + location = version + version = nil + end + + if args.tag then + if not version then + version = args.tag:gsub("^v", "") + end + end + + local protocol, pathname = dir.split_url(location) + if protocol == "file" then + if pathname == "." then + name = name or dir.base_name(fs.current_dir()) + end + elseif dir.is_basic_protocol(protocol) then + local filename = dir.base_name(location) + local newname, newversion = filename:match("(.*)-([^-]+)") + if newname then + name = name or newname + version = version or newversion:gsub("%.[a-z]+$", ""):gsub("%.tar$", "") + end + else + name = name or dir.base_name(location):gsub("%.[^.]+$", "") + end + + if not name then + return nil, "Could not infer rock name. "..util.see_help("write_rockspec") + end + version = version or "dev" + + local filename = args.output or dir.path(fs.current_dir(), name:lower().."-"..version.."-1.rockspec") + + local url = detect_url(location) + local homepage = detect_homepage(url, args.homepage) + + local rockspec, err = rockspecs.from_persisted_table(filename, { + rockspec_format = args.rockspec_format, + package = name, + version = version.."-1", + source = { + url = url, + tag = args.tag, + }, + description = { + summary = args.summary or "*** please specify description summary ***", + detailed = args.detailed or "*** please enter a detailed description ***", + homepage = homepage, + license = args.license or "*** please specify a license ***", + }, + dependencies = { + lua_version_dep[args.lua_versions], + }, + build = {}, + }) + assert(not err, err) + rockspec.source.protocol = protocol + + if not next(rockspec.dependencies) then + util.warning("Please specify supported Lua versions with --lua-versions=. "..util.see_help("write_rockspec")) + end + + local local_dir = location + + if location:match("://") then + rockspec.source.file = dir.base_name(location) + if not dir.is_basic_protocol(rockspec.source.protocol) then + if version ~= "dev" then + rockspec.source.tag = args.tag or "v" .. version + end + end + rockspec.source.dir = nil + local ok, base_dir, temp_dir = fetch_url(rockspec) + if ok then + if base_dir ~= dir.base_name(location) then + rockspec.source.dir = base_dir + end + end + if base_dir then + local_dir = dir.path(temp_dir, base_dir) + else + local_dir = nil + end + end + + if not local_dir then + local_dir = "." + end + + local libs = nil + if args.lib then + libs = {} + rockspec.external_dependencies = {} + for lib in args.lib:gmatch("([^,]+)") do + table.insert(libs, lib) + rockspec.external_dependencies[lib:upper()] = { + library = lib + } + end + end + + local ok, err = fs.change_dir(local_dir) + if not ok then return nil, "Failed reaching files from project - error entering directory "..local_dir end + + if not (args.summary and args.detailed) then + local summary, detailed = detect_description() + rockspec.description.summary = args.summary or summary + rockspec.description.detailed = args.detailed or detailed + end + + if not args.license then + local license, fulltext = check_license() + if license then + rockspec.description.license = license + elseif license then + util.title("Could not auto-detect type for project license:") + util.printout(fulltext) + util.printout() + util.title("Please fill in the source.license field manually or use --license.") + end + end + + fill_as_builtin(rockspec, libs) + + rockspec_cleanup(rockspec) + + persist.save_from_table(filename, rockspec, type_rockspec.order) + + util.printout() + util.printout("Wrote template at "..filename.." -- you should now edit and finish it.") + util.printout() + + return true +end + +return write_rockspec diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/core/cfg.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/core/cfg.lua new file mode 100644 index 000000000..b3ff3a649 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/core/cfg.lua @@ -0,0 +1,922 @@ + +--- Configuration for LuaRocks. +-- Tries to load the user's configuration file and +-- defines defaults for unset values. See the +-- config +-- file format documentation for details. +-- +-- End-users shouldn't edit this file. They can override any defaults +-- set in this file using their system-wide or user-specific configuration +-- files. Run `luarocks` with no arguments to see the locations of +-- these files in your platform. + +local table, pairs, require, os, pcall, ipairs, package, type, assert = + table, pairs, require, os, pcall, ipairs, package, type, assert + +local util = require("luarocks.core.util") +local persist = require("luarocks.core.persist") +local sysdetect = require("luarocks.core.sysdetect") +local vers = require("luarocks.core.vers") + +-------------------------------------------------------------------------------- + +local program_version = "3.9.2" + +local is_windows = package.config:sub(1,1) == "\\" + +-- Set order for platform overrides. +-- More general platform identifiers should be listed first, +-- more specific ones later. +local platform_order = { + -- Unixes + "unix", + "bsd", + "solaris", + "netbsd", + "openbsd", + "freebsd", + "dragonfly", + "linux", + "macosx", + "cygwin", + "msys", + "haiku", + -- Windows + "windows", + "win32", + "mingw", + "mingw32", + "msys2_mingw_w64", +} + +local function detect_sysconfdir() + if not debug then + return + end + local src = debug.getinfo(1, "S").source:gsub("\\", "/"):gsub("/+", "/") + if src:sub(1, 1) == "@" then + src = src:sub(2) + end + local basedir = src:match("^(.*)/luarocks/core/cfg.lua$") + if not basedir then + return + end + -- If installed in a Unix-like tree, use a Unix-like sysconfdir + local installdir = basedir:match("^(.*)/share/lua/[^/]*$") + if installdir then + if installdir == "/usr" then + return "/etc/luarocks" + end + return installdir .. "/etc/luarocks" + end + -- Otherwise, use base directory of sources + return basedir +end + +local load_config_file +do + -- Create global environment for the config files; + local function env_for_config_file(cfg, platforms) + local platforms_copy = {} + for k,v in pairs(platforms) do + platforms_copy[k] = v + end + + local e + e = { + home = cfg.home, + lua_version = cfg.lua_version, + platforms = platforms_copy, + processor = cfg.target_cpu, -- remains for compat reasons + target_cpu = cfg.target_cpu, -- replaces `processor` + os_getenv = os.getenv, + variables = cfg.variables or {}, + dump_env = function() + -- debug function, calling it from a config file will show all + -- available globals to that config file + print(util.show_table(e, "global environment")) + end, + } + return e + end + + -- Merge values from config files read into the `cfg` table + local function merge_overrides(cfg, overrides) + -- remove some stuff we do not want to integrate + overrides.os_getenv = nil + overrides.dump_env = nil + -- remove tables to be copied verbatim instead of deeply merged + if overrides.rocks_trees then cfg.rocks_trees = nil end + if overrides.rocks_servers then cfg.rocks_servers = nil end + -- perform actual merge + util.deep_merge(cfg, overrides) + end + + local function update_platforms(platforms, overrides) + if overrides[1] then + for k, _ in pairs(platforms) do + platforms[k] = nil + end + for _, v in ipairs(overrides) do + platforms[v] = true + end + -- set some fallback default in case the user provides an incomplete configuration. + -- LuaRocks expects a set of defaults to be available. + if not (platforms.unix or platforms.windows) then + platforms[is_windows and "windows" or "unix"] = true + end + end + end + + -- Load config file and merge its contents into the `cfg` module table. + -- @return filepath of successfully loaded file or nil if it failed + load_config_file = function(cfg, platforms, filepath) + local result, err, errcode = persist.load_into_table(filepath, env_for_config_file(cfg, platforms)) + if (not result) and errcode ~= "open" then + -- errcode is either "load" or "run"; bad config file, so error out + return nil, err, "config" + end + if result then + -- success in loading and running, merge contents and exit + update_platforms(platforms, result.platforms) + result.platforms = nil + merge_overrides(cfg, result) + return filepath + end + return nil -- nothing was loaded + end +end + +local platform_sets = { + freebsd = { unix = true, bsd = true, freebsd = true }, + openbsd = { unix = true, bsd = true, openbsd = true }, + dragonfly = { unix = true, bsd = true, dragonfly = true }, + solaris = { unix = true, solaris = true }, + windows = { windows = true, win32 = true }, + cygwin = { unix = true, cygwin = true }, + macosx = { unix = true, bsd = true, macosx = true, macos = true }, + netbsd = { unix = true, bsd = true, netbsd = true }, + haiku = { unix = true, haiku = true }, + linux = { unix = true, linux = true }, + mingw = { windows = true, win32 = true, mingw32 = true, mingw = true }, + msys = { unix = true, cygwin = true, msys = true }, + msys2_mingw_w64 = { windows = true, win32 = true, mingw32 = true, mingw = true, msys = true, msys2_mingw_w64 = true }, +} + +local function make_platforms(system) + -- fallback to Unix in unknown systems + return platform_sets[system] or { unix = true } +end + +-------------------------------------------------------------------------------- + +local function make_defaults(lua_version, target_cpu, platforms, home, hardcoded) + + -- Configure defaults: + local defaults = { + + lua_interpreter = hardcoded.LUA_INTERPRETER or "lua", + local_by_default = hardcoded.LOCAL_BY_DEFAULT or false, + accept_unknown_fields = false, + fs_use_modules = true, + hooks_enabled = true, + deps_mode = "one", + no_manifest = false, + check_certificates = false, + + cache_timeout = 60, + cache_fail_timeout = 86400, + + lua_modules_path = hardcoded.LUA_MODULES_LUA_SUBDIR or "/share/lua/"..lua_version, + lib_modules_path = hardcoded.LUA_MODULES_LIB_SUBDIR or "/lib/lua/"..lua_version, + rocks_subdir = hardcoded.ROCKS_SUBDIR or "/lib/luarocks/rocks-"..lua_version, + + arch = "unknown", + lib_extension = "unknown", + obj_extension = "unknown", + link_lua_explicitly = false, + + rocks_servers = hardcoded.ROCKS_SERVERS or { + { + "https://luarocks.org", + "https://raw.githubusercontent.com/rocks-moonscript-org/moonrocks-mirror/master/", + "https://luafr.org/luarocks/", + } + }, + disabled_servers = {}, + + upload = { + server = "https://luarocks.org", + tool_version = "1.0.0", + api_version = "1", + }, + + lua_extension = "lua", + connection_timeout = 30, -- 0 = no timeout + + variables = { + MAKE = os.getenv("MAKE") or "make", + CC = os.getenv("CC") or "cc", + LD = os.getenv("CC") or "ld", + AR = os.getenv("AR") or "ar", + RANLIB = os.getenv("RANLIB") or "ranlib", + + CVS = "cvs", + GIT = "git", + SSCM = "sscm", + SVN = "svn", + HG = "hg", + + GPG = "gpg", + + RSYNC = "rsync", + WGET = "wget", + SCP = "scp", + CURL = "curl", + + PWD = "pwd", + MKDIR = "mkdir", + RMDIR = "rmdir", + CP = "cp", + LS = "ls", + RM = "rm", + FIND = "find", + CHMOD = "chmod", + ICACLS = "icacls", + MKTEMP = "mktemp", + + ZIP = "zip", + UNZIP = "unzip -n", + GUNZIP = "gunzip", + BUNZIP2 = "bunzip2", + TAR = "tar", + + MD5SUM = "md5sum", + OPENSSL = "openssl", + MD5 = "md5", + TOUCH = "touch", + + CMAKE = "cmake", + SEVENZ = "7z", + + RSYNCFLAGS = "--exclude=.git -Oavz", + CURLNOCERTFLAG = "", + WGETNOCERTFLAG = "", + }, + + external_deps_subdirs = hardcoded.EXTERNAL_DEPS_SUBDIRS or { + bin = "bin", + lib = "lib", + include = "include" + }, + runtime_external_deps_subdirs = hardcoded.RUNTIME_EXTERNAL_DEPS_SUBDIRS or { + bin = "bin", + lib = "lib", + include = "include" + }, + } + + if platforms.windows then + + defaults.arch = "win32-"..target_cpu + defaults.lib_extension = "dll" + defaults.external_lib_extension = "dll" + defaults.static_lib_extension = "lib" + defaults.obj_extension = "obj" + defaults.external_deps_dirs = { "c:/external/", "c:/windows/system32" } + + defaults.makefile = "Makefile.win" + defaults.variables.PWD = "echo %cd%" + defaults.variables.MAKE = os.getenv("MAKE") or "nmake" + defaults.variables.CC = os.getenv("CC") or "cl" + defaults.variables.RC = os.getenv("WINDRES") or "rc" + defaults.variables.LD = os.getenv("LINK") or "link" + defaults.variables.MT = os.getenv("MT") or "mt" + defaults.variables.AR = os.getenv("AR") or "lib" + defaults.variables.LUALIB = "lua"..lua_version..".lib" + defaults.variables.CFLAGS = os.getenv("CFLAGS") or "/nologo /MD /O2" + defaults.variables.LDFLAGS = os.getenv("LDFLAGS") + defaults.variables.LIBFLAG = "/nologo /dll" + + defaults.external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + lib = { "?.lib", "?.dll", "lib?.dll" }, + include = { "?.h" } + } + defaults.runtime_external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + lib = { "?.dll", "lib?.dll" }, + include = { "?.h" } + } + defaults.export_path_separator = ";" + defaults.wrapper_suffix = ".bat" + + local localappdata = os.getenv("LOCALAPPDATA") + if not localappdata then + -- for Windows versions below Vista + localappdata = (os.getenv("USERPROFILE") or "c:/Users/All Users").."/Local Settings/Application Data" + end + defaults.local_cache = localappdata.."/LuaRocks/Cache" + defaults.web_browser = "start" + + defaults.external_deps_subdirs.lib = { "", "lib", "bin" } + defaults.runtime_external_deps_subdirs.lib = { "", "lib", "bin" } + defaults.link_lua_explicitly = true + defaults.fs_use_modules = false + end + + if platforms.mingw32 then + defaults.obj_extension = "o" + defaults.static_lib_extension = "a" + defaults.external_deps_dirs = { "c:/external/", "c:/mingw", "c:/windows/system32" } + defaults.cmake_generator = "MinGW Makefiles" + defaults.variables.MAKE = os.getenv("MAKE") or "mingw32-make" + if target_cpu == "x86_64" then + defaults.variables.CC = os.getenv("CC") or "x86_64-w64-mingw32-gcc" + defaults.variables.LD = os.getenv("CC") or "x86_64-w64-mingw32-gcc" + else + defaults.variables.CC = os.getenv("CC") or "mingw32-gcc" + defaults.variables.LD = os.getenv("CC") or "mingw32-gcc" + end + defaults.variables.AR = os.getenv("AR") or "ar" + defaults.variables.RC = os.getenv("WINDRES") or "windres" + defaults.variables.RANLIB = os.getenv("RANLIB") or "ranlib" + defaults.variables.CFLAGS = os.getenv("CFLAGS") or "-O2" + defaults.variables.LDFLAGS = os.getenv("LDFLAGS") + defaults.variables.LIBFLAG = "-shared" + defaults.makefile = "Makefile" + defaults.external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + -- mingw lookup list from http://stackoverflow.com/a/15853231/1793220 + -- ...should we keep ?.lib at the end? It's not in the above list. + lib = { "lib?.dll.a", "?.dll.a", "lib?.a", "cyg?.dll", "lib?.dll", "?.dll", "?.lib" }, + include = { "?.h" } + } + defaults.runtime_external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + lib = { "cyg?.dll", "?.dll", "lib?.dll" }, + include = { "?.h" } + } + defaults.link_lua_explicitly = true + end + + if platforms.unix then + defaults.lib_extension = "so" + defaults.static_lib_extension = "a" + defaults.external_lib_extension = "so" + defaults.obj_extension = "o" + defaults.external_deps_dirs = { "/usr/local", "/usr", "/", hardcoded.PREFIX } + + defaults.variables.CFLAGS = os.getenv("CFLAGS") or "-O2" + -- we pass -fPIC via CFLAGS because of old Makefile-based Lua projects + -- which didn't have -fPIC in their Makefiles but which honor CFLAGS + if not defaults.variables.CFLAGS:match("-fPIC") then + defaults.variables.CFLAGS = defaults.variables.CFLAGS.." -fPIC" + end + + defaults.variables.LDFLAGS = os.getenv("LDFLAGS") + + defaults.cmake_generator = "Unix Makefiles" + defaults.variables.CC = os.getenv("CC") or "gcc" + defaults.variables.LD = os.getenv("CC") or "gcc" + defaults.gcc_rpath = true + defaults.variables.LIBFLAG = "-shared" + defaults.variables.TEST = "test" + + defaults.external_deps_patterns = { + bin = { "?" }, + lib = { "lib?.a", "lib?.so", "lib?.so.*" }, + include = { "?.h" } + } + defaults.runtime_external_deps_patterns = { + bin = { "?" }, + lib = { "lib?.so", "lib?.so.*" }, + include = { "?.h" } + } + defaults.export_path_separator = ":" + defaults.wrapper_suffix = "" + local xdg_cache_home = os.getenv("XDG_CACHE_HOME") or home.."/.cache" + defaults.local_cache = xdg_cache_home.."/luarocks" + defaults.web_browser = "xdg-open" + end + + if platforms.cygwin then + defaults.lib_extension = "so" -- can be overridden in the config file for mingw builds + defaults.arch = "cygwin-"..target_cpu + defaults.cmake_generator = "Unix Makefiles" + defaults.variables.CC = "echo -llua | xargs " .. (os.getenv("CC") or "gcc") + defaults.variables.LD = "echo -llua | xargs " .. (os.getenv("CC") or "gcc") + defaults.variables.LIBFLAG = "-shared" + defaults.link_lua_explicitly = true + end + + if platforms.msys then + -- msys is basically cygwin made out of mingw, meaning the subsytem is unixish + -- enough, yet we can freely mix with native win32 + defaults.external_deps_patterns = { + bin = { "?.exe", "?.bat", "?" }, + lib = { "lib?.so", "lib?.so.*", "lib?.dll.a", "?.dll.a", + "lib?.a", "lib?.dll", "?.dll", "?.lib" }, + include = { "?.h" } + } + defaults.runtime_external_deps_patterns = { + bin = { "?.exe", "?.bat" }, + lib = { "lib?.so", "?.dll", "lib?.dll" }, + include = { "?.h" } + } + if platforms.mingw then + -- MSYS2 can build Windows programs that depend on + -- msys-2.0.dll (based on Cygwin) but MSYS2 is also designed + -- for building native Windows programs by MinGW. These + -- programs don't depend on msys-2.0.dll. + local pipe = io.popen("cygpath --windows %MINGW_PREFIX%") + local mingw_prefix = pipe:read("*l") + pipe:close() + defaults.external_deps_dirs = { mingw_prefix, "c:/windows/system32" } + defaults.makefile = "Makefile" + defaults.cmake_generator = "MSYS Makefiles" + defaults.local_cache = home.."/.cache/luarocks" + defaults.variables.MAKE = os.getenv("MAKE") or "make" + defaults.variables.CC = os.getenv("CC") or "gcc" + defaults.variables.RC = os.getenv("WINDRES") or "windres" + defaults.variables.LD = os.getenv("CC") or "gcc" + defaults.variables.MT = os.getenv("MT") or nil + defaults.variables.AR = os.getenv("AR") or "ar" + defaults.variables.RANLIB = os.getenv("RANLIB") or "ranlib" + defaults.variables.LUALIB = "liblua"..lua_version..".dll.a" + + defaults.variables.CFLAGS = os.getenv("CFLAGS") or "-O2 -fPIC" + if not defaults.variables.CFLAGS:match("-fPIC") then + defaults.variables.CFLAGS = defaults.variables.CFLAGS.." -fPIC" + end + + defaults.variables.LIBFLAG = "-shared" + end + end + + if platforms.bsd then + defaults.variables.MAKE = "gmake" + defaults.gcc_rpath = false + defaults.variables.CC = os.getenv("CC") or "cc" + defaults.variables.LD = os.getenv("CC") or defaults.variables.CC + end + + if platforms.macosx then + defaults.variables.MAKE = os.getenv("MAKE") or "make" + defaults.external_lib_extension = "dylib" + defaults.arch = "macosx-"..target_cpu + defaults.variables.LIBFLAG = "-bundle -undefined dynamic_lookup -all_load" + local version = util.popen_read("sw_vers -productVersion") + if not (version:match("^%d+%.%d+%.%d+$") or version:match("^%d+%.%d+$")) then + version = "10.3" + end + version = vers.parse_version(version) + if version >= vers.parse_version("11.0") then + version = vers.parse_version("11.0") + elseif version >= vers.parse_version("10.10") then + version = vers.parse_version("10.8") + elseif version >= vers.parse_version("10.5") then + version = vers.parse_version("10.5") + else + defaults.gcc_rpath = false + end + defaults.variables.CC = "env MACOSX_DEPLOYMENT_TARGET="..tostring(version).." gcc" + defaults.variables.LD = "env MACOSX_DEPLOYMENT_TARGET="..tostring(version).." gcc" + defaults.web_browser = "open" + + -- XCode SDK + local sdk_path = util.popen_read("xcrun --show-sdk-path 2>/dev/null") + if sdk_path then + table.insert(defaults.external_deps_dirs, sdk_path .. "/usr") + table.insert(defaults.external_deps_patterns.lib, 1, "lib?.tbd") + table.insert(defaults.runtime_external_deps_patterns.lib, 1, "lib?.tbd") + end + + -- Homebrew + table.insert(defaults.external_deps_dirs, "/usr/local/opt") + defaults.external_deps_subdirs.lib = { "", "lib", } + defaults.runtime_external_deps_subdirs.lib = { "", "lib", } + table.insert(defaults.external_deps_patterns.lib, 1, "/?/lib/lib?.dylib") + table.insert(defaults.runtime_external_deps_patterns.lib, 1, "/?/lib/lib?.dylib") + end + + if platforms.linux then + defaults.arch = "linux-"..target_cpu + + local gcc_arch = util.popen_read("gcc -print-multiarch 2>/dev/null") + if gcc_arch and gcc_arch ~= "" then + defaults.external_deps_subdirs.lib = { "lib", "lib/" .. gcc_arch, "lib64" } + defaults.runtime_external_deps_subdirs.lib = { "lib", "lib/" .. gcc_arch, "lib64" } + else + defaults.external_deps_subdirs.lib = { "lib", "lib64" } + defaults.runtime_external_deps_subdirs.lib = { "lib", "lib64" } + end + end + + if platforms.freebsd then + defaults.arch = "freebsd-"..target_cpu + elseif platforms.dragonfly then + defaults.arch = "dragonfly-"..target_cpu + elseif platforms.openbsd then + defaults.arch = "openbsd-"..target_cpu + elseif platforms.netbsd then + defaults.arch = "netbsd-"..target_cpu + elseif platforms.solaris then + defaults.arch = "solaris-"..target_cpu + defaults.variables.MAKE = "gmake" + end + + -- Expose some more values detected by LuaRocks for use by rockspec authors. + defaults.variables.LIB_EXTENSION = defaults.lib_extension + defaults.variables.OBJ_EXTENSION = defaults.obj_extension + + return defaults +end + +local function use_defaults(cfg, defaults) + + -- Populate variables with values from their 'defaults' counterparts + -- if they were not already set by user. + if not cfg.variables then + cfg.variables = {} + end + for k,v in pairs(defaults.variables) do + if not cfg.variables[k] then + cfg.variables[k] = v + end + end + + util.deep_merge_under(cfg, defaults) + + -- FIXME get rid of this + if not cfg.check_certificates then + cfg.variables.CURLNOCERTFLAG = "-k" + cfg.variables.WGETNOCERTFLAG = "--no-check-certificate" + end +end + +local function get_first_arg() + local arg = rawget(_G, 'arg') + if not arg then + return + end + local first_arg = arg[0] + local i = -1 + while arg[i] do + first_arg = arg[i] + i = i -1 + end + return first_arg +end + +-------------------------------------------------------------------------------- + +local cfg = {} + +--- Initializes the LuaRocks configuration for variables, paths +-- and OS detection. +-- @param detected table containing information detected about the +-- environment. All fields below are optional: +-- * lua_version (in x.y format, e.g. "5.3") +-- * lua_bindir (e.g. "/usr/local/bin") +-- * lua_dir (e.g. "/usr/local") +-- * lua_interpreter (e.g. "lua-5.3") +-- * project_dir (a string with the path of the project directory +-- when using per-project environments, as created with `luarocks init`) +-- @param warning a logging function for warnings that takes a string +-- @return true on success; nil and an error message on failure. +function cfg.init(detected, warning) + if cfg.initialized == true then + return true + end + + detected = detected or {} + + local exit_ok = true + local exit_err = nil + local exit_what = nil + + local hc_ok, hardcoded = pcall(require, "luarocks.core.hardcoded") + if not hc_ok then + hardcoded = {} + end + + local init = cfg.init + + ---------------------------------------- + -- Reset the cfg table. + ---------------------------------------- + + for k, _ in pairs(cfg) do + cfg[k] = nil + end + + cfg.program_version = program_version + + if hardcoded.IS_BINARY then + cfg.is_binary = true + end + + -- Use detected values as defaults, overridable via config files or CLI args + + local first_arg = get_first_arg() + + cfg.lua_version = detected.lua_version or hardcoded.LUA_VERSION or _VERSION:sub(5) + cfg.lua_interpreter = detected.lua_interpreter or hardcoded.LUA_INTERPRETER or (first_arg and first_arg:gsub(".*[\\/]", "")) or (is_windows and "lua.exe" or "lua") + cfg.project_dir = (not hardcoded.FORCE_CONFIG) and detected.project_dir + + do + local lua_bindir = detected.lua_bindir or hardcoded.LUA_BINDIR or (first_arg and first_arg:gsub("[\\/][^\\/]+$", "")) + local lua_dir = detected.lua_dir or hardcoded.LUA_DIR or (lua_bindir and lua_bindir:gsub("[\\/]bin$", "")) + cfg.variables = { + LUA_DIR = lua_dir, + LUA_BINDIR = lua_bindir, + LUA_LIBDIR = hardcoded.LUA_LIBDIR, + LUA_INCDIR = hardcoded.LUA_INCDIR, + } + end + + cfg.init = init + + ---------------------------------------- + -- System detection. + ---------------------------------------- + + -- A proper build of LuaRocks will hardcode the system + -- and proc values with hardcoded.SYSTEM and hardcoded.PROCESSOR. + -- If that is not available, we try to identify the system. + local system, processor = sysdetect.detect() + if hardcoded.SYSTEM then + system = hardcoded.SYSTEM + end + if hardcoded.PROCESSOR then + processor = hardcoded.PROCESSOR + end + + if system == "windows" then + if os.getenv("VCINSTALLDIR") then + -- running from the Development Command prompt for VS 2017 + system = "windows" + else + local msystem = os.getenv("MSYSTEM") + if msystem == nil then + system = "mingw" + elseif msystem == "MSYS" then + system = "msys" + else + -- MINGW32 or MINGW64 + system = "msys2_mingw_w64" + end + end + end + + cfg.target_cpu = processor + + local platforms = make_platforms(system) + + ---------------------------------------- + -- Platform is determined. + -- Let's load the config files. + ---------------------------------------- + + local sys_config_file + local home_config_file + local project_config_file + + local config_file_name = "config-"..cfg.lua_version..".lua" + + do + local sysconfdir = os.getenv("LUAROCKS_SYSCONFDIR") or hardcoded.SYSCONFDIR + if platforms.windows and not platforms.msys2_mingw_w64 then + cfg.home = os.getenv("APPDATA") or "c:" + cfg.home_tree = cfg.home.."/luarocks" + cfg.sysconfdir = sysconfdir or ((os.getenv("PROGRAMFILES") or "c:") .. "/luarocks") + else + cfg.home = hardcoded.HOMEDIR or os.getenv("HOME") or "" + local localdir = hardcoded.LOCALDIR or cfg.home + local home_tree_subdir = hardcoded.HOME_TREE_SUBDIR or "/.luarocks" + cfg.homeconfdir = localdir .. home_tree_subdir + cfg.home_tree = localdir .. home_tree_subdir + cfg.sysconfdir = sysconfdir or detect_sysconfdir() or "/etc/luarocks" + end + end + + -- Load system configuration file + sys_config_file = (cfg.sysconfdir .. "/" .. config_file_name):gsub("\\", "/") + local sys_config_ok, err = load_config_file(cfg, platforms, sys_config_file) + if err then + exit_ok, exit_err, exit_what = nil, err, "config" + end + + -- Load user configuration file (if allowed) + local home_config_ok + local project_config_ok + if not hardcoded.FORCE_CONFIG then + local env_var = "LUAROCKS_CONFIG_" .. cfg.lua_version:gsub("%.", "_") + local env_value = os.getenv(env_var) + if not env_value then + env_var = "LUAROCKS_CONFIG" + env_value = os.getenv(env_var) + end + -- first try environment provided file, so we can explicitly warn when it is missing + if env_value then + local env_ok, err = load_config_file(cfg, platforms, env_value) + if err then + exit_ok, exit_err, exit_what = nil, err, "config" + elseif warning and not env_ok then + warning("Warning: could not load configuration file `"..env_value.."` given in environment variable "..env_var.."\n") + end + if env_ok then + home_config_ok = true + home_config_file = env_value + end + end + + -- try XDG config home + if platforms.unix and not home_config_ok then + local xdg_config_home = os.getenv("XDG_CONFIG_HOME") or cfg.home .. "/.config" + cfg.homeconfdir = xdg_config_home .. "/luarocks" + home_config_file = (cfg.homeconfdir .. "/" .. config_file_name):gsub("\\", "/") + home_config_ok, err = load_config_file(cfg, platforms, home_config_file) + if err then + exit_ok, exit_err, exit_what = nil, err, "config" + end + end + + -- try the alternative defaults if there was no environment specified file or it didn't work + if not home_config_ok then + cfg.homeconfdir = cfg.home_tree + home_config_file = (cfg.homeconfdir .. "/" .. config_file_name):gsub("\\", "/") + home_config_ok, err = load_config_file(cfg, platforms, home_config_file) + if err then + exit_ok, exit_err, exit_what = nil, err, "config" + end + end + + -- finally, use the project-specific config file if any + if cfg.project_dir then + project_config_file = cfg.project_dir .. "/.luarocks/" .. config_file_name + project_config_ok, err = load_config_file(cfg, platforms, project_config_file) + if err then + exit_ok, exit_err, exit_what = nil, err, "config" + end + end + end + + if hardcoded.FORCE_HARDCODED then + util.deep_merge(cfg.variables, hardcoded) + end + + ---------------------------------------- + -- Config files are loaded. + -- Let's finish up the cfg table. + ---------------------------------------- + + -- Settings given via the CLI (i.e. --lua-dir) take precedence over config files. + cfg.project_dir = detected.given_project_dir or cfg.project_dir + cfg.lua_version = detected.given_lua_version or cfg.lua_version + if detected.given_lua_dir then + cfg.variables.LUA_DIR = detected.given_lua_dir + cfg.variables.LUA_BINDIR = detected.lua_bindir + cfg.variables.LUA_LIBDIR = nil + cfg.variables.LUA_INCDIR = nil + cfg.lua_interpreter = detected.lua_interpreter + end + + -- Build a default list of rocks trees if not given + if cfg.rocks_trees == nil then + cfg.rocks_trees = {} + if cfg.home_tree then + table.insert(cfg.rocks_trees, { name = "user", root = cfg.home_tree } ) + end + if hardcoded.PREFIX and hardcoded.PREFIX ~= cfg.home_tree then + table.insert(cfg.rocks_trees, { name = "system", root = hardcoded.PREFIX } ) + end + end + + local defaults = make_defaults(cfg.lua_version, processor, platforms, cfg.home, hardcoded) + + if platforms.windows and hardcoded.WIN_TOOLS then + local tools = { "SEVENZ", "CP", "FIND", "LS", "MD5SUM", "WGET", } + for _, tool in ipairs(tools) do + defaults.variables[tool] = '"' .. hardcoded.WIN_TOOLS .. "/" .. defaults.variables[tool] .. '.exe"' + end + else + defaults.fs_use_modules = true + end + + use_defaults(cfg, defaults) + + cfg.variables.LUA = cfg.variables.LUA or (cfg.variables.LUA_BINDIR and (cfg.variables.LUA_BINDIR .. "/" .. cfg.lua_interpreter):gsub("//", "/")) + cfg.user_agent = "LuaRocks/"..cfg.program_version.." "..cfg.arch + + cfg.config_files = { + project = cfg.project_dir and { + file = project_config_file, + found = not not project_config_ok, + }, + system = { + file = sys_config_file, + found = not not sys_config_ok, + }, + user = { + file = home_config_file, + found = not not home_config_ok, + }, + nearest = project_config_ok + and project_config_file + or (home_config_ok + and home_config_file + or sys_config_file), + } + + cfg.cache = {} + + ---------------------------------------- + -- Attributes of cfg are set. + -- Let's add some methods. + ---------------------------------------- + + do + local function make_paths_from_tree(tree) + local lua_path, lib_path, bin_path + if type(tree) == "string" then + lua_path = tree..cfg.lua_modules_path + lib_path = tree..cfg.lib_modules_path + bin_path = tree.."/bin" + else + lua_path = tree.lua_dir or tree.root..cfg.lua_modules_path + lib_path = tree.lib_dir or tree.root..cfg.lib_modules_path + bin_path = tree.bin_dir or tree.root.."/bin" + end + return lua_path, lib_path, bin_path + end + + function cfg.package_paths(current) + local new_path, new_cpath, new_bin = {}, {}, {} + local function add_tree_to_paths(tree) + local lua_path, lib_path, bin_path = make_paths_from_tree(tree) + table.insert(new_path, lua_path.."/?.lua") + table.insert(new_path, lua_path.."/?/init.lua") + table.insert(new_cpath, lib_path.."/?."..cfg.lib_extension) + table.insert(new_bin, bin_path) + end + if current then + add_tree_to_paths(current) + end + for _,tree in ipairs(cfg.rocks_trees) do + add_tree_to_paths(tree) + end + return table.concat(new_path, ";"), table.concat(new_cpath, ";"), table.concat(new_bin, cfg.export_path_separator) + end + end + + function cfg.init_package_paths() + local lr_path, lr_cpath, lr_bin = cfg.package_paths() + package.path = util.cleanup_path(package.path .. ";" .. lr_path, ";", cfg.lua_version, true) + package.cpath = util.cleanup_path(package.cpath .. ";" .. lr_cpath, ";", cfg.lua_version, true) + end + + --- Check if platform was detected + -- @param name string: The platform name to check. + -- @return boolean: true if LuaRocks is currently running on queried platform. + function cfg.is_platform(name) + assert(type(name) == "string") + return platforms[name] + end + + -- @param direction (optional) "least-specific-first" (default) or "most-specific-first" + function cfg.each_platform(direction) + direction = direction or "least-specific-first" + local i, delta + if direction == "least-specific-first" then + i = 0 + delta = 1 + else + i = #platform_order + 1 + delta = -1 + end + return function() + local p + repeat + i = i + delta + p = platform_order[i] + until (not p) or platforms[p] + return p + end + end + + function cfg.print_platforms() + local platform_keys = {} + for k,_ in pairs(platforms) do + table.insert(platform_keys, k) + end + table.sort(platform_keys) + return table.concat(platform_keys, ", ") + end + + cfg.initialized = true + return exit_ok, exit_err, exit_what +end + +return cfg diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/core/dir.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/core/dir.lua new file mode 100644 index 000000000..46dbeafd0 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/core/dir.lua @@ -0,0 +1,91 @@ + +local dir = {} + +local require = nil +-------------------------------------------------------------------------------- + +local function unquote(c) + local first, last = c:sub(1,1), c:sub(-1) + if (first == '"' and last == '"') or + (first == "'" and last == "'") then + return c:sub(2,-2) + end + return c +end + +--- Describe a path in a cross-platform way. +-- Use this function to avoid platform-specific directory +-- separators in other modules. Removes trailing slashes from +-- each component given, to avoid repeated separators. +-- Separators inside strings are kept, to handle URLs containing +-- protocols. +-- @param ... strings representing directories +-- @return string: a string with a platform-specific representation +-- of the path. +function dir.path(...) + local t = {...} + while t[1] == "" do + table.remove(t, 1) + end + for i, c in ipairs(t) do + t[i] = unquote(c) + end + return (table.concat(t, "/"):gsub("([^:])/+", "%1/"):gsub("^/+", "/"):gsub("/*$", "")) +end + +--- Split protocol and path from an URL or local pathname. +-- URLs should be in the "protocol://path" format. +-- For local pathnames, "file" is returned as the protocol. +-- @param url string: an URL or a local pathname. +-- @return string, string: the protocol, and the pathname without the protocol. +function dir.split_url(url) + assert(type(url) == "string") + + url = unquote(url) + local protocol, pathname = url:match("^([^:]*)://(.*)") + if not protocol then + protocol = "file" + pathname = url + end + return protocol, pathname +end + +--- Normalize a url or local path. +-- URLs should be in the "protocol://path" format. System independent +-- forward slashes are used, removing trailing and double slashes +-- @param url string: an URL or a local pathname. +-- @return string: Normalized result. +function dir.normalize(name) + local protocol, pathname = dir.split_url(name) + pathname = pathname:gsub("\\", "/"):gsub("(.)/*$", "%1"):gsub("//", "/") + local pieces = {} + local drive = "" + if pathname:match("^.:") then + drive, pathname = pathname:match("^(.:)(.*)$") + end + pathname = pathname .. "/" + for piece in pathname:gmatch("(.-)/") do + if piece == ".." then + local prev = pieces[#pieces] + if not prev or prev == ".." then + table.insert(pieces, "..") + elseif prev ~= "" then + table.remove(pieces) + end + elseif piece ~= "." then + table.insert(pieces, piece) + end + end + if #pieces == 0 then + pathname = drive .. "." + elseif #pieces == 1 and pieces[1] == "" then + pathname = drive .. "/" + else + pathname = drive .. table.concat(pieces, "/") + end + if protocol ~= "file" then pathname = protocol .."://"..pathname end + return pathname +end + +return dir + diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/core/manif.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/core/manif.lua new file mode 100644 index 000000000..3925f6365 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/core/manif.lua @@ -0,0 +1,114 @@ + +--- Core functions for querying manifest files. +local manif = {} + +local persist = require("luarocks.core.persist") +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.core.dir") +local util = require("luarocks.core.util") +local vers = require("luarocks.core.vers") +local path = require("luarocks.core.path") +local require = nil +-------------------------------------------------------------------------------- + +-- Table with repository identifiers as keys and tables mapping +-- Lua versions to cached loaded manifests as values. +local manifest_cache = {} + +--- Cache a loaded manifest. +-- @param repo_url string: The repository identifier. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @param manifest table: the manifest to be cached. +function manif.cache_manifest(repo_url, lua_version, manifest) + lua_version = lua_version or cfg.lua_version + manifest_cache[repo_url] = manifest_cache[repo_url] or {} + manifest_cache[repo_url][lua_version] = manifest +end + +--- Attempt to get cached loaded manifest. +-- @param repo_url string: The repository identifier. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @return table or nil: loaded manifest or nil if cache is empty. +function manif.get_cached_manifest(repo_url, lua_version) + lua_version = lua_version or cfg.lua_version + return manifest_cache[repo_url] and manifest_cache[repo_url][lua_version] +end + +--- Back-end function that actually loads the manifest +-- and stores it in the manifest cache. +-- @param file string: The local filename of the manifest file. +-- @param repo_url string: The repository identifier. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @return table or (nil, string, string): the manifest or nil, +-- error message and error code ("open", "load", "run"). +function manif.manifest_loader(file, repo_url, lua_version) + local manifest, err, errcode = persist.load_into_table(file) + if not manifest then + return nil, "Failed loading manifest for "..repo_url..": "..err, errcode + end + manif.cache_manifest(repo_url, lua_version, manifest) + return manifest, err, errcode +end + +--- Load a local manifest describing a repository. +-- This is used by the luarocks.loader only. +-- @param repo_url string: URL or pathname for the repository. +-- @return table or (nil, string, string): A table representing the manifest, +-- or nil followed by an error message and an error code, see manifest_loader. +function manif.fast_load_local_manifest(repo_url) + assert(type(repo_url) == "string") + + local cached_manifest = manif.get_cached_manifest(repo_url) + if cached_manifest then + return cached_manifest + end + + local pathname = dir.path(repo_url, "manifest") + return manif.manifest_loader(pathname, repo_url, nil, true) +end + +function manif.load_rocks_tree_manifests(deps_mode) + local trees = {} + path.map_trees(deps_mode, function(tree) + local manifest, err = manif.fast_load_local_manifest(path.rocks_dir(tree)) + if manifest then + table.insert(trees, {tree=tree, manifest=manifest}) + end + end) + return trees +end + +function manif.scan_dependencies(name, version, tree_manifests, dest) + if dest[name] then + return + end + dest[name] = version + + for _, tree in ipairs(tree_manifests) do + local manifest = tree.manifest + + local pkgdeps + if manifest.dependencies and manifest.dependencies[name] then + pkgdeps = manifest.dependencies[name][version] + end + if pkgdeps then + for _, dep in ipairs(pkgdeps) do + local pkg, constraints = dep.name, dep.constraints + + for _, t in ipairs(tree_manifests) do + local entries = t.manifest.repository[pkg] + if entries then + for ver, _ in util.sortedpairs(entries, vers.compare_versions) do + if (not constraints) or vers.match_constraints(vers.parse_version(ver), constraints) then + manif.scan_dependencies(pkg, ver, tree_manifests, dest) + end + end + end + end + end + return + end + end +end + +return manif diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/core/path.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/core/path.lua new file mode 100644 index 000000000..b354a41a6 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/core/path.lua @@ -0,0 +1,155 @@ + +--- Core LuaRocks-specific path handling functions. +local path = {} + +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.core.dir") +local require = nil +-------------------------------------------------------------------------------- + +function path.rocks_dir(tree) + if tree == nil then + tree = cfg.root_dir + end + if type(tree) == "string" then + return dir.path(tree, cfg.rocks_subdir) + end + assert(type(tree) == "table") + return tree.rocks_dir or dir.path(tree.root, cfg.rocks_subdir) +end + +--- Produce a versioned version of a filename. +-- @param file string: filename (must start with prefix) +-- @param prefix string: Path prefix for file +-- @param name string: Rock name +-- @param version string: Rock version +-- @return string: a pathname with the same directory parts and a versioned basename. +function path.versioned_name(file, prefix, name, version) + assert(type(file) == "string") + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local rest = file:sub(#prefix+1):gsub("^/*", "") + local name_version = (name.."_"..version):gsub("%-", "_"):gsub("%.", "_") + return dir.path(prefix, name_version.."-"..rest) +end + +--- Convert a pathname to a module identifier. +-- In Unix, for example, a path "foo/bar/baz.lua" is converted to +-- "foo.bar.baz"; "bla/init.lua" returns "bla.init"; "foo.so" returns "foo". +-- @param file string: Pathname of module +-- @return string: The module identifier, or nil if given path is +-- not a conformant module path (the function does not check if the +-- path actually exists). +function path.path_to_module(file) + assert(type(file) == "string") + + local exts = {} + local paths = package.path .. ";" .. package.cpath + for entry in paths:gmatch("[^;]+") do + local ext = entry:match("%.([a-z]+)$") + if ext then + exts[ext] = true + end + end + + local name + for ext, _ in pairs(exts) do + name = file:match("(.*)%." .. ext .. "$") + if name then + name = name:gsub("[/\\]", ".") + break + end + end + + if not name then name = file end + + -- remove any beginning and trailing slashes-converted-to-dots + name = name:gsub("^%.+", ""):gsub("%.+$", "") + + return name +end + +function path.deploy_lua_dir(tree) + if type(tree) == "string" then + return dir.path(tree, cfg.lua_modules_path) + else + assert(type(tree) == "table") + return tree.lua_dir or dir.path(tree.root, cfg.lua_modules_path) + end +end + +function path.deploy_lib_dir(tree) + if type(tree) == "string" then + return dir.path(tree, cfg.lib_modules_path) + else + assert(type(tree) == "table") + return tree.lib_dir or dir.path(tree.root, cfg.lib_modules_path) + end +end + +local is_src_extension = { [".lua"] = true, [".tl"] = true, [".tld"] = true, [".moon"] = true } + +--- Return the pathname of the file that would be loaded for a module, indexed. +-- @param file_name string: module file name as in manifest (eg. "socket/core.so") +-- @param name string: name of the package (eg. "luasocket") +-- @param version string: version number (eg. "2.0.2-1") +-- @param tree string: repository path (eg. "/usr/local") +-- @param i number: the index, 1 if version is the current default, > 1 otherwise. +-- This is done this way for use by select_module in luarocks.loader. +-- @return string: filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so") +function path.which_i(file_name, name, version, tree, i) + local deploy_dir + local extension = file_name:match("%.[a-z]+$") + if is_src_extension[extension] then + deploy_dir = path.deploy_lua_dir(tree) + file_name = dir.path(deploy_dir, file_name) + else + deploy_dir = path.deploy_lib_dir(tree) + file_name = dir.path(deploy_dir, file_name) + end + if i > 1 then + file_name = path.versioned_name(file_name, deploy_dir, name, version) + end + return file_name +end + +function path.rocks_tree_to_string(tree) + if type(tree) == "string" then + return tree + else + assert(type(tree) == "table") + return tree.root + end +end + +--- Apply a given function to the active rocks trees based on chosen dependency mode. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for no trees (this function becomes a nop). +-- @param fn function: function to be applied, with the tree dir (string) as the first +-- argument and the remaining varargs of map_trees as the following arguments. +-- @return a table with all results of invocations of fn collected. +function path.map_trees(deps_mode, fn, ...) + local result = {} + local current = cfg.root_dir or cfg.rocks_trees[1] + if deps_mode == "one" then + table.insert(result, (fn(current, ...)) or 0) + else + local use = false + if deps_mode == "all" then + use = true + end + for _, tree in ipairs(cfg.rocks_trees or {}) do + if dir.normalize(path.rocks_tree_to_string(tree)) == dir.normalize(path.rocks_tree_to_string(current)) then + use = true + end + if use then + table.insert(result, (fn(tree, ...)) or 0) + end + end + end + return result +end + +return path diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/core/persist.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/core/persist.lua new file mode 100644 index 000000000..57e7b5d42 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/core/persist.lua @@ -0,0 +1,81 @@ + +local persist = {} + +local require = nil +-------------------------------------------------------------------------------- + +--- Load and run a Lua file in an environment. +-- @param filename string: the name of the file. +-- @param env table: the environment table. +-- @return (true, any) or (nil, string, string): true and the return value +-- of the file, or nil, an error message and an error code ("open", "load" +-- or "run") in case of errors. +function persist.run_file(filename, env) + local fd, err = io.open(filename) + if not fd then + return nil, err, "open" + end + local str, err = fd:read("*a") + fd:close() + if not str then + return nil, err, "open" + end + str = str:gsub("^#![^\n]*\n", "") + local chunk, ran + if _VERSION == "Lua 5.1" then -- Lua 5.1 + chunk, err = loadstring(str, filename) + if chunk then + setfenv(chunk, env) + ran, err = pcall(chunk) + end + else -- Lua 5.2 + chunk, err = load(str, filename, "t", env) + if chunk then + ran, err = pcall(chunk) + end + end + if not chunk then + return nil, "Error loading file: "..err, "load" + end + if not ran then + return nil, "Error running file: "..err, "run" + end + return true, err +end + +--- Load a Lua file containing assignments, storing them in a table. +-- The global environment is not propagated to the loaded file. +-- @param filename string: the name of the file. +-- @param tbl table or nil: if given, this table is used to store +-- loaded values. +-- @return (table, table) or (nil, string, string): a table with the file's assignments +-- as fields and set of undefined globals accessed in file, +-- or nil, an error message and an error code ("open"; couldn't open the file, +-- "load"; compile-time error, or "run"; run-time error) +-- in case of errors. +function persist.load_into_table(filename, tbl) + assert(type(filename) == "string") + assert(type(tbl) == "table" or not tbl) + + local result = tbl or {} + local globals = {} + local globals_mt = { + __index = function(t, k) + globals[k] = true + end + } + local save_mt = getmetatable(result) + setmetatable(result, globals_mt) + + local ok, err, errcode = persist.run_file(filename, result) + + setmetatable(result, save_mt) + + if not ok then + return nil, err, errcode + end + return result, globals +end + +return persist + diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/core/sysdetect.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/core/sysdetect.lua new file mode 100644 index 000000000..f4af1eb48 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/core/sysdetect.lua @@ -0,0 +1,420 @@ +-- Detect the operating system and architecture without forking a subprocess. +-- +-- We are not going for exhaustive list of every historical system here, +-- but aiming to cover every platform where LuaRocks is known to run. +-- If your system is not detected, patches are welcome! + +local sysdetect = {} + +local function hex(s) + return s:gsub("$(..)", function(x) + return string.char(tonumber(x, 16)) + end) +end + +local function read_int8(fd) + if io.type(fd) == "closed file" then + return nil + end + local s = fd:read(1) + if not s then + fd:close() + return nil + end + return s:byte() +end + +local LITTLE = 1 +-- local BIG = 2 + +local function bytes2number(s, endian) + local r = 0 + if endian == LITTLE then + for i = #s, 1, -1 do + r = r*256 + s:byte(i,i) + end + else + for i = 1, #s do + r = r*256 + s:byte(i,i) + end + end + return r +end + +local function read(fd, bytes, endian) + if io.type(fd) == "closed file" then + return nil + end + local s = fd:read(bytes) + if not s + then fd:close() + return nil + end + return bytes2number(s, endian) +end + +local function read_int32le(fd) + return read(fd, 4, LITTLE) +end + +-------------------------------------------------------------------------------- +-- @section ELF +-------------------------------------------------------------------------------- + +local e_osabi = { + [0x00] = "sysv", + [0x01] = "hpux", + [0x02] = "netbsd", + [0x03] = "linux", + [0x04] = "hurd", + [0x06] = "solaris", + [0x07] = "aix", + [0x08] = "irix", + [0x09] = "freebsd", + [0x0c] = "openbsd", +} + +local e_machines = { + [0x02] = "sparc", + [0x03] = "x86", + [0x08] = "mips", + [0x0f] = "hppa", + [0x12] = "sparcv8", + [0x14] = "ppc", + [0x15] = "ppc64", + [0x16] = "s390", + [0x28] = "arm", + [0x2a] = "superh", + [0x2b] = "sparcv9", + [0x32] = "ia_64", + [0x3E] = "x86_64", + [0xB6] = "alpha", + [0xB7] = "aarch64", + [0xF3] = "riscv64", + [0x9026] = "alpha", +} + +local SHT_NOTE = 7 + +local function read_elf_section_headers(fd, hdr) + local endian = hdr.endian + local word = hdr.word + + local strtab_offset + local sections = {} + for i = 0, hdr.e_shnum - 1 do + fd:seek("set", hdr.e_shoff + (i * hdr.e_shentsize)) + local section = {} + section.sh_name_off = read(fd, 4, endian) + section.sh_type = read(fd, 4, endian) + section.sh_flags = read(fd, word, endian) + section.sh_addr = read(fd, word, endian) + section.sh_offset = read(fd, word, endian) + section.sh_size = read(fd, word, endian) + section.sh_link = read(fd, 4, endian) + section.sh_info = read(fd, 4, endian) + if section.sh_type == SHT_NOTE then + fd:seek("set", section.sh_offset) + section.namesz = read(fd, 4, endian) + section.descsz = read(fd, 4, endian) + section.type = read(fd, 4, endian) + section.namedata = fd:read(section.namesz):gsub("%z.*", "") + section.descdata = fd:read(section.descsz) + elseif i == hdr.e_shstrndx then + strtab_offset = section.sh_offset + end + table.insert(sections, section) + end + if strtab_offset then + for _, section in ipairs(sections) do + fd:seek("set", strtab_offset + section.sh_name_off) + section.name = fd:read(32):gsub("%z.*", "") + sections[section.name] = section + end + end + return sections +end + +local function detect_elf_system(fd, hdr, sections) + local system = e_osabi[hdr.osabi] + local endian = hdr.endian + + if system == "sysv" then + local abitag = sections[".note.ABI-tag"] + if abitag then + if abitag.namedata == "GNU" and abitag.type == 1 + and abitag.descdata:sub(0, 4) == "\0\0\0\0" then + return "linux" + end + elseif sections[".SUNW_version"] + or sections[".SUNW_signature"] then + return "solaris" + elseif sections[".note.netbsd.ident"] then + return "netbsd" + elseif sections[".note.openbsd.ident"] then + return "openbsd" + elseif sections[".note.tag"] and + sections[".note.tag"].namedata == "DragonFly" then + return "dragonfly" + end + + local gnu_version_r = sections[".gnu.version_r"] + if gnu_version_r then + + local dynstr = sections[".dynstr"].sh_offset + + local idx = 0 + for _ = 0, gnu_version_r.sh_info - 1 do + fd:seek("set", gnu_version_r.sh_offset + idx) + assert(read(fd, 2, endian)) -- vn_version + local vn_cnt = read(fd, 2, endian) + local vn_file = read(fd, 4, endian) + local vn_next = read(fd, 2, endian) + + fd:seek("set", dynstr + vn_file) + local libname = fd:read(64):gsub("%z.*", "") + + if hdr.e_type == 0x03 and libname == "libroot.so" then + return "haiku" + elseif libname:match("linux") then + return "linux" + end + + idx = idx + (vn_next * (vn_cnt + 1)) + end + end + + local procfile = io.open("/proc/sys/kernel/ostype") + if procfile then + local version = procfile:read(6) + procfile:close() + if version == "Linux\n" then + return "linux" + end + end + end + + return system +end + +local function read_elf_header(fd) + local hdr = {} + + hdr.bits = read_int8(fd) + hdr.endian = read_int8(fd) + hdr.elf_version = read_int8(fd) + if hdr.elf_version ~= 1 then + return nil + end + hdr.osabi = read_int8(fd) + if not hdr.osabi then + return nil + end + + local endian = hdr.endian + fd:seek("set", 0x10) + hdr.e_type = read(fd, 2, endian) + local machine = read(fd, 2, endian) + local processor = e_machines[machine] or "unknown" + if endian == 1 and processor == "ppc64" then + processor = "ppc64le" + end + + local elfversion = read(fd, 4, endian) + if elfversion ~= 1 then + return nil + end + + local word = (hdr.bits == 1) and 4 or 8 + hdr.word = word + + hdr.e_entry = read(fd, word, endian) + hdr.e_phoff = read(fd, word, endian) + hdr.e_shoff = read(fd, word, endian) + hdr.e_flags = read(fd, 4, endian) + hdr.e_ehsize = read(fd, 2, endian) + hdr.e_phentsize = read(fd, 2, endian) + hdr.e_phnum = read(fd, 2, endian) + hdr.e_shentsize = read(fd, 2, endian) + hdr.e_shnum = read(fd, 2, endian) + hdr.e_shstrndx = read(fd, 2, endian) + + return hdr, processor +end + +local function detect_elf(fd) + local hdr, processor = read_elf_header(fd) + if not hdr then + return nil + end + local sections = read_elf_section_headers(fd, hdr) + local system = detect_elf_system(fd, hdr, sections) + return system, processor +end + +-------------------------------------------------------------------------------- +-- @section Mach Objects (Apple) +-------------------------------------------------------------------------------- + +local mach_l64 = { + [7] = "x86_64", + [12] = "aarch64", +} + +local mach_b64 = { + [0] = "ppc64", +} + +local mach_l32 = { + [7] = "x86", + [12] = "arm", +} + +local mach_b32 = { + [0] = "ppc", +} + +local function detect_mach(magic, fd) + if not magic then + return nil + end + + if magic == hex("$CA$FE$BA$BE") then + -- fat binary, go for the first one + fd:seek("set", 0x12) + local offs = read_int8(fd) + if not offs then + return nil + end + fd:seek("set", offs * 256) + magic = fd:read(4) + return detect_mach(magic, fd) + end + + local cputype = read_int8(fd) + + if magic == hex("$CF$FA$ED$FE") then + return "macosx", mach_l64[cputype] or "unknown" + elseif magic == hex("$FE$ED$CF$FA") then + return "macosx", mach_b64[cputype] or "unknown" + elseif magic == hex("$CE$FA$ED$FE") then + return "macosx", mach_l32[cputype] or "unknown" + elseif magic == hex("$FE$ED$FA$CE") then + return "macosx", mach_b32[cputype] or "unknown" + end +end + +-------------------------------------------------------------------------------- +-- @section PE (Windows) +-------------------------------------------------------------------------------- + +local pe_machine = { + [0x8664] = "x86_64", + [0x01c0] = "arm", + [0x01c4] = "armv7l", + [0xaa64] = "arm64", + [0x014c] = "x86", +} + +local function detect_pe(fd) + fd:seek("set", 60) -- position of PE header position + local peoffset = read_int32le(fd) -- read position of PE header + if not peoffset then + return nil + end + local system = "windows" + fd:seek("set", peoffset + 4) -- move to position of Machine section + local machine = read(fd, 2, LITTLE) + local processor = pe_machine[machine] + + local rdata_pos = fd:read(736):match(".rdata%z%z............(....)") + if rdata_pos then + rdata_pos = bytes2number(rdata_pos, LITTLE) + fd:seek("set", rdata_pos) + local data = fd:read(512) + if data:match("cygwin") or data:match("cyggcc") then + system = "cygwin" + end + end + + return system, processor or "unknown" +end + +-------------------------------------------------------------------------------- +-- @section API +-------------------------------------------------------------------------------- + +function sysdetect.detect_file(file) + assert(type(file) == "string") + local fd = io.open(file, "rb") + if not fd then + return nil + end + local magic = fd:read(4) + if magic == hex("$7FELF") then + return detect_elf(fd) + end + if magic == hex("MZ$90$00") then + return detect_pe(fd) + end + return detect_mach(magic, fd) +end + +local cache_system +local cache_processor + +function sysdetect.detect(input_file) + local dirsep = package.config:sub(1,1) + local files + + local arg = rawget(_G, 'arg') or {} + if input_file then + files = { input_file } + else + if cache_system then + return cache_system, cache_processor + end + + local PATHsep + local interp = arg and arg[-1] + if dirsep == "/" then + -- Unix + files = { + "/bin/sh", -- Unix: well-known POSIX path + "/proc/self/exe", -- Linux: this should always have a working binary + } + PATHsep = ":" + else + -- Windows + local systemroot = os.getenv("SystemRoot") + files = { + systemroot .. "\\system32\\notepad.exe", -- well-known Windows path + systemroot .. "\\explorer.exe", -- well-known Windows path + } + if interp and not interp:lower():match("exe$") then + interp = interp .. ".exe" + end + PATHsep = ";" + end + if interp then + if interp:match(dirsep) then + -- interpreter path is absolute + table.insert(files, interp) + else + for d in (os.getenv("PATH") or ""):gmatch("[^"..PATHsep.."]+") do + table.insert(files, d .. dirsep .. interp) + end + end + end + end + for _, f in ipairs(files) do + local system, processor = sysdetect.detect_file(f) + if system then + cache_system = system + cache_processor = processor + return system, processor + end + end +end + +return sysdetect diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/core/util.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/core/util.lua new file mode 100644 index 000000000..26e783698 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/core/util.lua @@ -0,0 +1,318 @@ + +local util = {} + +local require = nil +-------------------------------------------------------------------------------- + +--- Run a process and read a its output. +-- Equivalent to io.popen(cmd):read("*l"), except that it +-- closes the fd right away. +-- @param cmd string: The command to execute +-- @param spec string: "*l" by default, to read a single line. +-- May be used to read more, passing, for instance, "*a". +-- @return string: the output of the program. +function util.popen_read(cmd, spec) + local dir_sep = package.config:sub(1, 1) + local tmpfile = (dir_sep == "\\") + and (os.getenv("TMP") .. "/luarocks-" .. tostring(math.floor(math.random() * 10000))) + or os.tmpname() + os.execute(cmd .. " > " .. tmpfile) + local fd = io.open(tmpfile, "rb") + if not fd then + os.remove(tmpfile) + return "" + end + local out = fd:read(spec or "*l") + fd:close() + os.remove(tmpfile) + return out or "" +end + +--- +-- Formats tables with cycles recursively to any depth. +-- References to other tables are shown as values. +-- Self references are indicated. +-- The string returned is "Lua code", which can be processed +-- (in the case in which indent is composed by spaces or "--"). +-- Userdata and function keys and values are shown as strings, +-- which logically are exactly not equivalent to the original code. +-- This routine can serve for pretty formating tables with +-- proper indentations, apart from printing them: +-- io.write(table.show(t, "t")) -- a typical use +-- Written by Julio Manuel Fernandez-Diaz, +-- Heavily based on "Saving tables with cycles", PIL2, p. 113. +-- @param t table: is the table. +-- @param tname string: is the name of the table (optional) +-- @param top_indent string: is a first indentation (optional). +-- @return string: the pretty-printed table +function util.show_table(t, tname, top_indent) + local cart -- a container + local autoref -- for self references + + local function is_empty_table(tbl) return next(tbl) == nil end + + local function basic_serialize(o) + local so = tostring(o) + if type(o) == "function" then + local info = debug and debug.getinfo(o, "S") + if not info then + return ("%q"):format(so) + end + -- info.name is nil because o is not a calling level + if info.what == "C" then + return ("%q"):format(so .. ", C function") + else + -- the information is defined through lines + return ("%q"):format(so .. ", defined in (" .. info.linedefined .. "-" .. info.lastlinedefined .. ")" .. info.source) + end + elseif type(o) == "number" then + return so + else + return ("%q"):format(so) + end + end + + local function add_to_cart(value, name, indent, saved, field) + indent = indent or "" + saved = saved or {} + field = field or name + + cart = cart .. indent .. field + + if type(value) ~= "table" then + cart = cart .. " = " .. basic_serialize(value) .. ";\n" + else + if saved[value] then + cart = cart .. " = {}; -- " .. saved[value] .. " (self reference)\n" + autoref = autoref .. name .. " = " .. saved[value] .. ";\n" + else + saved[value] = name + if is_empty_table(value) then + cart = cart .. " = {};\n" + else + cart = cart .. " = {\n" + for k, v in pairs(value) do + k = basic_serialize(k) + local fname = ("%s[%s]"):format(name, k) + field = ("[%s]"):format(k) + -- three spaces between levels + add_to_cart(v, fname, indent .. " ", saved, field) + end + cart = cart .. indent .. "};\n" + end + end + end + end + + tname = tname or "__unnamed__" + if type(t) ~= "table" then + return tname .. " = " .. basic_serialize(t) + end + cart, autoref = "", "" + add_to_cart(t, tname, top_indent) + return cart .. autoref +end + +--- Merges contents of src on top of dst's contents +-- (i.e. if an key from src already exists in dst, replace it). +-- @param dst Destination table, which will receive src's contents. +-- @param src Table which provides new contents to dst. +function util.deep_merge(dst, src) + for k, v in pairs(src) do + if type(v) == "table" then + if dst[k] == nil then + dst[k] = {} + end + if type(dst[k]) == "table" then + util.deep_merge(dst[k], v) + else + dst[k] = v + end + else + dst[k] = v + end + end +end + +--- Merges contents of src below those of dst's contents +-- (i.e. if an key from src already exists in dst, do not replace it). +-- @param dst Destination table, which will receive src's contents. +-- @param src Table which provides new contents to dst. +function util.deep_merge_under(dst, src) + for k, v in pairs(src) do + if type(v) == "table" then + if dst[k] == nil then + dst[k] = {} + end + if type(dst[k]) == "table" then + util.deep_merge_under(dst[k], v) + end + elseif dst[k] == nil then + dst[k] = v + end + end +end + +--- Clean up a path-style string ($PATH, $LUA_PATH/package.path, etc.), +-- removing repeated entries and making sure only the relevant +-- Lua version is used. +-- Example: given ("a;b;c;a;b;d", ";"), returns "a;b;c;d". +-- @param list string: A path string (from $PATH or package.path) +-- @param sep string: The separator +-- @param lua_version (optional) string: The Lua version to use. +-- @param keep_first (optional) if true, keep first occurrence in case +-- of duplicates; otherwise keep last occurrence. The default is false. +function util.cleanup_path(list, sep, lua_version, keep_first) + assert(type(list) == "string") + assert(type(sep) == "string") + local parts = util.split_string(list, sep) + local final, entries = {}, {} + local start, stop, step + + if keep_first then + start, stop, step = 1, #parts, 1 + else + start, stop, step = #parts, 1, -1 + end + + for i = start, stop, step do + local part = parts[i]:gsub("//", "/") + if lua_version then + part = part:gsub("/lua/([%d.]+)/", function(part_version) + if part_version:sub(1, #lua_version) ~= lua_version then + return "/lua/"..lua_version.."/" + end + end) + end + if not entries[part] then + local at = keep_first and #final+1 or 1 + table.insert(final, at, part) + entries[part] = true + end + end + + return table.concat(final, sep) +end + +-- from http://lua-users.org/wiki/SplitJoin +-- by Philippe Lhoste +function util.split_string(str, delim, maxNb) + -- Eliminate bad cases... + if string.find(str, delim) == nil then + return { str } + end + if maxNb == nil or maxNb < 1 then + maxNb = 0 -- No limit + end + local result = {} + local pat = "(.-)" .. delim .. "()" + local nb = 0 + local lastPos + for part, pos in string.gmatch(str, pat) do + nb = nb + 1 + result[nb] = part + lastPos = pos + if nb == maxNb then break end + end + -- Handle the last field + if nb ~= maxNb then + result[nb + 1] = string.sub(str, lastPos) + end + return result +end + +--- Return an array of keys of a table. +-- @param tbl table: The input table. +-- @return table: The array of keys. +function util.keys(tbl) + local ks = {} + for k,_ in pairs(tbl) do + table.insert(ks, k) + end + return ks +end + +--- Print a line to standard error +function util.printerr(...) + io.stderr:write(table.concat({...},"\t")) + io.stderr:write("\n") +end + +--- Display a warning message. +-- @param msg string: the warning message +function util.warning(msg) + util.printerr("Warning: "..msg) +end + +--- Simple sort function used as a default for util.sortedpairs. +local function default_sort(a, b) + local ta = type(a) + local tb = type(b) + if ta == "number" and tb == "number" then + return a < b + elseif ta == "number" then + return true + elseif tb == "number" then + return false + else + return tostring(a) < tostring(b) + end +end + +--- A table iterator generator that returns elements sorted by key, +-- to be used in "for" loops. +-- @param tbl table: The table to be iterated. +-- @param sort_function function or table or nil: An optional comparison function +-- to be used by table.sort when sorting keys, or an array listing an explicit order +-- for keys. If a value itself is an array, it is taken so that the first element +-- is a string representing the field name, and the second element is a priority table +-- for that key, which is returned by the iterator as the third value after the key +-- and the value. +-- @return function: the iterator function. +function util.sortedpairs(tbl, sort_function) + sort_function = sort_function or default_sort + local keys = util.keys(tbl) + local sub_orders = {} + + if type(sort_function) == "function" then + table.sort(keys, sort_function) + else + local order = sort_function + local ordered_keys = {} + local all_keys = keys + keys = {} + + for _, order_entry in ipairs(order) do + local key, sub_order + if type(order_entry) == "table" then + key = order_entry[1] + sub_order = order_entry[2] + else + key = order_entry + end + + if tbl[key] then + ordered_keys[key] = true + sub_orders[key] = sub_order + table.insert(keys, key) + end + end + + table.sort(all_keys, default_sort) + for _, key in ipairs(all_keys) do + if not ordered_keys[key] then + table.insert(keys, key) + end + end + end + + local i = 1 + return function() + local key = keys[i] + i = i + 1 + return key, tbl[key], sub_orders[key] + end +end + +return util + diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/core/vers.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/core/vers.lua new file mode 100644 index 000000000..8e617984a --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/core/vers.lua @@ -0,0 +1,207 @@ + +local vers = {} + +local util = require("luarocks.core.util") +local require = nil +-------------------------------------------------------------------------------- + +local deltas = { + dev = 120000000, + scm = 110000000, + cvs = 100000000, + rc = -1000, + pre = -10000, + beta = -100000, + alpha = -1000000 +} + +local version_mt = { + --- Equality comparison for versions. + -- All version numbers must be equal. + -- If both versions have revision numbers, they must be equal; + -- otherwise the revision number is ignored. + -- @param v1 table: version table to compare. + -- @param v2 table: version table to compare. + -- @return boolean: true if they are considered equivalent. + __eq = function(v1, v2) + if #v1 ~= #v2 then + return false + end + for i = 1, #v1 do + if v1[i] ~= v2[i] then + return false + end + end + if v1.revision and v2.revision then + return (v1.revision == v2.revision) + end + return true + end, + --- Size comparison for versions. + -- All version numbers are compared. + -- If both versions have revision numbers, they are compared; + -- otherwise the revision number is ignored. + -- @param v1 table: version table to compare. + -- @param v2 table: version table to compare. + -- @return boolean: true if v1 is considered lower than v2. + __lt = function(v1, v2) + for i = 1, math.max(#v1, #v2) do + local v1i, v2i = v1[i] or 0, v2[i] or 0 + if v1i ~= v2i then + return (v1i < v2i) + end + end + if v1.revision and v2.revision then + return (v1.revision < v2.revision) + end + return false + end, + -- @param v1 table: version table to compare. + -- @param v2 table: version table to compare. + -- @return boolean: true if v1 is considered lower than or equal to v2. + __le = function(v1, v2) + return not (v2 < v1) -- luacheck: ignore + end, + --- Return version as a string. + -- @param v The version table. + -- @return The string representation. + __tostring = function(v) + return v.string + end, +} + +local version_cache = {} +setmetatable(version_cache, { + __mode = "kv" +}) + +--- Parse a version string, converting to table format. +-- A version table contains all components of the version string +-- converted to numeric format, stored in the array part of the table. +-- If the version contains a revision, it is stored numerically +-- in the 'revision' field. The original string representation of +-- the string is preserved in the 'string' field. +-- Returned version tables use a metatable +-- allowing later comparison through relational operators. +-- @param vstring string: A version number in string format. +-- @return table or nil: A version table or nil +-- if the input string contains invalid characters. +function vers.parse_version(vstring) + if not vstring then return nil end + assert(type(vstring) == "string") + + local cached = version_cache[vstring] + if cached then + return cached + end + + local version = {} + local i = 1 + + local function add_token(number) + version[i] = version[i] and version[i] + number/100000 or number + i = i + 1 + end + + -- trim leading and trailing spaces + local v = vstring:match("^%s*(.*)%s*$") + version.string = v + -- store revision separately if any + local main, revision = v:match("(.*)%-(%d+)$") + if revision then + v = main + version.revision = tonumber(revision) + end + while #v > 0 do + -- extract a number + local token, rest = v:match("^(%d+)[%.%-%_]*(.*)") + if token then + add_token(tonumber(token)) + else + -- extract a word + token, rest = v:match("^(%a+)[%.%-%_]*(.*)") + if not token then + util.warning("version number '"..v.."' could not be parsed.") + version[i] = 0 + break + end + version[i] = deltas[token] or (token:byte() / 1000) + end + v = rest + end + setmetatable(version, version_mt) + version_cache[vstring] = version + return version +end + +--- Utility function to compare version numbers given as strings. +-- @param a string: one version. +-- @param b string: another version. +-- @return boolean: True if a > b. +function vers.compare_versions(a, b) + if a == b then + return false + end + return vers.parse_version(a) > vers.parse_version(b) +end + +--- A more lenient check for equivalence between versions. +-- This returns true if the requested components of a version +-- match and ignore the ones that were not given. For example, +-- when requesting "2", then "2", "2.1", "2.3.5-9"... all match. +-- When requesting "2.1", then "2.1", "2.1.3" match, but "2.2" +-- doesn't. +-- @param version string or table: Version to be tested; may be +-- in string format or already parsed into a table. +-- @param requested string or table: Version requested; may be +-- in string format or already parsed into a table. +-- @return boolean: True if the tested version matches the requested +-- version, false otherwise. +local function partial_match(version, requested) + assert(type(version) == "string" or type(version) == "table") + assert(type(requested) == "string" or type(version) == "table") + + if type(version) ~= "table" then version = vers.parse_version(version) end + if type(requested) ~= "table" then requested = vers.parse_version(requested) end + if not version or not requested then return false end + + for i, ri in ipairs(requested) do + local vi = version[i] or 0 + if ri ~= vi then return false end + end + if requested.revision then + return requested.revision == version.revision + end + return true +end + +--- Check if a version satisfies a set of constraints. +-- @param version table: A version in table format +-- @param constraints table: An array of constraints in table format. +-- @return boolean: True if version satisfies all constraints, +-- false otherwise. +function vers.match_constraints(version, constraints) + assert(type(version) == "table") + assert(type(constraints) == "table") + local ok = true + setmetatable(version, version_mt) + for _, constr in pairs(constraints) do + if type(constr.version) == "string" then + constr.version = vers.parse_version(constr.version) + end + local constr_version, constr_op = constr.version, constr.op + setmetatable(constr_version, version_mt) + if constr_op == "==" then ok = version == constr_version + elseif constr_op == "~=" then ok = version ~= constr_version + elseif constr_op == ">" then ok = version > constr_version + elseif constr_op == "<" then ok = version < constr_version + elseif constr_op == ">=" then ok = version >= constr_version + elseif constr_op == "<=" then ok = version <= constr_version + elseif constr_op == "~>" then ok = partial_match(version, constr_version) + end + if not ok then break end + end + return ok +end + +return vers diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/deplocks.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/deplocks.lua new file mode 100644 index 000000000..d62908f46 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/deplocks.lua @@ -0,0 +1,106 @@ +local deplocks = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") +local persist = require("luarocks.persist") + +local deptable = {} +local deptable_mode = "start" +local deplock_abs_filename +local deplock_root_rock_name + +function deplocks.init(root_rock_name, dirname) + if deptable_mode ~= "start" then + return + end + deptable_mode = "create" + + local filename = dir.path(dirname, "luarocks.lock") + deplock_abs_filename = fs.absolute_name(filename) + deplock_root_rock_name = root_rock_name + + deptable = {} +end + +function deplocks.get_abs_filename(root_rock_name) + if root_rock_name == deplock_root_rock_name then + return deplock_abs_filename + end +end + +function deplocks.load(root_rock_name, dirname) + if deptable_mode ~= "start" then + return true, nil + end + deptable_mode = "locked" + + local filename = dir.path(dirname, "luarocks.lock") + local ok, result, errcode = persist.run_file(filename, {}) + if errcode == "load" or errcode == "run" then + -- bad config file or depends on env, so error out + return nil, nil, "Could not read existing lockfile " .. filename + end + + if errcode == "open" then + -- could not open, maybe file does not exist + return true, nil + end + + deplock_abs_filename = fs.absolute_name(filename) + deplock_root_rock_name = root_rock_name + + deptable = result + return true, filename +end + +function deplocks.add(depskey, name, version) + if deptable_mode == "locked" then + return + end + + local dk = deptable[depskey] + if not dk then + dk = {} + deptable[depskey] = dk + end + + if not dk[name] then + dk[name] = version + end +end + +function deplocks.get(depskey, name) + local dk = deptable[depskey] + if not dk then + return nil + end + + return deptable[name] +end + +function deplocks.write_file() + if deptable_mode ~= "create" then + return true + end + + return persist.save_as_module(deplock_abs_filename, deptable) +end + +-- a table-like interface to deplocks +function deplocks.proxy(depskey) + return setmetatable({}, { + __index = function(_, k) + return deplocks.get(depskey, k) + end, + __newindex = function(_, k, v) + return deplocks.add(depskey, k, v) + end, + }) +end + +function deplocks.each(depskey) + return util.sortedpairs(deptable[depskey] or {}) +end + +return deplocks diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/deps.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/deps.lua new file mode 100644 index 000000000..07f73e65e --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/deps.lua @@ -0,0 +1,783 @@ + +--- High-level dependency related functions. +local deps = {} + +local cfg = require("luarocks.core.cfg") +local manif = require("luarocks.manif") +local path = require("luarocks.path") +local dir = require("luarocks.dir") +local fun = require("luarocks.fun") +local util = require("luarocks.util") +local vers = require("luarocks.core.vers") +local queries = require("luarocks.queries") +local builtin = require("luarocks.build.builtin") +local deplocks = require("luarocks.deplocks") + +--- Generate a function that matches dep queries against the manifest, +-- taking into account rocks_provided, the list of versions to skip, +-- and the lockfile. +-- @param deps_mode "one", "none", "all" or "order" +-- @param rocks_provided a one-level table mapping names to versions, +-- listing rocks to consider provided by the VM +-- @param rocks_provided table: A table of auto-provided dependencies. +-- by this Lua implementation for the given dependency. +-- @param depskey key to use when matching the lockfile ("dependencies", +-- "build_dependencies", etc.) +-- @param skip_set a two-level table mapping names to versions to +-- boolean, listing rocks that should not be matched +-- @return function(dep): {string}, {string:string}, string, boolean +-- * array of matching versions +-- * map of versions to locations +-- * version matched via lockfile if any +-- * true if rock matched via rocks_provided +local function prepare_get_versions(deps_mode, rocks_provided, depskey, skip_set) + assert(type(deps_mode) == "string") + assert(type(rocks_provided) == "table") + assert(type(depskey) == "string") + assert(type(skip_set) == "table" or skip_set == nil) + + return function(dep) + local versions, locations + local provided = rocks_provided[dep.name] + if provided then + -- Provided rocks have higher priority than manifest's rocks. + versions, locations = { provided }, {} + else + if deps_mode == "none" then + deps_mode = "one" + end + versions, locations = manif.get_versions(dep, deps_mode) + end + + if skip_set and skip_set[dep.name] then + for i = #versions, 1, -1 do + local v = versions[i] + if skip_set[dep.name][v] then + table.remove(versions, i) + end + end + end + + local lockversion = deplocks.get(depskey, dep.name) + + return versions, locations, lockversion, provided ~= nil + end +end + +--- Attempt to match a dependency to an installed rock. +-- @param get_versions a getter function obtained via prepare_get_versions +-- @return (string, string, table) or (nil, nil, table): +-- 1. latest installed version of the rock matching the dependency +-- 2. location where the installed version is installed +-- 3. the 'dep' query table +-- 4. true if provided via VM +-- or +-- 1. nil +-- 2. nil +-- 3. either 'dep' or an alternative query to be used +-- 4. false +local function match_dep(dep, get_versions) + assert(type(dep) == "table") + assert(type(get_versions) == "function") + + local versions, locations, lockversion, provided = get_versions(dep) + + local latest_version + local latest_vstring + for _, vstring in ipairs(versions) do + local version = vers.parse_version(vstring) + if vers.match_constraints(version, dep.constraints) then + if not latest_version or version > latest_version then + latest_version = version + latest_vstring = vstring + end + end + end + + if lockversion and not locations[lockversion] then + local latest_matching_msg = "" + if latest_vstring and latest_vstring ~= lockversion then + latest_matching_msg = " (latest matching is " .. latest_vstring .. ")" + end + util.printout("Forcing " .. dep.name .. " to pinned version " .. lockversion .. latest_matching_msg) + return nil, nil, queries.new(dep.name, dep.namespace, lockversion) + end + + return latest_vstring, locations[latest_vstring], dep, provided +end + +local function match_all_deps(dependencies, get_versions) + assert(type(dependencies) == "table") + assert(type(get_versions) == "function") + + local matched, missing, no_upgrade = {}, {}, {} + + for _, dep in ipairs(dependencies) do + local found, _, provided + found, _, dep, provided = match_dep(dep, get_versions) + if found then + if not provided then + matched[dep] = {name = dep.name, version = found} + end + else + if dep.constraints[1] and dep.constraints[1].no_upgrade then + no_upgrade[dep.name] = dep + else + missing[dep.name] = dep + end + end + end + return matched, missing, no_upgrade +end + +--- Attempt to match dependencies of a rockspec to installed rocks. +-- @param dependencies table: The table of dependencies. +-- @param rocks_provided table: The table of auto-provided dependencies. +-- @param skip_set table or nil: Program versions to not use as valid matches. +-- Table where keys are program names and values are tables where keys +-- are program versions and values are 'true'. +-- @param deps_mode string: Which trees to check dependencies for +-- @return table, table, table: A table where keys are dependencies parsed +-- in table format and values are tables containing fields 'name' and +-- version' representing matches; a table of missing dependencies +-- parsed as tables; and a table of "no-upgrade" missing dependencies +-- (to be used in plugin modules so that a plugin does not force upgrade of +-- its parent application). +function deps.match_deps(dependencies, rocks_provided, skip_set, deps_mode) + assert(type(dependencies) == "table") + assert(type(rocks_provided) == "table") + assert(type(skip_set) == "table" or skip_set == nil) + assert(type(deps_mode) == "string") + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies", skip_set) + return match_all_deps(dependencies, get_versions) +end + +local function rock_status(dep, get_versions) + assert(dep:type() == "query") + assert(type(get_versions) == "function") + + local installed, _, _, provided = match_dep(dep, get_versions) + local installation_type = provided and "provided by VM" or "installed" + return installed and installed.." "..installation_type or "not installed" +end + +--- Check depenendencies of a package and report any missing ones. +-- @param name string: package name. +-- @param version string: package version. +-- @param dependencies table: array of dependencies. +-- @param deps_mode string: Which trees to check dependencies for +-- @param rocks_provided table: A table of auto-dependencies provided +-- by this Lua implementation for the given dependency. +-- "one" for the current default tree, "all" for all trees, +-- "order" for all trees with priority >= the current default, "none" for no trees. +function deps.report_missing_dependencies(name, version, dependencies, deps_mode, rocks_provided) + assert(type(name) == "string") + assert(type(version) == "string") + assert(type(dependencies) == "table") + assert(type(deps_mode) == "string") + assert(type(rocks_provided) == "table") + + if deps_mode == "none" then + return + end + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies") + + local first_missing_dep = true + + for _, dep in ipairs(dependencies) do + local found, _ + found, _, dep = match_dep(dep, get_versions) + if not found then + if first_missing_dep then + util.printout(("Missing dependencies for %s %s:"):format(name, version)) + first_missing_dep = false + end + + util.printout((" %s (%s)"):format(tostring(dep), rock_status(dep, get_versions))) + end + end +end + +function deps.fulfill_dependency(dep, deps_mode, rocks_provided, verify, depskey) + assert(dep:type() == "query") + assert(type(deps_mode) == "string" or deps_mode == nil) + assert(type(rocks_provided) == "table" or rocks_provided == nil) + assert(type(verify) == "boolean" or verify == nil) + assert(type(depskey) == "string") + + deps_mode = deps_mode or "all" + rocks_provided = rocks_provided or {} + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, depskey) + + local found, where + found, where, dep = match_dep(dep, get_versions) + if found then + local tree_manifests = manif.load_rocks_tree_manifests(deps_mode) + manif.scan_dependencies(dep.name, found, tree_manifests, deplocks.proxy(depskey)) + return true, found, where + end + + local search = require("luarocks.search") + local install = require("luarocks.cmd.install") + + local url, search_err = search.find_suitable_rock(dep) + if not url then + return nil, "Could not satisfy dependency "..tostring(dep)..": "..search_err + end + util.printout("Installing "..url) + local install_args = { + rock = url, + deps_mode = deps_mode, + namespace = dep.namespace, + verify = verify, + } + local ok, install_err, errcode = install.command(install_args) + if not ok then + return nil, "Failed installing dependency: "..url.." - "..install_err, errcode + end + + found, where = match_dep(dep, get_versions) + assert(found) + return true, found, where +end + +local function check_supported_platforms(rockspec) + if rockspec.supported_platforms and next(rockspec.supported_platforms) then + local all_negative = true + local supported = false + for _, plat in pairs(rockspec.supported_platforms) do + local neg + neg, plat = plat:match("^(!?)(.*)") + if neg == "!" then + if cfg.is_platform(plat) then + return nil, "This rockspec for "..rockspec.package.." does not support "..plat.." platforms." + end + else + all_negative = false + if cfg.is_platform(plat) then + supported = true + break + end + end + end + if supported == false and not all_negative then + local plats = cfg.print_platforms() + return nil, "This rockspec for "..rockspec.package.." does not support "..plats.." platforms." + end + end + + return true +end + +--- Check dependencies of a rock and attempt to install any missing ones. +-- Packages are installed using the LuaRocks "install" command. +-- Aborts the program if a dependency could not be fulfilled. +-- @param rockspec table: A rockspec in table format. +-- @param depskey string: Rockspec key to fetch to get dependency table +-- ("dependencies", "build_dependencies", etc.). +-- @param deps_mode string +-- @param verify boolean +-- @param deplock_dir string: dirname of the deplock file +-- @return boolean or (nil, string, [string]): True if no errors occurred, or +-- nil and an error message if any test failed, followed by an optional +-- error code. +function deps.fulfill_dependencies(rockspec, depskey, deps_mode, verify, deplock_dir) + assert(type(rockspec) == "table") + assert(type(depskey) == "string") + assert(type(deps_mode) == "string") + assert(type(verify) == "boolean" or verify == nil) + assert(type(deplock_dir) == "string" or deplock_dir == nil) + + local name = rockspec.name + local version = rockspec.version + local rocks_provided = rockspec.rocks_provided + + local ok, filename, err = deplocks.load(name, deplock_dir or ".") + if filename then + util.printout("Using dependencies pinned in lockfile: " .. filename) + + local get_versions = prepare_get_versions("none", rocks_provided, depskey) + for dnsname, dversion in deplocks.each(depskey) do + local dname, dnamespace = util.split_namespace(dnsname) + local dep = queries.new(dname, dnamespace, dversion) + + util.printout(("%s %s is pinned to %s (%s)"):format( + name, version, tostring(dep), rock_status(dep, get_versions))) + + local ok, err = deps.fulfill_dependency(dep, "none", rocks_provided, verify, depskey) + if not ok then + return nil, err + end + end + util.printout() + return true + elseif err then + util.warning(err) + end + + ok, err = check_supported_platforms(rockspec) + if not ok then + return nil, err + end + + deps.report_missing_dependencies(name, version, rockspec[depskey], deps_mode, rocks_provided) + + util.printout() + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, depskey) + for _, dep in ipairs(rockspec[depskey]) do + + util.printout(("%s %s depends on %s (%s)"):format( + name, version, tostring(dep), rock_status(dep, get_versions))) + + local ok, found_or_err, _, no_upgrade = deps.fulfill_dependency(dep, deps_mode, rocks_provided, verify, depskey) + if ok then + deplocks.add(depskey, dep.name, found_or_err) + else + if no_upgrade then + util.printerr("This version of "..name.." is designed for use with") + util.printerr(tostring(dep)..", but is configured to avoid upgrading it") + util.printerr("automatically. Please upgrade "..dep.name.." with") + util.printerr(" luarocks install "..dep.name) + util.printerr("or look for a suitable version of "..name.." with") + util.printerr(" luarocks search "..name) + end + return nil, found_or_err + end + end + + return true +end + +--- If filename matches a pattern, return the capture. +-- For example, given "libfoo.so" and "lib?.so" is a pattern, +-- returns "foo" (which can then be used to build names +-- based on other patterns. +-- @param file string: a filename +-- @param pattern string: a pattern, where ? is to be matched by the filename. +-- @return string The pattern, if found, or nil. +local function deconstruct_pattern(file, pattern) + local depattern = "^"..(pattern:gsub("%.", "%%."):gsub("%*", ".*"):gsub("?", "(.*)")).."$" + return (file:match(depattern)) +end + +--- Construct all possible patterns for a name and add to the files array. +-- Run through the patterns array replacing all occurrences of "?" +-- with the given file name and store them in the files array. +-- @param file string A raw name (e.g. "foo") +-- @param array of string An array of patterns with "?" as the wildcard +-- (e.g. {"?.so", "lib?.so"}) +-- @param files The array of constructed names +local function add_all_patterns(file, patterns, files) + for _, pattern in ipairs(patterns) do + table.insert(files, {#files + 1, (pattern:gsub("?", file))}) + end +end + +local function get_external_deps_dirs(mode) + local patterns = cfg.external_deps_patterns + local subdirs = cfg.external_deps_subdirs + if mode == "install" then + patterns = cfg.runtime_external_deps_patterns + subdirs = cfg.runtime_external_deps_subdirs + end + local dirs = { + BINDIR = { subdir = subdirs.bin, testfile = "program", pattern = patterns.bin }, + INCDIR = { subdir = subdirs.include, testfile = "header", pattern = patterns.include }, + LIBDIR = { subdir = subdirs.lib, testfile = "library", pattern = patterns.lib } + } + if mode == "install" then + dirs.INCDIR = nil + end + return dirs +end + +local function resolve_prefix(prefix, dirs) + if type(prefix) == "string" then + return prefix + elseif type(prefix) == "table" then + if prefix.bin then + dirs.BINDIR.subdir = prefix.bin + end + if prefix.include then + if dirs.INCDIR then + dirs.INCDIR.subdir = prefix.include + end + end + if prefix.lib then + dirs.LIBDIR.subdir = prefix.lib + end + return prefix.prefix + end +end + +local function add_patterns_for_file(files, file, patterns) + -- If it doesn't look like it contains a filename extension + if not (file:match("%.[a-z]+$") or file:match("%.[a-z]+%.")) then + add_all_patterns(file, patterns, files) + else + for _, pattern in ipairs(patterns) do + local matched = deconstruct_pattern(file, pattern) + if matched then + add_all_patterns(matched, patterns, files) + end + end + table.insert(files, {#files + 1, file}) + end +end + +local function check_external_dependency_at(prefix, name, ext_files, vars, dirs, err_files, cache) + local fs = require("luarocks.fs") + cache = cache or {} + + for dirname, dirdata in util.sortedpairs(dirs) do + local paths + local path_var_value = vars[name.."_"..dirname] + if path_var_value then + paths = { path_var_value } + elseif type(dirdata.subdir) == "table" then + paths = {} + for i,v in ipairs(dirdata.subdir) do + paths[i] = dir.path(prefix, v) + end + else + paths = { dir.path(prefix, dirdata.subdir) } + end + dirdata.dir = paths[1] + local file_or_files = ext_files[dirdata.testfile] + if file_or_files then + local files = {} + if type(file_or_files) == "string" then + add_patterns_for_file(files, file_or_files, dirdata.pattern) + elseif type(file_or_files) == "table" then + for _, f in ipairs(file_or_files) do + add_patterns_for_file(files, f, dirdata.pattern) + end + end + + local found = false + table.sort(files, function(a, b) + if (not a[2]:match("%*")) and b[2]:match("%*") then + return true + elseif a[2]:match("%*") and (not b[2]:match("%*")) then + return false + else + return a[1] < b[1] + end + end) + for _, fa in ipairs(files) do + + local f = fa[2] + -- small convenience hack + if f:match("%.so$") or f:match("%.dylib$") or f:match("%.dll$") then + f = f:gsub("%.[^.]+$", "."..cfg.external_lib_extension) + end + + local pattern + if f:match("%*") then + pattern = "^" .. f:gsub("([-.+])", "%%%1"):gsub("%*", ".*") .. "$" + f = "matching "..f + end + + for _, d in ipairs(paths) do + if pattern then + if not cache[d] then + cache[d] = fs.list_dir(d) + end + local match = string.match + for _, entry in ipairs(cache[d]) do + if match(entry, pattern) then + found = true + break + end + end + else + found = fs.is_file(dir.path(d, f)) + end + if found then + dirdata.dir = d + dirdata.file = f + break + else + table.insert(err_files[dirdata.testfile], f.." in "..d) + end + end + if found then + break + end + end + if not found then + return nil, dirname, dirdata.testfile + end + end + end + + for dirname, dirdata in pairs(dirs) do + vars[name.."_"..dirname] = dirdata.dir + vars[name.."_"..dirname.."_FILE"] = dirdata.file + end + vars[name.."_DIR"] = prefix + return true +end + +local function check_external_dependency(name, ext_files, vars, mode, cache) + local ok + local err_dirname + local err_testfile + local err_files = {program = {}, header = {}, library = {}} + + local dirs = get_external_deps_dirs(mode) + + local prefixes + if vars[name .. "_DIR"] then + prefixes = { vars[name .. "_DIR"] } + elseif vars.DEPS_DIR then + prefixes = { vars.DEPS_DIR } + else + prefixes = cfg.external_deps_dirs + end + + for _, prefix in ipairs(prefixes) do + prefix = resolve_prefix(prefix, dirs) + if cfg.is_platform("mingw32") and name == "LUA" then + dirs.LIBDIR.pattern = fun.filter(util.deep_copy(dirs.LIBDIR.pattern), function(s) + return not s:match("%.a$") + end) + elseif cfg.is_platform("windows") and name == "LUA" then + dirs.LIBDIR.pattern = fun.filter(util.deep_copy(dirs.LIBDIR.pattern), function(s) + return not s:match("%.dll$") + end) + end + ok, err_dirname, err_testfile = check_external_dependency_at(prefix, name, ext_files, vars, dirs, err_files, cache) + if ok then + return true + end + end + + return nil, err_dirname, err_testfile, err_files +end + +--- Set up path-related variables for external dependencies. +-- For each key in the external_dependencies table in the +-- rockspec file, four variables are created: _DIR, _BINDIR, +-- _INCDIR and _LIBDIR. These are not overwritten +-- if already set (e.g. by the LuaRocks config file or through the +-- command-line). Values in the external_dependencies table +-- are tables that may contain a "header" or a "library" field, +-- with filenames to be tested for existence. +-- @param rockspec table: The rockspec table. +-- @param mode string: if "build" is given, checks all files; +-- if "install" is given, do not scan for headers. +-- @return boolean or (nil, string): True if no errors occurred, or +-- nil and an error message if any test failed. +function deps.check_external_deps(rockspec, mode) + assert(rockspec:type() == "rockspec") + + if not rockspec.external_dependencies then + rockspec.external_dependencies = builtin.autodetect_external_dependencies(rockspec.build) + end + if not rockspec.external_dependencies then + return true + end + + for name, ext_files in util.sortedpairs(rockspec.external_dependencies) do + local ok, err_dirname, err_testfile, err_files = check_external_dependency(name, ext_files, rockspec.variables, mode) + if not ok then + local lines = {"Could not find "..err_testfile.." file for "..name} + + local err_paths = {} + for _, err_file in ipairs(err_files[err_testfile]) do + if not err_paths[err_file] then + err_paths[err_file] = true + table.insert(lines, " No file "..err_file) + end + end + + table.insert(lines, "You may have to install "..name.." in your system and/or pass "..name.."_DIR or "..name.."_"..err_dirname.." to the luarocks command.") + table.insert(lines, "Example: luarocks install "..rockspec.name.." "..name.."_DIR=/usr/local") + + return nil, table.concat(lines, "\n"), "dependency" + end + end + return true +end + +--- Recursively add satisfied dependencies of a package to a table, +-- to build a transitive closure of all dependent packages. +-- Additionally ensures that `dependencies` table of the manifest is up-to-date. +-- @param results table: The results table being built, maps package names to versions. +-- @param manifest table: The manifest table containing dependencies. +-- @param name string: Package name. +-- @param version string: Package version. +function deps.scan_deps(results, manifest, name, version, deps_mode) + assert(type(results) == "table") + assert(type(manifest) == "table") + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local fetch = require("luarocks.fetch") + + if results[name] then + return + end + if not manifest.dependencies then manifest.dependencies = {} end + local md = manifest.dependencies + if not md[name] then md[name] = {} end + local mdn = md[name] + local dependencies = mdn[version] + local rocks_provided + if not dependencies then + local rockspec, err = fetch.load_local_rockspec(path.rockspec_file(name, version), false) + if not rockspec then + util.printerr("Couldn't load rockspec for "..name.." "..version..": "..err) + return + end + dependencies = rockspec.dependencies + rocks_provided = rockspec.rocks_provided + mdn[version] = dependencies + else + rocks_provided = util.get_rocks_provided() + end + + local get_versions = prepare_get_versions(deps_mode, rocks_provided, "dependencies") + + local matched = match_all_deps(dependencies, get_versions) + results[name] = version + for _, match in pairs(matched) do + deps.scan_deps(results, manifest, match.name, match.version, deps_mode) + end +end + +local function lua_h_exists(d, luaver) + local n = tonumber(luaver) + local major = math.floor(n) + local minor = (n - major) * 10 + local luanum = math.floor(major * 100 + minor) + + local lua_h = dir.path(d, "lua.h") + local fd = io.open(lua_h) + if fd then + local data = fd:read("*a") + fd:close() + if data:match("LUA_VERSION_NUM%s*" .. tostring(luanum)) then + return d + end + return nil, "Lua header found at " .. d .. " does not match Lua version " .. luaver .. ". You may want to override this by configuring LUA_INCDIR.", "dependency", 2 + end + + return nil, "Failed finding Lua header files. You may need to install them or configure LUA_INCDIR.", "dependency", 1 +end + +local function find_lua_incdir(prefix, luaver, luajitver) + luajitver = luajitver and luajitver:gsub("%-.*", "") + local shortv = luaver:gsub("%.", "") + local incdirs = { + prefix .. "/include/lua/" .. luaver, + prefix .. "/include/lua" .. luaver, + prefix .. "/include/lua-" .. luaver, + prefix .. "/include/lua" .. shortv, + prefix .. "/include", + prefix, + luajitver and (prefix .. "/include/luajit-" .. (luajitver:match("^(%d+%.%d+)") or "")), + } + local errprio = 0 + local mainerr + for _, d in ipairs(incdirs) do + local ok, err, _, prio = lua_h_exists(d, luaver) + if ok then + return d + end + if prio > errprio then + mainerr = err + errprio = prio + end + end + + -- not found, will fallback to a default + return nil, mainerr +end + +function deps.check_lua_incdir(vars) + local ljv = util.get_luajit_version() + + if vars.LUA_INCDIR then + return lua_h_exists(vars.LUA_INCDIR, cfg.lua_version) + end + + if vars.LUA_DIR then + local d, err = find_lua_incdir(vars.LUA_DIR, cfg.lua_version, ljv) + if d then + vars.LUA_INCDIR = d + return true + end + return nil, err + end + + return nil, "Failed finding Lua header files. You may need to install them or configure LUA_INCDIR.", "dependency" +end + +function deps.check_lua_libdir(vars) + local fs = require("luarocks.fs") + local ljv = util.get_luajit_version() + + if vars.LUA_LIBDIR and vars.LUALIB and fs.exists(dir.path(vars.LUA_LIBDIR, vars.LUALIB)) then + return true + end + + -- Tarantool has no separate dynamic library. All necessary + -- symbols are exposed from its executable file. + if cfg.lua_interpreter == "tarantool" then + return true + end + + local shortv = cfg.lua_version:gsub("%.", "") + local libnames = { + "lua" .. cfg.lua_version, + "lua" .. shortv, + "lua-" .. cfg.lua_version, + "lua-" .. shortv, + "lua", + } + if ljv then + table.insert(libnames, 1, "luajit-" .. cfg.lua_version) + end + local cache = {} + local save_LUA_INCDIR = vars.LUA_INCDIR + local ok = check_external_dependency("LUA", { library = libnames }, vars, "build", cache) + vars.LUA_INCDIR = save_LUA_INCDIR + local err + if ok then + local filename = dir.path(vars.LUA_LIBDIR, vars.LUA_LIBDIR_FILE) + local fd = io.open(filename, "r") + if fd then + if not vars.LUA_LIBDIR_FILE:match((cfg.lua_version:gsub("%.", "%%.?"))) then + -- if filename isn't versioned, check file contents + local txt = fd:read("*a") + ok = txt:match("Lua " .. cfg.lua_version, 1, true) + or txt:match("lua" .. (cfg.lua_version:gsub("%.", "")), 1, true) + if not ok then + err = "Lua library at " .. filename .. " does not match Lua version " .. cfg.lua_version .. ". You may want to override this by configuring LUA_LIBDIR." + end + end + + fd:close() + end + end + + if ok then + vars.LUALIB = vars.LUA_LIBDIR_FILE + return true + else + err = err or "Failed finding Lua library. You may need to configure LUA_LIBDIR." + return nil, err, "dependency" + end +end + +function deps.get_deps_mode(args) + return args.deps_mode or cfg.deps_mode +end + +return deps diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/dir.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/dir.lua new file mode 100644 index 000000000..74adbea2b --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/dir.lua @@ -0,0 +1,48 @@ + +--- Generic utilities for handling pathnames. +local dir = {} + +local core = require("luarocks.core.dir") + +dir.path = core.path +dir.split_url = core.split_url +dir.normalize = core.normalize + +--- Strip the path off a path+filename. +-- @param pathname string: A path+name, such as "/a/b/c" +-- or "\a\b\c". +-- @return string: The filename without its path, such as "c". +function dir.base_name(pathname) + assert(type(pathname) == "string") + + local base = pathname:gsub("[/\\]*$", ""):match(".*[/\\]([^/\\]*)") + return base or pathname +end + +--- Strip the name off a path+filename. +-- @param pathname string: A path+name, such as "/a/b/c". +-- @return string: The filename without its path, such as "/a/b". +-- For entries such as "/a/b/", "/a" is returned. If there are +-- no directory separators in input, "" is returned. +function dir.dir_name(pathname) + assert(type(pathname) == "string") + return (pathname:gsub("/*$", ""):match("(.*)[/]+[^/]*")) or "" +end + +--- Returns true if protocol does not require additional tools. +-- @param protocol The protocol name +function dir.is_basic_protocol(protocol) + return protocol == "http" or protocol == "https" or protocol == "ftp" or protocol == "file" +end + +function dir.deduce_base_dir(url) + -- for extensions like foo.tar.gz, "gz" is stripped first + local known_exts = {} + for _, ext in ipairs{"zip", "git", "tgz", "tar", "gz", "bz2"} do + known_exts[ext] = "" + end + local base = dir.base_name(url) + return (base:gsub("%.([^.]*)$", known_exts):gsub("%.tar", "")) +end + +return dir diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/download.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/download.lua new file mode 100644 index 000000000..07a2a65f0 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/download.lua @@ -0,0 +1,68 @@ +local download = {} + +local path = require("luarocks.path") +local fetch = require("luarocks.fetch") +local search = require("luarocks.search") +local queries = require("luarocks.queries") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") + +local function get_file(filename) + local protocol, pathname = dir.split_url(filename) + if protocol == "file" then + local ok, err = fs.copy(pathname, fs.current_dir(), "read") + if ok then + return pathname + else + return nil, err + end + else + -- discard third result + local ok, err = fetch.fetch_url(filename) + return ok, err + end +end + +function download.download(arch, name, namespace, version, all, check_lua_versions) + local substring = (all and name == "") + local query = queries.new(name, namespace, version, substring, arch) + local search_err + + if all then + local results = search.search_repos(query) + local has_result = false + local all_ok = true + local any_err = "" + for name, result in pairs(results) do -- luacheck: ignore 422 + for version, items in pairs(result) do -- luacheck: ignore 422 + for _, item in ipairs(items) do + -- Ignore provided rocks. + if item.arch ~= "installed" then + has_result = true + local filename = path.make_url(item.repo, name, version, item.arch) + local ok, err = get_file(filename) + if not ok then + all_ok = false + any_err = any_err .. "\n" .. err + end + end + end + end + end + + if has_result then + return all_ok, any_err + end + else + local url + url, search_err = search.find_rock_checking_lua_versions(query, check_lua_versions) + if url then + return get_file(url) + end + end + local rock = util.format_rock_name(name, namespace, version) + return nil, "Could not find a result named "..rock..(search_err and ": "..search_err or ".") +end + +return download diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch.lua new file mode 100644 index 000000000..90e7c79ec --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch.lua @@ -0,0 +1,537 @@ + +--- Functions related to fetching and loading local and remote files. +local fetch = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local rockspecs = require("luarocks.rockspecs") +local signing = require("luarocks.signing") +local persist = require("luarocks.persist") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") + + +--- Fetch a local or remote file, using a local cache directory. +-- Make a remote or local URL/pathname local, fetching the file if necessary. +-- Other "fetch" and "load" functions use this function to obtain files. +-- If a local pathname is given, it is returned as a result. +-- @param url string: a local pathname or a remote URL. +-- @param mirroring string: mirroring mode. +-- If set to "no_mirror", then rocks_servers mirror configuration is not used. +-- @return (string, nil, nil, boolean) or (nil, string, [string]): +-- in case of success: +-- * the absolute local pathname for the fetched file +-- * nil +-- * nil +-- * `true` if the file was fetched from cache +-- in case of failure: +-- * nil +-- * an error message +-- * an optional error code. +function fetch.fetch_caching(url, mirroring) + local repo_url, filename = url:match("^(.*)/([^/]+)$") + local name = repo_url:gsub("[/:]","_") + local cache_dir = dir.path(cfg.local_cache, name) + local ok = fs.make_dir(cache_dir) + if not ok then + cfg.local_cache = fs.make_temp_dir("local_cache") + cache_dir = dir.path(cfg.local_cache, name) + ok = fs.make_dir(cache_dir) + if not ok then + return nil, "Failed creating temporary cache directory "..cache_dir + end + end + + local cachefile = dir.path(cache_dir, filename) + if cfg.aggressive_cache and (not name:match("^manifest")) and fs.exists(cachefile) then + return cachefile, nil, nil, true + end + + local file, err, errcode, from_cache = fetch.fetch_url(url, cachefile, true, mirroring) + if not file then + return nil, err or "Failed downloading "..url, errcode + end + return file, nil, nil, from_cache +end + +local function ensure_trailing_slash(url) + return (url:gsub("/*$", "/")) +end + +local function is_url_relative_to_rocks_servers(url, servers) + for _, item in ipairs(servers) do + if type(item) == "table" then + for i, s in ipairs(item) do + local base = ensure_trailing_slash(s) + if string.find(url, base, 1, true) == 1 then + return i, url:sub(#base + 1), item + end + end + end + end +end + +local function download_with_mirrors(url, filename, cache, servers) + local idx, rest, mirrors = is_url_relative_to_rocks_servers(url, servers) + + if not idx then + -- URL is not from a rock server + return fs.download(url, filename, cache) + end + + -- URL is from a rock server: try to download it falling back to mirrors. + local err = "\n" + for i = idx, #mirrors do + local try_url = ensure_trailing_slash(mirrors[i]) .. rest + if i > idx then + util.warning("Failed downloading. Attempting mirror at " .. try_url) + end + local ok, name, from_cache = fs.download(try_url, filename, cache) + if ok then + return ok, name, from_cache + else + err = err .. name .. "\n" + end + end + + return nil, err +end + +--- Fetch a local or remote file. +-- Make a remote or local URL/pathname local, fetching the file if necessary. +-- Other "fetch" and "load" functions use this function to obtain files. +-- If a local pathname is given, it is returned as a result. +-- @param url string: a local pathname or a remote URL. +-- @param filename string or nil: this function attempts to detect the +-- resulting local filename of the remote file as the basename of the URL; +-- if that is not correct (due to a redirection, for example), the local +-- filename can be given explicitly as this second argument. +-- @param cache boolean: compare remote timestamps via HTTP HEAD prior to +-- re-downloading the file. +-- @param mirroring string: mirroring mode. +-- If set to "no_mirror", then rocks_servers mirror configuration is not used. +-- @return (string, nil, nil, boolean) or (nil, string, [string]): +-- in case of success: +-- * the absolute local pathname for the fetched file +-- * nil +-- * nil +-- * `true` if the file was fetched from cache +-- in case of failure: +-- * nil +-- * an error message +-- * an optional error code. +function fetch.fetch_url(url, filename, cache, mirroring) + assert(type(url) == "string") + assert(type(filename) == "string" or not filename) + + local protocol, pathname = dir.split_url(url) + if protocol == "file" then + local fullname = dir.normalize(fs.absolute_name(pathname)) + if not fs.exists(fullname) then + local hint = (not pathname:match("^/")) + and (" - note that given path in rockspec is not absolute: " .. url) + or "" + return nil, "Local file not found: " .. fullname .. hint + end + filename = filename or dir.base_name(pathname) + local dstname = dir.normalize(fs.absolute_name(dir.path(".", filename))) + local ok, err + if fullname == dstname then + ok = true + else + ok, err = fs.copy(fullname, dstname) + end + if ok then + return dstname + else + return nil, "Failed copying local file " .. fullname .. " to " .. dstname .. ": " .. err + end + elseif dir.is_basic_protocol(protocol) then + local ok, name, from_cache + if mirroring ~= "no_mirror" then + ok, name, from_cache = download_with_mirrors(url, filename, cache, cfg.rocks_servers) + else + ok, name, from_cache = fs.download(url, filename, cache) + end + if not ok then + return nil, "Failed downloading "..url..(name and " - "..name or ""), "network" + end + return name, nil, nil, from_cache + else + return nil, "Unsupported protocol "..protocol + end +end + +--- For remote URLs, create a temporary directory and download URL inside it. +-- This temporary directory will be deleted on program termination. +-- For local URLs, just return the local pathname and its directory. +-- @param url string: URL to be downloaded +-- @param tmpname string: name pattern to use for avoiding conflicts +-- when creating temporary directory. +-- @param filename string or nil: local filename of URL to be downloaded, +-- in case it can't be inferred from the URL. +-- @return (string, string) or (nil, string, [string]): absolute local pathname of +-- the fetched file and temporary directory name; or nil and an error message +-- followed by an optional error code +function fetch.fetch_url_at_temp_dir(url, tmpname, filename, cache) + assert(type(url) == "string") + assert(type(tmpname) == "string") + assert(type(filename) == "string" or not filename) + filename = filename or dir.base_name(url) + + local protocol, pathname = dir.split_url(url) + if protocol == "file" then + if fs.exists(pathname) then + return pathname, dir.dir_name(fs.absolute_name(pathname)) + else + return nil, "File not found: "..pathname + end + else + local temp_dir, err = fs.make_temp_dir(tmpname) + if not temp_dir then + return nil, "Failed creating temporary directory "..tmpname..": "..err + end + util.schedule_function(fs.delete, temp_dir) + local ok, err = fs.change_dir(temp_dir) + if not ok then return nil, err end + + local file, err, errcode + + if cache then + local cachefile + cachefile, err, errcode = fetch.fetch_caching(url) + + if cachefile then + file = dir.path(temp_dir, filename) + fs.copy(cachefile, file) + end + else + file, err, errcode = fetch.fetch_url(url, filename, cache) + end + + fs.pop_dir() + if not file then + return nil, "Error fetching file: "..err, errcode + end + + return file, temp_dir + end +end + +-- Determine base directory of a fetched URL by extracting its +-- archive and looking for a directory in the root. +-- @param file string: absolute local pathname of the fetched file +-- @param temp_dir string: temporary directory in which URL was fetched. +-- @param src_url string: URL to use when inferring base directory. +-- @param src_dir string or nil: expected base directory (inferred +-- from src_url if not given). +-- @return (string, string) or (string, nil) or (nil, string): +-- The inferred base directory and the one actually found (which may +-- be nil if not found), or nil followed by an error message. +-- The inferred dir is returned first to avoid confusion with errors, +-- because it is never nil. +function fetch.find_base_dir(file, temp_dir, src_url, src_dir) + local ok, err = fs.change_dir(temp_dir) + if not ok then return nil, err end + fs.unpack_archive(file) + local inferred_dir = src_dir or dir.deduce_base_dir(src_url) + local found_dir = nil + if fs.exists(inferred_dir) then + found_dir = inferred_dir + else + util.printerr("Directory "..inferred_dir.." not found") + local files = fs.list_dir() + if files then + table.sort(files) + for i,filename in ipairs(files) do + if fs.is_dir(filename) then + util.printerr("Found "..filename) + found_dir = filename + break + end + end + end + end + fs.pop_dir() + return inferred_dir, found_dir +end + +local function fetch_and_verify_signature_for(url, filename, tmpdir) + local sig_url = signing.signature_url(url) + local sig_file, err, errcode = fetch.fetch_url_at_temp_dir(sig_url, tmpdir) + if not sig_file then + return nil, "Could not fetch signature file for verification: " .. err, errcode + end + + local ok, err = signing.verify_signature(filename, sig_file) + if not ok then + return nil, "Failed signature verification: " .. err + end + + return fs.absolute_name(sig_file) +end + +--- Obtain a rock and unpack it. +-- If a directory is not given, a temporary directory will be created, +-- which will be deleted on program termination. +-- @param rock_file string: URL or filename of the rock. +-- @param dest string or nil: if given, directory will be used as +-- a permanent destination. +-- @param verify boolean: if true, download and verify signature for rockspec +-- @return string or (nil, string, [string]): the directory containing the contents +-- of the unpacked rock. +function fetch.fetch_and_unpack_rock(url, dest, verify) + assert(type(url) == "string") + assert(type(dest) == "string" or not dest) + + local name = dir.base_name(url):match("(.*)%.[^.]*%.rock") + local tmpname = "luarocks-rock-" .. name + + local rock_file, err, errcode = fetch.fetch_url_at_temp_dir(url, tmpname, nil, true) + if not rock_file then + return nil, "Could not fetch rock file: " .. err, errcode + end + + local sig_file + if verify then + sig_file, err = fetch_and_verify_signature_for(url, rock_file, tmpname) + if err then + return nil, err + end + end + + rock_file = fs.absolute_name(rock_file) + + local unpack_dir + if dest then + unpack_dir = dest + local ok, err = fs.make_dir(unpack_dir) + if not ok then + return nil, "Failed unpacking rock file: " .. err + end + else + unpack_dir, err = fs.make_temp_dir(name) + if not unpack_dir then + return nil, "Failed creating temporary dir: " .. err + end + end + if not dest then + util.schedule_function(fs.delete, unpack_dir) + end + local ok, err = fs.change_dir(unpack_dir) + if not ok then return nil, err end + ok, err = fs.unzip(rock_file) + if not ok then + return nil, "Failed unpacking rock file: " .. rock_file .. ": " .. err + end + if sig_file then + ok, err = fs.copy(sig_file, ".") + if not ok then + return nil, "Failed copying signature file" + end + end + fs.pop_dir() + return unpack_dir +end + +--- Back-end function that actually loads the local rockspec. +-- Performs some validation and postprocessing of the rockspec contents. +-- @param rel_filename string: The local filename of the rockspec file. +-- @param quick boolean: if true, skips some steps when loading +-- rockspec. +-- @return table or (nil, string): A table representing the rockspec +-- or nil followed by an error message. +function fetch.load_local_rockspec(rel_filename, quick) + assert(type(rel_filename) == "string") + local abs_filename = fs.absolute_name(rel_filename) + + local basename = dir.base_name(abs_filename) + if basename ~= "rockspec" then + if not basename:match("(.*)%-[^-]*%-[0-9]*") then + return nil, "Expected filename in format 'name-version-revision.rockspec'." + end + end + + local tbl, err = persist.load_into_table(abs_filename) + if not tbl then + return nil, "Could not load rockspec file "..abs_filename.." ("..err..")" + end + + local rockspec, err = rockspecs.from_persisted_table(abs_filename, tbl, err, quick) + if not rockspec then + return nil, abs_filename .. ": " .. err + end + + local name_version = rockspec.package:lower() .. "-" .. rockspec.version + if basename ~= "rockspec" and basename ~= name_version .. ".rockspec" then + return nil, "Inconsistency between rockspec filename ("..basename..") and its contents ("..name_version..".rockspec)." + end + + return rockspec +end + +--- Load a local or remote rockspec into a table. +-- This is the entry point for the LuaRocks tools. +-- Only the LuaRocks runtime loader should use +-- load_local_rockspec directly. +-- @param filename string: Local or remote filename of a rockspec. +-- @param location string or nil: Where to download. If not given, +-- a temporary dir is created. +-- @param verify boolean: if true, download and verify signature for rockspec +-- @return table or (nil, string, [string]): A table representing the rockspec +-- or nil followed by an error message and optional error code. +function fetch.load_rockspec(url, location, verify) + assert(type(url) == "string") + + local name + local basename = dir.base_name(url) + if basename == "rockspec" then + name = "rockspec" + else + name = basename:match("(.*)%.rockspec") + if not name then + return nil, "Filename '"..url.."' does not look like a rockspec." + end + end + + local tmpname = "luarocks-rockspec-"..name + local filename, err, errcode + if location then + local ok, err = fs.change_dir(location) + if not ok then return nil, err end + filename, err = fetch.fetch_url(url) + fs.pop_dir() + else + filename, err, errcode = fetch.fetch_url_at_temp_dir(url, tmpname, nil, true) + end + if not filename then + return nil, err, errcode + end + + if verify then + local _, err = fetch_and_verify_signature_for(url, filename, tmpname) + if err then + return nil, err + end + end + + return fetch.load_local_rockspec(filename) +end + +--- Download sources for building a rock using the basic URL downloader. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Whether to extract the sources from +-- the fetched source tarball or not. +-- @param dest_dir string or nil: If set, will extract to the given directory; +-- if not given, will extract to a temporary directory. +-- @return (string, string) or (nil, string, [string]): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message and optional error code. +function fetch.get_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(extract) == "boolean") + assert(type(dest_dir) == "string" or not dest_dir) + + local url = rockspec.source.url + local name = rockspec.name.."-"..rockspec.version + local filename = rockspec.source.file + local source_file, store_dir + local ok, err, errcode + if dest_dir then + ok, err = fs.change_dir(dest_dir) + if not ok then return nil, err, "dest_dir" end + source_file, err, errcode = fetch.fetch_url(url, filename) + fs.pop_dir() + store_dir = dest_dir + else + source_file, store_dir, errcode = fetch.fetch_url_at_temp_dir(url, "luarocks-source-"..name, filename) + end + if not source_file then + return nil, err or store_dir, errcode + end + if rockspec.source.md5 then + if not fs.check_md5(source_file, rockspec.source.md5) then + return nil, "MD5 check for "..filename.." has failed.", "md5" + end + end + if extract then + local ok, err = fs.change_dir(store_dir) + if not ok then return nil, err end + ok, err = fs.unpack_archive(rockspec.source.file) + if not ok then return nil, err end + if not fs.exists(rockspec.source.dir) then + + -- If rockspec.source.dir can't be found, see if we only have one + -- directory in store_dir. If that's the case, assume it's what + -- we're looking for. + -- We only do this if the rockspec source.dir was not set, and only + -- with rockspecs newer than 3.0. + local file_count, found_dir = 0 + + if not rockspec.source.dir_set and rockspec:format_is_at_least("3.0") then + for file in fs.dir() do + file_count = file_count + 1 + if fs.is_dir(file) then + found_dir = file + end + end + end + + if file_count == 1 and found_dir then + rockspec.source.dir = found_dir + else + return nil, "Directory "..rockspec.source.dir.." not found inside archive "..rockspec.source.file, "source.dir", source_file, store_dir + end + end + fs.pop_dir() + end + return source_file, store_dir +end + +--- Download sources for building a rock, calling the appropriate protocol method. +-- @param rockspec table: The rockspec table +-- @param extract boolean: When downloading compressed formats, whether to extract +-- the sources from the fetched archive or not. +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- if not given, will extract to a temporary directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function fetch.fetch_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(extract) == "boolean") + assert(type(dest_dir) == "string" or not dest_dir) + + -- auto-convert git://github.com URLs to use git+https + -- see https://github.blog/2021-09-01-improving-git-protocol-security-github/ + if rockspec.source.url:match("^git://github%.com/") + or rockspec.source.url:match("^git://www%.github%.com/") then + rockspec.source.url = rockspec.source.url:gsub("^git://", "git+https://") + rockspec.source.protocol = "git+https" + end + + local protocol = rockspec.source.protocol + local ok, proto + if dir.is_basic_protocol(protocol) then + proto = fetch + else + ok, proto = pcall(require, "luarocks.fetch."..protocol:gsub("[+-]", "_")) + if not ok then + return nil, "Unknown protocol "..protocol + end + end + + if cfg.only_sources_from + and rockspec.source.pathname + and #rockspec.source.pathname > 0 then + if #cfg.only_sources_from == 0 then + return nil, "Can't download "..rockspec.source.url.." -- download from remote servers disabled" + elseif rockspec.source.pathname:find(cfg.only_sources_from, 1, true) ~= 1 then + return nil, "Can't download "..rockspec.source.url.." -- only downloading from "..cfg.only_sources_from + end + end + + return proto.get_sources(rockspec, extract, dest_dir) +end + +return fetch diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/cvs.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/cvs.lua new file mode 100644 index 000000000..d78e6e605 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/cvs.lua @@ -0,0 +1,55 @@ + +--- Fetch back-end for retrieving sources from CVS. +local cvs = {} + +local unpack = unpack or table.unpack + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") + +--- Download sources for building a rock, using CVS. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function cvs.get_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(dest_dir) == "string" or not dest_dir) + + local cvs_cmd = rockspec.variables.CVS + local ok, err_msg = fs.is_tool_available(cvs_cmd, "CVS") + if not ok then + return nil, err_msg + end + + local name_version = rockspec.name .. "-" .. rockspec.version + local module = rockspec.source.module or dir.base_name(rockspec.source.url) + local command = {cvs_cmd, "-d"..rockspec.source.pathname, "export", module} + if rockspec.source.tag then + table.insert(command, 4, "-r") + table.insert(command, 5, rockspec.source.tag) + end + local store_dir + if not dest_dir then + store_dir = fs.make_temp_dir(name_version) + if not store_dir then + return nil, "Failed creating temporary directory." + end + util.schedule_function(fs.delete, store_dir) + else + store_dir = dest_dir + end + local ok, err = fs.change_dir(store_dir) + if not ok then return nil, err end + if not fs.execute(unpack(command)) then + return nil, "Failed fetching files from CVS." + end + fs.pop_dir() + return module, store_dir +end + + +return cvs diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git.lua new file mode 100644 index 000000000..b522a977b --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git.lua @@ -0,0 +1,165 @@ + +--- Fetch back-end for retrieving sources from GIT. +local git = {} + +local unpack = unpack or table.unpack + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local vers = require("luarocks.core.vers") +local util = require("luarocks.util") + +local cached_git_version + +--- Get git version. +-- @param git_cmd string: name of git command. +-- @return table: git version as returned by luarocks.core.vers.parse_version. +local function git_version(git_cmd) + if not cached_git_version then + local version_line = io.popen(fs.Q(git_cmd)..' --version'):read() + local version_string = version_line:match('%d-%.%d+%.?%d*') + cached_git_version = vers.parse_version(version_string) + end + + return cached_git_version +end + +--- Check if git satisfies version requirement. +-- @param git_cmd string: name of git command. +-- @param version string: required version. +-- @return boolean: true if git matches version or is newer, false otherwise. +local function git_is_at_least(git_cmd, version) + return git_version(git_cmd) >= vers.parse_version(version) +end + +--- Git >= 1.7.10 can clone a branch **or tag**, < 1.7.10 by branch only. We +-- need to know this in order to build the appropriate command; if we can't +-- clone by tag then we'll have to issue a subsequent command to check out the +-- given tag. +-- @param git_cmd string: name of git command. +-- @return boolean: Whether Git can clone by tag. +local function git_can_clone_by_tag(git_cmd) + return git_is_at_least(git_cmd, "1.7.10") +end + +--- Git >= 1.8.4 can fetch submodules shallowly, saving bandwidth and time for +-- submodules with large history. +-- @param git_cmd string: name of git command. +-- @return boolean: Whether Git can fetch submodules shallowly. +local function git_supports_shallow_submodules(git_cmd) + return git_is_at_least(git_cmd, "1.8.4") +end + +--- Git >= 2.10 can fetch submodules shallowly according to .gitmodules configuration, allowing more granularity. +-- @param git_cmd string: name of git command. +-- @return boolean: Whether Git can fetch submodules shallowly according to .gitmodules. +local function git_supports_shallow_recommendations(git_cmd) + return git_is_at_least(git_cmd, "2.10.0") +end + +local function git_identifier(git_cmd, ver) + if not (ver:match("^dev%-%d+$") or ver:match("^scm%-%d+$")) then + return nil + end + local pd = io.popen(fs.command_at(fs.current_dir(), fs.Q(git_cmd).." log --pretty=format:%ai_%h -n 1")) + if not pd then + return nil + end + local date_hash = pd:read("*l") + pd:close() + if not date_hash then + return nil + end + local date, time, tz, hash = date_hash:match("([^%s]+) ([^%s]+) ([^%s]+)_([^%s]+)") + date = date:gsub("%-", "") + time = time:gsub(":", "") + return date .. "." .. time .. "." .. hash +end + +--- Download sources for building a rock, using git. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function git.get_sources(rockspec, extract, dest_dir, depth) + assert(rockspec:type() == "rockspec") + assert(type(dest_dir) == "string" or not dest_dir) + + local git_cmd = rockspec.variables.GIT + local name_version = rockspec.name .. "-" .. rockspec.version + local module = dir.base_name(rockspec.source.url) + -- Strip off .git from base name if present + module = module:gsub("%.git$", "") + + local ok, err_msg = fs.is_tool_available(git_cmd, "Git") + if not ok then + return nil, err_msg + end + + local store_dir + if not dest_dir then + store_dir = fs.make_temp_dir(name_version) + if not store_dir then + return nil, "Failed creating temporary directory." + end + util.schedule_function(fs.delete, store_dir) + else + store_dir = dest_dir + end + store_dir = fs.absolute_name(store_dir) + local ok, err = fs.change_dir(store_dir) + if not ok then return nil, err end + + local command = {fs.Q(git_cmd), "clone", "--recursive", depth or "--depth=1", rockspec.source.url, module} + local tag_or_branch = rockspec.source.tag or rockspec.source.branch + -- If the tag or branch is explicitly set to "master" in the rockspec, then + -- we can avoid passing it to Git since it's the default. + if tag_or_branch == "master" then tag_or_branch = nil end + if tag_or_branch then + if git_can_clone_by_tag(git_cmd) then + -- The argument to `--branch` can actually be a branch or a tag as of + -- Git 1.7.10. + table.insert(command, 3, "--branch=" .. tag_or_branch) + end + end + if not fs.execute(unpack(command)) then + return nil, "Failed cloning git repository." + end + ok, err = fs.change_dir(module) + if not ok then return nil, err end + if tag_or_branch and not git_can_clone_by_tag() then + if not fs.execute(fs.Q(git_cmd), "checkout", tag_or_branch) then + return nil, 'Failed to check out the "' .. tag_or_branch ..'" tag or branch.' + end + end + + -- Fetching git submodules is supported only when rockspec format is >= 3.0. + if rockspec:format_is_at_least("3.0") then + command = {fs.Q(git_cmd), "submodule", "update", "--init", "--recursive"} + + if git_supports_shallow_recommendations(git_cmd) then + table.insert(command, 5, "--recommend-shallow") + elseif git_supports_shallow_submodules(git_cmd) then + -- Fetch only the last commit of each submodule. + table.insert(command, 5, "--depth=1") + end + + if not fs.execute(unpack(command)) then + return nil, 'Failed to fetch submodules.' + end + end + + if not rockspec.source.tag then + rockspec.source.identifier = git_identifier(git_cmd, rockspec.version) + end + + fs.delete(dir.path(store_dir, module, ".git")) + fs.delete(dir.path(store_dir, module, ".gitignore")) + fs.pop_dir() + fs.pop_dir() + return module, store_dir +end + +return git diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_file.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_file.lua new file mode 100644 index 000000000..8d46bbca3 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_file.lua @@ -0,0 +1,19 @@ + +--- Fetch back-end for retrieving sources from local Git repositories. +local git_file = {} + +local git = require("luarocks.fetch.git") + +--- Fetch sources for building a rock from a local Git repository. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function git_file.get_sources(rockspec, extract, dest_dir) + rockspec.source.url = rockspec.source.url:gsub("^git.file://", "") + return git.get_sources(rockspec, extract, dest_dir) +end + +return git_file diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_http.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_http.lua new file mode 100644 index 000000000..d85e2572d --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_http.lua @@ -0,0 +1,26 @@ + +--- Fetch back-end for retrieving sources from Git repositories +-- that use http:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `git clone http://example.com/foo.git` +-- you can use this in the rockspec: +-- source = { url = "git+http://example.com/foo.git" } +-- Prefer using the normal git:// fetch mode as it is more widely +-- available in older versions of LuaRocks. +local git_http = {} + +local git = require("luarocks.fetch.git") + +--- Fetch sources for building a rock from a local Git repository. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function git_http.get_sources(rockspec, extract, dest_dir) + rockspec.source.url = rockspec.source.url:gsub("^git.", "") + return git.get_sources(rockspec, extract, dest_dir, "--") +end + +return git_http diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_https.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_https.lua new file mode 100644 index 000000000..67f8ad6cc --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_https.lua @@ -0,0 +1,7 @@ +--- Fetch back-end for retrieving sources from Git repositories +-- that use https:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `git clone https://example.com/foo.git` +-- you can use this in the rockspec: +-- source = { url = "git+https://example.com/foo.git" } +return require "luarocks.fetch.git_http" diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_ssh.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_ssh.lua new file mode 100644 index 000000000..0c2c0750f --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/git_ssh.lua @@ -0,0 +1,32 @@ +--- Fetch back-end for retrieving sources from Git repositories +-- that use ssh:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `git clone ssh://git@example.com/path/foo.git +-- you can use this in the rockspec: +-- source = { url = "git+ssh://git@example.com/path/foo.git" } +-- It also handles scp-style ssh urls: git@example.com:path/foo.git, +-- but you have to prepend the "git+ssh://" and why not use the "newer" +-- style anyway? +local git_ssh = {} + +local git = require("luarocks.fetch.git") + +--- Fetch sources for building a rock from a local Git repository. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function git_ssh.get_sources(rockspec, extract, dest_dir) + rockspec.source.url = rockspec.source.url:gsub("^git.", "") + + -- Handle old-style scp-like git ssh urls + if rockspec.source.url:match("^ssh://[^/]+:[^%d]") then + rockspec.source.url = rockspec.source.url:gsub("^ssh://", "") + end + + return git.get_sources(rockspec, extract, dest_dir, "--") +end + +return git_ssh diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg.lua new file mode 100644 index 000000000..0ef0f5e4a --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg.lua @@ -0,0 +1,65 @@ + +--- Fetch back-end for retrieving sources from HG. +local hg = {} + +local unpack = unpack or table.unpack + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") + +--- Download sources for building a rock, using hg. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function hg.get_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(dest_dir) == "string" or not dest_dir) + + local hg_cmd = rockspec.variables.HG + local ok, err_msg = fs.is_tool_available(hg_cmd, "Mercurial") + if not ok then + return nil, err_msg + end + + local name_version = rockspec.name .. "-" .. rockspec.version + -- Strip off special hg:// protocol type + local url = rockspec.source.url:gsub("^hg://", "") + + local module = dir.base_name(url) + + local command = {hg_cmd, "clone", url, module} + local tag_or_branch = rockspec.source.tag or rockspec.source.branch + if tag_or_branch then + command = {hg_cmd, "clone", "--rev", tag_or_branch, url, module} + end + local store_dir + if not dest_dir then + store_dir = fs.make_temp_dir(name_version) + if not store_dir then + return nil, "Failed creating temporary directory." + end + util.schedule_function(fs.delete, store_dir) + else + store_dir = dest_dir + end + local ok, err = fs.change_dir(store_dir) + if not ok then return nil, err end + if not fs.execute(unpack(command)) then + return nil, "Failed cloning hg repository." + end + ok, err = fs.change_dir(module) + if not ok then return nil, err end + + fs.delete(dir.path(store_dir, module, ".hg")) + fs.delete(dir.path(store_dir, module, ".hgignore")) + fs.pop_dir() + fs.pop_dir() + return module, store_dir +end + + +return hg diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg_http.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg_http.lua new file mode 100644 index 000000000..8f506daff --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg_http.lua @@ -0,0 +1,24 @@ + +--- Fetch back-end for retrieving sources from hg repositories +-- that use http:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `hg clone http://example.com/foo` +-- you can use this in the rockspec: +-- source = { url = "hg+http://example.com/foo" } +local hg_http = {} + +local hg = require("luarocks.fetch.hg") + +--- Download sources for building a rock, using hg over http. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function hg_http.get_sources(rockspec, extract, dest_dir) + rockspec.source.url = rockspec.source.url:gsub("^hg.", "") + return hg.get_sources(rockspec, extract, dest_dir) +end + +return hg_http diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg_https.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg_https.lua new file mode 100644 index 000000000..e67417fe8 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg_https.lua @@ -0,0 +1,8 @@ + +--- Fetch back-end for retrieving sources from hg repositories +-- that use https:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `hg clone https://example.com/foo` +-- you can use this in the rockspec: +-- source = { url = "hg+https://example.com/foo" } +return require "luarocks.fetch.hg_http" diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg_ssh.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg_ssh.lua new file mode 100644 index 000000000..0c365fabe --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/hg_ssh.lua @@ -0,0 +1,8 @@ + +--- Fetch back-end for retrieving sources from hg repositories +-- that use ssh:// transport. For example, for fetching a repository +-- that requires the following command line: +-- `hg clone ssh://example.com/foo` +-- you can use this in the rockspec: +-- source = { url = "hg+ssh://example.com/foo" } +return require "luarocks.fetch.hg_http" diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/sscm.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/sscm.lua new file mode 100644 index 000000000..32bb2eccf --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/sscm.lua @@ -0,0 +1,44 @@ + +--- Fetch back-end for retrieving sources from Surround SCM Server +local sscm = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") + +--- Download sources via Surround SCM Server for building a rock. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function sscm.get_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(dest_dir) == "string" or not dest_dir) + + local sscm_cmd = rockspec.variables.SSCM + local module = rockspec.source.module or dir.base_name(rockspec.source.url) + local branch, repository = string.match(rockspec.source.pathname, "^([^/]*)/(.*)") + if not branch or not repository then + return nil, "Error retrieving branch and repository from rockspec." + end + -- Search for working directory. + local working_dir + local tmp = io.popen(string.format(sscm_cmd..[[ property "/" -d -b%s -p%s]], branch, repository)) + for line in tmp:lines() do + --%c because a chr(13) comes in the end. + working_dir = string.match(line, "Working directory:[%s]*(.*)%c$") + if working_dir then break end + end + tmp:close() + if not working_dir then + return nil, "Error retrieving working directory from SSCM." + end + if not fs.execute(sscm_cmd, "get", "*", "-e" , "-r", "-b"..branch, "-p"..repository, "-tmodify", "-wreplace") then + return nil, "Failed fetching files from SSCM." + end + -- FIXME: This function does not honor the dest_dir parameter. + return module, working_dir +end + +return sscm diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/svn.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/svn.lua new file mode 100644 index 000000000..b6618afcf --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fetch/svn.lua @@ -0,0 +1,64 @@ + +--- Fetch back-end for retrieving sources from Subversion. +local svn = {} + +local unpack = unpack or table.unpack + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") + +--- Download sources for building a rock, using Subversion. +-- @param rockspec table: The rockspec table +-- @param extract boolean: Unused in this module (required for API purposes.) +-- @param dest_dir string or nil: If set, will extract to the given directory. +-- @return (string, string) or (nil, string): The absolute pathname of +-- the fetched source tarball and the temporary directory created to +-- store it; or nil and an error message. +function svn.get_sources(rockspec, extract, dest_dir) + assert(rockspec:type() == "rockspec") + assert(type(dest_dir) == "string" or not dest_dir) + + local svn_cmd = rockspec.variables.SVN + local ok, err_msg = fs.is_tool_available(svn_cmd, "Subversion") + if not ok then + return nil, err_msg + end + + local name_version = rockspec.name .. "-" .. rockspec.version + local module = rockspec.source.module or dir.base_name(rockspec.source.url) + local url = rockspec.source.url:gsub("^svn://", "") + local command = {svn_cmd, "checkout", url, module} + if rockspec.source.tag then + table.insert(command, 5, "-r") + table.insert(command, 6, rockspec.source.tag) + end + local store_dir + if not dest_dir then + store_dir = fs.make_temp_dir(name_version) + if not store_dir then + return nil, "Failed creating temporary directory." + end + util.schedule_function(fs.delete, store_dir) + else + store_dir = dest_dir + end + local ok, err = fs.change_dir(store_dir) + if not ok then return nil, err end + if not fs.execute(unpack(command)) then + return nil, "Failed fetching files from Subversion." + end + ok, err = fs.change_dir(module) + if not ok then return nil, err end + for _, d in ipairs(fs.find(".")) do + if dir.base_name(d) == ".svn" then + fs.delete(dir.path(store_dir, module, d)) + end + end + fs.pop_dir() + fs.pop_dir() + return module, store_dir +end + + +return svn diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fs.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fs.lua new file mode 100644 index 000000000..5df87d442 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fs.lua @@ -0,0 +1,140 @@ + +--- Proxy module for filesystem and platform abstractions. +-- All code using "fs" code should require "luarocks.fs", +-- and not the various platform-specific implementations. +-- However, see the documentation of the implementation +-- for the API reference. + +local pairs = pairs + +local fs = {} +-- To avoid a loop when loading the other fs modules. +package.loaded["luarocks.fs"] = fs + +local cfg = require("luarocks.core.cfg") + +local pack = table.pack or function(...) return { n = select("#", ...), ... } end +local unpack = table.unpack or unpack + +math.randomseed(os.time()) + +local fs_is_verbose = false + +do + local old_popen, old_execute + + -- patch io.popen and os.execute to display commands in verbose mode + function fs.verbose() + fs_is_verbose = true + + if old_popen or old_execute then return end + old_popen = io.popen + -- luacheck: push globals io os + io.popen = function(one, two) + if two == nil then + print("\nio.popen: ", one) + return old_popen(one) + end + print("\nio.popen: ", one, "Mode:", two) + return old_popen(one, two) + end + + old_execute = os.execute + os.execute = function(cmd) + -- redact api keys if present + print("\nos.execute: ", (cmd:gsub("(/api/[^/]+/)([^/]+)/", function(cap, key) return cap.."/" end)) ) + local code = pack(old_execute(cmd)) + print("Results: "..tostring(code.n)) + for i = 1,code.n do + print(" "..tostring(i).." ("..type(code[i]).."): "..tostring(code[i])) + end + return unpack(code, 1, code.n) + end + -- luacheck: pop + end +end + +do + local function load_fns(fs_table, inits) + for name, fn in pairs(fs_table) do + if name ~= "init" and not fs[name] then + fs[name] = function(...) + if fs_is_verbose then + local args = pack(...) + for i=1, args.n do + local arg = args[i] + local pok, v = pcall(string.format, "%q", arg) + args[i] = pok and v or tostring(arg) + end + print("fs." .. name .. "(" .. table.concat(args, ", ") .. ")") + end + return fn(...) + end + end + end + if fs_table.init then + table.insert(inits, fs_table.init) + end + end + + local function load_platform_fns(patt, inits) + local each_platform = cfg.each_platform + + -- FIXME A quick hack for the experimental Windows build + if os.getenv("LUAROCKS_CROSS_COMPILING") then + each_platform = function() + local i = 0 + local plats = { "linux", "unix" } + return function() + i = i + 1 + return plats[i] + end + end + end + + for platform in each_platform("most-specific-first") do + local ok, fs_plat = pcall(require, patt:format(platform)) + if ok and fs_plat then + load_fns(fs_plat, inits) + end + end + end + + function fs.init() + local inits = {} + + if fs.current_dir then + -- unload luarocks fs so it can be reloaded using all modules + -- providing extra functionality in the current package paths + for k, _ in pairs(fs) do + if k ~= "init" and k ~= "verbose" then + fs[k] = nil + end + end + for m, _ in pairs(package.loaded) do + if m:match("luarocks%.fs%.") then + package.loaded[m] = nil + end + end + end + + -- Load platform-specific functions + load_platform_fns("luarocks.fs.%s", inits) + + -- Load platform-independent pure-Lua functionality + load_fns(require("luarocks.fs.lua"), inits) + + -- Load platform-specific fallbacks for missing Lua modules + load_platform_fns("luarocks.fs.%s.tools", inits) + + -- Load platform-independent external tool functionality + load_fns(require("luarocks.fs.tools"), inits) + + -- Run platform-specific initializations after everything is loaded + for _, init in ipairs(inits) do + init() + end + end +end + +return fs diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fs/lua.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/lua.lua new file mode 100644 index 000000000..178071a47 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/lua.lua @@ -0,0 +1,1229 @@ + +--- Native Lua implementation of filesystem and platform abstractions, +-- using LuaFileSystem, LuaSocket, LuaSec, lua-zlib, LuaPosix, MD5. +-- module("luarocks.fs.lua") +local fs_lua = {} + +local fs = require("luarocks.fs") + +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local util = require("luarocks.util") + +local pack = table.pack or function(...) return { n = select("#", ...), ... } end + +local socket_ok, zip_ok, lfs_ok, md5_ok, digest_ok, posix_ok, bz2_ok, _ +local http, ftp, zip, lfs, md5, digest, posix, bz2 + +if cfg.fs_use_modules then + socket_ok, http = pcall(require, "socket.http") + _, ftp = pcall(require, "socket.ftp") + zip_ok, zip = pcall(require, "luarocks.tools.zip") + bz2_ok, bz2 = pcall(require, "bz2") + lfs_ok, lfs = pcall(require, "lfs") + md5_ok, md5 = pcall(require, "md5") + digest_ok, digest = pcall(require, "digest") + posix_ok, posix = pcall(require, "posix") +end + +local patch = require("luarocks.tools.patch") +local tar = require("luarocks.tools.tar") + +local dir_stack = {} + +--- Test is file/dir is writable. +-- Warning: testing if a file/dir is writable does not guarantee +-- that it will remain writable and therefore it is no replacement +-- for checking the result of subsequent operations. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function fs_lua.is_writable(file) + assert(file) + file = dir.normalize(file) + local result + if fs.is_dir(file) then + local file2 = dir.path(file, '.tmpluarockstestwritable') + local fh = io.open(file2, 'wb') + result = fh ~= nil + if fh then fh:close() end + os.remove(file2) + else + local fh = io.open(file, 'r+b') + result = fh ~= nil + if fh then fh:close() end + end + return result +end + +local function quote_args(command, ...) + local out = { command } + local args = pack(...) + for i=1, args.n do + local arg = args[i] + assert(type(arg) == "string") + out[#out+1] = fs.Q(arg) + end + return table.concat(out, " ") +end + +--- Run the given command, quoting its arguments. +-- The command is executed in the current directory in the dir stack. +-- @param command string: The command to be executed. No quoting/escaping +-- is applied. +-- @param ... Strings containing additional arguments, which are quoted. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function fs_lua.execute(command, ...) + assert(type(command) == "string") + return fs.execute_string(quote_args(command, ...)) +end + +--- Run the given command, quoting its arguments, silencing its output. +-- The command is executed in the current directory in the dir stack. +-- Silencing is omitted if 'verbose' mode is enabled. +-- @param command string: The command to be executed. No quoting/escaping +-- is applied. +-- @param ... Strings containing additional arguments, which will be quoted. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function fs_lua.execute_quiet(command, ...) + assert(type(command) == "string") + if cfg.verbose then -- omit silencing output + return fs.execute_string(quote_args(command, ...)) + else + return fs.execute_string(fs.quiet(quote_args(command, ...))) + end +end + +function fs.execute_env(env, command, ...) + assert(type(command) == "string") + local envstr = {} + for var, val in pairs(env) do + table.insert(envstr, fs.export_cmd(var, val)) + end + return fs.execute_string(table.concat(envstr, "\n") .. "\n" .. quote_args(command, ...)) +end + +local tool_available_cache = {} + +function fs_lua.set_tool_available(tool_name, value) + assert(type(value) == "boolean") + tool_available_cache[tool_name] = value +end + +--- Checks if the given tool is available. +-- The tool is executed using a flag, usually just to ask its version. +-- @param tool_cmd string: The command to be used to check the tool's presence (e.g. hg in case of Mercurial) +-- @param tool_name string: The actual name of the tool (e.g. Mercurial) +function fs_lua.is_tool_available(tool_cmd, tool_name) + assert(type(tool_cmd) == "string") + assert(type(tool_name) == "string") + + local ok + if tool_available_cache[tool_name] ~= nil then + ok = tool_available_cache[tool_name] + else + local tool_cmd_no_args = tool_cmd:gsub(" [^\"]*$", "") + + -- if it looks like the tool has a pathname, try that first + if tool_cmd_no_args:match("[/\\]") then + local fd = io.open(tool_cmd_no_args, "r") + if fd then + fd:close() + ok = true + end + end + + if not ok then + ok = fs.search_in_path(tool_cmd_no_args) + end + + tool_available_cache[tool_name] = (ok == true) + end + + if ok then + return true + else + local msg = "'%s' program not found. Make sure %s is installed and is available in your PATH " .. + "(or you may want to edit the 'variables.%s' value in file '%s')" + return nil, msg:format(tool_cmd, tool_name, tool_name:upper(), cfg.config_files.nearest) + end +end + +--- Check the MD5 checksum for a file. +-- @param file string: The file to be checked. +-- @param md5sum string: The string with the expected MD5 checksum. +-- @return boolean: true if the MD5 checksum for 'file' equals 'md5sum', false + msg if not +-- or if it could not perform the check for any reason. +function fs_lua.check_md5(file, md5sum) + file = dir.normalize(file) + local computed, msg = fs.get_md5(file) + if not computed then + return false, msg + end + if computed:match("^"..md5sum) then + return true + else + return false, "Mismatch MD5 hash for file "..file + end +end + +--- List the contents of a directory. +-- @param at string or nil: directory to list (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. +function fs_lua.list_dir(at) + local result = {} + for file in fs.dir(at) do + result[#result+1] = file + end + return result +end + +--- Iterate over the contents of a directory. +-- @param at string or nil: directory to list (will be the current +-- directory if none is given). +-- @return function: an iterator function suitable for use with +-- the for statement. +function fs_lua.dir(at) + if not at then + at = fs.current_dir() + end + at = dir.normalize(at) + if not fs.is_dir(at) then + return function() end + end + return coroutine.wrap(function() fs.dir_iterator(at) end) +end + +--- List the Lua modules at a specific require path. +-- eg. `modules("luarocks.cmd")` would return a list of all LuaRocks command +-- modules, in the current Lua path. +function fs_lua.modules(at) + at = at or "" + if #at > 0 then + -- turn require path into file path + at = at:gsub("%.", package.config:sub(1,1)) .. package.config:sub(1,1) + end + + local path = package.path:sub(-1, -1) == ";" and package.path or package.path .. ";" + local paths = {} + for location in path:gmatch("(.-);") do + if location:lower() == "?.lua" then + location = "./?.lua" + end + local _, q_count = location:gsub("%?", "") -- only use the ones with a single '?' + if location:match("%?%.[lL][uU][aA]$") and q_count == 1 then -- only use when ending with "?.lua" + location = location:gsub("%?%.[lL][uU][aA]$", at) + table.insert(paths, location) + end + end + + if #paths == 0 then + return {} + end + + local modules = {} + local is_duplicate = {} + for _, path in ipairs(paths) do -- luacheck: ignore 421 + local files = fs.list_dir(path) + for _, filename in ipairs(files or {}) do + if filename:match("%.[lL][uU][aA]$") then + filename = filename:sub(1,-5) -- drop the extension + if not is_duplicate[filename] then + is_duplicate[filename] = true + table.insert(modules, filename) + end + end + end + end + + return modules +end + +function fs_lua.filter_file(fn, input_filename, output_filename) + local fd, err = io.open(input_filename, "rb") + if not fd then + return nil, err + end + + local input, err = fd:read("*a") + fd:close() + if not input then + return nil, err + end + + local output, err = fn(input) + if not output then + return nil, err + end + + fd, err = io.open(output_filename, "wb") + if not fd then + return nil, err + end + + local ok, err = fd:write(output) + fd:close() + if not ok then + return nil, err + end + + return true +end + +function fs_lua.system_temp_dir() + return os.getenv("TMPDIR") or os.getenv("TEMP") or "/tmp" +end + +--------------------------------------------------------------------- +-- LuaFileSystem functions +--------------------------------------------------------------------- + +if lfs_ok then + +--- Run the given command. +-- The command is executed in the current directory in the dir stack. +-- @param cmd string: No quoting/escaping is applied to the command. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function fs_lua.execute_string(cmd) + local code = os.execute(cmd) + return (code == 0 or code == true) +end + +--- Obtain current directory. +-- Uses the module's internal dir stack. +-- @return string: the absolute pathname of the current directory. +function fs_lua.current_dir() + return lfs.currentdir() +end + +--- Change the current directory. +-- Uses the module's internal dir stack. This does not have exact +-- semantics of chdir, as it does not handle errors the same way, +-- but works well for our purposes for now. +-- @param d string: The directory to switch to. +function fs_lua.change_dir(d) + table.insert(dir_stack, lfs.currentdir()) + d = dir.normalize(d) + return lfs.chdir(d) +end + +--- Change directory to root. +-- Allows leaving a directory (e.g. for deleting it) in +-- a crossplatform way. +function fs_lua.change_dir_to_root() + local current = lfs.currentdir() + if not current or current == "" then + return false + end + table.insert(dir_stack, current) + lfs.chdir("/") -- works on Windows too + return true +end + +--- Change working directory to the previous in the dir stack. +-- @return true if a pop occurred, false if the stack was empty. +function fs_lua.pop_dir() + local d = table.remove(dir_stack) + if d then + lfs.chdir(d) + return true + else + return false + end +end + +--- Create a directory if it does not already exist. +-- If any of the higher levels in the path name do not exist +-- too, they are created as well. +-- @param directory string: pathname of directory to create. +-- @return boolean or (boolean, string): true on success or (false, error message) on failure. +function fs_lua.make_dir(directory) + assert(type(directory) == "string") + directory = dir.normalize(directory) + local path = nil + if directory:sub(2, 2) == ":" then + path = directory:sub(1, 2) + directory = directory:sub(4) + else + if directory:match("^/") then + path = "" + end + end + for d in directory:gmatch("([^/]+)/*") do + path = path and path .. "/" .. d or d + local mode = lfs.attributes(path, "mode") + if not mode then + local ok, err = lfs.mkdir(path) + if not ok then + return false, err + end + ok, err = fs.set_permissions(path, "exec", "all") + if not ok then + return false, err + end + elseif mode ~= "directory" then + return false, path.." is not a directory" + end + end + return true +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param d string: pathname of directory to remove. +function fs_lua.remove_dir_if_empty(d) + assert(d) + d = dir.normalize(d) + lfs.rmdir(d) +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param d string: pathname of directory to remove. +function fs_lua.remove_dir_tree_if_empty(d) + assert(d) + d = dir.normalize(d) + for i=1,10 do + lfs.rmdir(d) + d = dir.dir_name(d) + end +end + +local function are_the_same_file(f1, f2) + if f1 == f2 then + return true + end + if cfg.is_platform("unix") then + local i1 = lfs.attributes(f1, "ino") + local i2 = lfs.attributes(f2, "ino") + if i1 ~= nil and i1 == i2 then + return true + end + end + return false +end + +--- Copy a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perms string ("read" or "exec") or nil: Permissions for destination +-- file or nil to use the source file permissions +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.copy(src, dest, perms) + assert(src and dest) + src = dir.normalize(src) + dest = dir.normalize(dest) + local destmode = lfs.attributes(dest, "mode") + if destmode == "directory" then + dest = dir.path(dest, dir.base_name(src)) + end + if are_the_same_file(src, dest) then + return nil, "The source and destination are the same files" + end + local src_h, err = io.open(src, "rb") + if not src_h then return nil, err end + local dest_h, err = io.open(dest, "w+b") + if not dest_h then src_h:close() return nil, err end + while true do + local block = src_h:read(8192) + if not block then break end + dest_h:write(block) + end + src_h:close() + dest_h:close() + + local fullattrs + if not perms then + fullattrs = lfs.attributes(src, "permissions") + end + if fullattrs and posix_ok then + return posix.chmod(dest, fullattrs) + else + if not perms then + perms = fullattrs:match("x") and "exec" or "read" + end + return fs.set_permissions(dest, perms, "all") + end +end + +--- Implementation function for recursive copy of directory contents. +-- Assumes paths are normalized. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perms string ("read" or "exec") or nil: Optional permissions. +-- If not given, permissions of the source are copied over to the destination. +-- @return boolean or (boolean, string): true on success, false on failure +local function recursive_copy(src, dest, perms) + local srcmode = lfs.attributes(src, "mode") + + if srcmode == "file" then + local ok = fs.copy(src, dest, perms) + if not ok then return false end + elseif srcmode == "directory" then + local subdir = dir.path(dest, dir.base_name(src)) + local ok, err = fs.make_dir(subdir) + if not ok then return nil, err end + if pcall(lfs.dir, src) == false then + return false + end + for file in lfs.dir(src) do + if file ~= "." and file ~= ".." then + local ok = recursive_copy(dir.path(src, file), subdir, perms) + if not ok then return false end + end + end + end + return true +end + +--- Recursively copy the contents of a directory. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perms string ("read" or "exec") or nil: Optional permissions. +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.copy_contents(src, dest, perms) + assert(src and dest) + src = dir.normalize(src) + dest = dir.normalize(dest) + if not fs.is_dir(src) then + return false, src .. " is not a directory" + end + if pcall(lfs.dir, src) == false then + return false, "Permission denied" + end + for file in lfs.dir(src) do + if file ~= "." and file ~= ".." then + local ok = recursive_copy(dir.path(src, file), dest, perms) + if not ok then + return false, "Failed copying "..src.." to "..dest + end + end + end + return true +end + +--- Implementation function for recursive removal of directories. +-- Assumes paths are normalized. +-- @param name string: Pathname of file +-- @return boolean or (boolean, string): true on success, +-- or nil and an error message on failure. +local function recursive_delete(name) + local ok = os.remove(name) + if ok then return true end + local pok, ok, err = pcall(function() + for file in lfs.dir(name) do + if file ~= "." and file ~= ".." then + local ok, err = recursive_delete(dir.path(name, file)) + if not ok then return nil, err end + end + end + local ok, err = lfs.rmdir(name) + return ok, (not ok) and err + end) + if pok then + return ok, err + else + return pok, ok + end +end + +--- Delete a file or a directory and all its contents. +-- @param name string: Pathname of source +-- @return nil +function fs_lua.delete(name) + name = dir.normalize(name) + recursive_delete(name) +end + +--- Internal implementation function for fs.dir. +-- Yields a filename on each iteration. +-- @param at string: directory to list +-- @return nil or (nil and string): an error message on failure +function fs_lua.dir_iterator(at) + local pok, iter, arg = pcall(lfs.dir, at) + if not pok then + return nil, iter + end + for file in iter, arg do + if file ~= "." and file ~= ".." then + coroutine.yield(file) + end + end +end + +--- Implementation function for recursive find. +-- Assumes paths are normalized. +-- @param cwd string: Current working directory in recursion. +-- @param prefix string: Auxiliary prefix string to form pathname. +-- @param result table: Array of strings where results are collected. +local function recursive_find(cwd, prefix, result) + local pok, iter, arg = pcall(lfs.dir, cwd) + if not pok then + return nil + end + for file in iter, arg do + if file ~= "." and file ~= ".." then + local item = prefix .. file + table.insert(result, item) + local pathname = dir.path(cwd, file) + if lfs.attributes(pathname, "mode") == "directory" then + recursive_find(pathname, item.."/", result) + end + end + end +end + +--- Recursively scan the contents of a directory. +-- @param at string or nil: directory to scan (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. +function fs_lua.find(at) + assert(type(at) == "string" or not at) + if not at then + at = fs.current_dir() + end + at = dir.normalize(at) + local result = {} + recursive_find(at, "", result) + return result +end + +--- Test for existence of a file. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function fs_lua.exists(file) + assert(file) + file = dir.normalize(file) + return type(lfs.attributes(file)) == "table" +end + +--- Test is pathname is a directory. +-- @param file string: pathname to test +-- @return boolean: true if it is a directory, false otherwise. +function fs_lua.is_dir(file) + assert(file) + file = dir.normalize(file) + return lfs.attributes(file, "mode") == "directory" +end + +--- Test is pathname is a regular file. +-- @param file string: pathname to test +-- @return boolean: true if it is a file, false otherwise. +function fs_lua.is_file(file) + assert(file) + file = dir.normalize(file) + return lfs.attributes(file, "mode") == "file" +end + +-- Set access and modification times for a file. +-- @param filename File to set access and modification times for. +-- @param time may be a number containing the format returned +-- by os.time, or a table ready to be processed via os.time; if +-- nil, current time is assumed. +function fs_lua.set_time(file, time) + assert(time == nil or type(time) == "table" or type(time) == "number") + file = dir.normalize(file) + if type(time) == "table" then + time = os.time(time) + end + return lfs.touch(file, time) +end + +else -- if not lfs_ok + +function fs_lua.exists(file) + assert(file) + file = dir.normalize(fs.absolute_name(file)) + -- check if file exists by attempting to open it + return util.exists(file) +end + +end + +--------------------------------------------------------------------- +-- lua-bz2 functions +--------------------------------------------------------------------- + +if bz2_ok then + +local function bunzip2_string(data) + local decompressor = bz2.initDecompress() + local output, err = decompressor:update(data) + if not output then + return nil, err + end + decompressor:close() + return output +end + +--- Uncompresses a .bz2 file. +-- @param infile string: pathname of .bz2 file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function fs_lua.bunzip2(infile, outfile) + assert(type(infile) == "string") + assert(outfile == nil or type(outfile) == "string") + if not outfile then + outfile = infile:gsub("%.bz2$", "") + end + + return fs.filter_file(bunzip2_string, infile, outfile) +end + +end + +--------------------------------------------------------------------- +-- luarocks.tools.zip functions +--------------------------------------------------------------------- + +if zip_ok then + +function fs_lua.zip(zipfile, ...) + return zip.zip(zipfile, ...) +end + +function fs_lua.unzip(zipfile) + return zip.unzip(zipfile) +end + +function fs_lua.gunzip(infile, outfile) + return zip.gunzip(infile, outfile) +end + +end + +--------------------------------------------------------------------- +-- LuaSocket functions +--------------------------------------------------------------------- + +if socket_ok then + +local ltn12 = require("ltn12") +local luasec_ok, https = pcall(require, "ssl.https") + +local redirect_protocols = { + http = http, + https = luasec_ok and https, +} + +local function request(url, method, http, loop_control) -- luacheck: ignore 431 + local result = {} + + if cfg.verbose then + print(method, url) + end + + local proxy = os.getenv("http_proxy") + if type(proxy) ~= "string" then proxy = nil end + -- LuaSocket's http.request crashes when given URLs missing the scheme part. + if proxy and not proxy:find("://") then + proxy = "http://" .. proxy + end + + if cfg.show_downloads then + io.write(method.." "..url.." ...\n") + end + local dots = 0 + if cfg.connection_timeout and cfg.connection_timeout > 0 then + http.TIMEOUT = cfg.connection_timeout + end + local res, status, headers, err = http.request { + url = url, + proxy = proxy, + method = method, + redirect = false, + sink = ltn12.sink.table(result), + step = cfg.show_downloads and function(...) + io.write(".") + io.flush() + dots = dots + 1 + if dots == 70 then + io.write("\n") + dots = 0 + end + return ltn12.pump.step(...) + end, + headers = { + ["user-agent"] = cfg.user_agent.." via LuaSocket" + }, + } + if cfg.show_downloads then + io.write("\n") + end + if not res then + return nil, status + elseif status == 301 or status == 302 then + local location = headers.location + if location then + local protocol, rest = dir.split_url(location) + if redirect_protocols[protocol] then + if not loop_control then + loop_control = {} + elseif loop_control[location] then + return nil, "Redirection loop -- broken URL?" + end + loop_control[url] = true + return request(location, method, redirect_protocols[protocol], loop_control) + else + return nil, "URL redirected to unsupported protocol - install luasec to get HTTPS support.", "https" + end + end + return nil, err + elseif status ~= 200 then + return nil, err + else + return result, status, headers, err + end +end + +local function write_timestamp(filename, data) + local fd = io.open(filename, "w") + if fd then + fd:write(data) + fd:close() + end +end + +local function read_timestamp(filename) + local fd = io.open(filename, "r") + if fd then + local data = fd:read("*a") + fd:close() + return data + end +end + +local function fail_with_status(filename, status, headers) + write_timestamp(filename .. ".unixtime", os.time()) + write_timestamp(filename .. ".status", status) + return nil, status, headers +end + +-- @param url string: URL to fetch. +-- @param filename string: local filename of the file to fetch. +-- @param http table: The library to use (http from LuaSocket or LuaSec) +-- @param cache boolean: Whether to use a `.timestamp` file to check +-- via the HTTP Last-Modified header if the full download is needed. +-- @return (boolean | (nil, string, string?)): True if successful, or +-- nil, error message and optionally HTTPS error in case of errors. +local function http_request(url, filename, http, cache) -- luacheck: ignore 431 + if cache then + local status = read_timestamp(filename..".status") + local timestamp = read_timestamp(filename..".timestamp") + if status or timestamp then + local unixtime = read_timestamp(filename..".unixtime") + if tonumber(unixtime) then + local diff = os.time() - tonumber(unixtime) + if status then + if diff < cfg.cache_fail_timeout then + return nil, status, {} + end + else + if diff < cfg.cache_timeout then + return true, nil, nil, true + end + end + end + + local result, status, headers, err = request(url, "HEAD", http) -- luacheck: ignore 421 + if not result then + return fail_with_status(filename, status, headers) + end + if status == 200 and headers["last-modified"] == timestamp then + write_timestamp(filename .. ".unixtime", os.time()) + return true, nil, nil, true + end + end + end + local result, status, headers, err = request(url, "GET", http) + if not result then + if status then + return fail_with_status(filename, status, headers) + end + end + if cache and headers["last-modified"] then + write_timestamp(filename .. ".timestamp", headers["last-modified"]) + write_timestamp(filename .. ".unixtime", os.time()) + end + local file = io.open(filename, "wb") + if not file then return nil, 0, {} end + for _, data in ipairs(result) do + file:write(data) + end + file:close() + return true +end + +local function ftp_request(url, filename) + local content, err = ftp.get(url) + if not content then + return false, err + end + local file = io.open(filename, "wb") + if not file then return false, err end + file:write(content) + file:close() + return true +end + +local downloader_warning = false + +--- Download a remote file. +-- @param url string: URL to be fetched. +-- @param filename string or nil: this function attempts to detect the +-- resulting local filename of the remote file as the basename of the URL; +-- if that is not correct (due to a redirection, for example), the local +-- filename can be given explicitly as this second argument. +-- @return (boolean, string, boolean): +-- In case of success: +-- * true +-- * a string with the filename +-- * true if the file was retrieved from local cache +-- In case of failure: +-- * false +-- * error message +function fs_lua.download(url, filename, cache) + assert(type(url) == "string") + assert(type(filename) == "string" or not filename) + + filename = fs.absolute_name(filename or dir.base_name(url)) + + -- delegate to the configured downloader so we don't have to deal with whitelists + if os.getenv("no_proxy") then + return fs.use_downloader(url, filename, cache) + end + + local ok, err, https_err, from_cache + if util.starts_with(url, "http:") then + ok, err, https_err, from_cache = http_request(url, filename, http, cache) + elseif util.starts_with(url, "ftp:") then + ok, err = ftp_request(url, filename) + elseif util.starts_with(url, "https:") then + -- skip LuaSec when proxy is enabled since it is not supported + if luasec_ok and not os.getenv("https_proxy") then + local _ + ok, err, _, from_cache = http_request(url, filename, https, cache) + else + https_err = true + end + else + err = "Unsupported protocol" + end + if https_err then + local downloader, err = fs.which_tool("downloader") + if not downloader then + return nil, err + end + if not downloader_warning then + util.warning("falling back to "..downloader.." - install luasec to get native HTTPS support") + downloader_warning = true + end + return fs.use_downloader(url, filename, cache) + elseif not ok then + return nil, err + end + return true, filename, from_cache +end + +else --...if socket_ok == false then + +function fs_lua.download(url, filename, cache) + return fs.use_downloader(url, filename, cache) +end + +end +--------------------------------------------------------------------- +-- MD5 functions +--------------------------------------------------------------------- + +local md5_hex = nil + +if digest_ok then + md5_hex = digest.md5_hex +elseif md5_ok then + md5_hex = md5.sumhexa + --- Support the interface of lmd5 by lhf in addition to md5 by Roberto + --- and the keplerproject. + if not md5_hex and md5.digest then + md5_hex = md5.digest + end +end + +if md5_hex then + +--- Get the MD5 checksum for a file. +-- @param file string: The file to be computed. +-- @return string: The MD5 checksum or nil + error +function fs_lua.get_md5(file) + file = fs.absolute_name(file) + local file_handler = io.open(file, "rb") + if not file_handler then return nil, "Failed to open file for reading: "..file end + local computed = md5_hex(file_handler:read("*a")) + file_handler:close() + if computed then return computed end + return nil, "Failed to compute MD5 hash for file "..file +end + +end + +--------------------------------------------------------------------- +-- POSIX functions +--------------------------------------------------------------------- + +function fs_lua._unix_rwx_to_number(rwx, neg) + local num = 0 + neg = neg or false + for i = 1, 9 do + local c = rwx:sub(10 - i, 10 - i) == "-" + if neg == c then + num = num + 2^(i-1) + end + end + return math.floor(num) +end + +if posix_ok then + +local octal_to_rwx = { + ["0"] = "---", + ["1"] = "--x", + ["2"] = "-w-", + ["3"] = "-wx", + ["4"] = "r--", + ["5"] = "r-x", + ["6"] = "rw-", + ["7"] = "rwx", +} + +do + local umask_cache + function fs_lua._unix_umask() + if umask_cache then + return umask_cache + end + -- LuaPosix (as of 34.0.4) only returns the umask as rwx + local rwx = posix.umask() + local num = fs_lua._unix_rwx_to_number(rwx, true) + umask_cache = ("%03o"):format(num) + return umask_cache + end +end + +function fs_lua.set_permissions(filename, mode, scope) + local perms + if mode == "read" and scope == "user" then + perms = fs._unix_moderate_permissions("600") + elseif mode == "exec" and scope == "user" then + perms = fs._unix_moderate_permissions("700") + elseif mode == "read" and scope == "all" then + perms = fs._unix_moderate_permissions("644") + elseif mode == "exec" and scope == "all" then + perms = fs._unix_moderate_permissions("755") + else + return false, "Invalid permission " .. mode .. " for " .. scope + end + + -- LuaPosix (as of 5.1.15) does not support octal notation... + local new_perms = {} + for c in perms:sub(-3):gmatch(".") do + table.insert(new_perms, octal_to_rwx[c]) + end + perms = table.concat(new_perms) + local err = posix.chmod(filename, perms) + return err == 0 +end + +function fs_lua.current_user() + return posix.getpwuid(posix.geteuid()).pw_name +end + +function fs_lua.is_superuser() + return posix.geteuid() == 0 +end + +-- This call is not available on all systems, see #677 +if posix.mkdtemp then + +--- Create a temporary directory. +-- @param name_pattern string: name pattern to use for avoiding conflicts +-- when creating temporary directory. +-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure. +function fs_lua.make_temp_dir(name_pattern) + assert(type(name_pattern) == "string") + name_pattern = dir.normalize(name_pattern) + + return posix.mkdtemp(fs.system_temp_dir() .. "/luarocks_" .. name_pattern:gsub("/", "_") .. "-XXXXXX") +end + +end -- if posix.mkdtemp + +end + +--------------------------------------------------------------------- +-- Other functions +--------------------------------------------------------------------- + +if lfs_ok and not fs_lua.make_temp_dir then + +function fs_lua.make_temp_dir(name_pattern) + assert(type(name_pattern) == "string") + name_pattern = dir.normalize(name_pattern) + + local pattern = fs.system_temp_dir() .. "/luarocks_" .. name_pattern:gsub("/", "_") .. "-" + + while true do + local name = pattern .. tostring(math.random(10000000)) + if lfs.mkdir(name) then + return name + end + end +end + +end + +--- Apply a patch. +-- @param patchname string: The filename of the patch. +-- @param patchdata string or nil: The actual patch as a string. +-- @param create_delete boolean: Support creating and deleting files in a patch. +function fs_lua.apply_patch(patchname, patchdata, create_delete) + local p, all_ok = patch.read_patch(patchname, patchdata) + if not all_ok then + return nil, "Failed reading patch "..patchname + end + if p then + return patch.apply_patch(p, 1, create_delete) + end +end + +--- Move a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perms string ("read" or "exec") or nil: Permissions for destination +-- file or nil to use the source file permissions. +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.move(src, dest, perms) + assert(src and dest) + if fs.exists(dest) and not fs.is_dir(dest) then + return false, "File already exists: "..dest + end + local ok, err = fs.copy(src, dest, perms) + if not ok then + return false, err + end + fs.delete(src) + if fs.exists(src) then + return false, "Failed move: could not delete "..src.." after copy." + end + return true +end + +--- Check if user has write permissions for the command. +-- Assumes the configuration variables under cfg have been previously set up. +-- @param args table: the args table passed to run() drivers. +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function fs_lua.check_command_permissions(args) + local ok = true + local err = "" + for _, directory in ipairs { cfg.rocks_dir, cfg.deploy_lua_dir, cfg.deploy_bin_dir, cfg.deploy_lua_dir } do + if fs.exists(directory) then + if not fs.is_writable(directory) then + ok = false + err = "Your user does not have write permissions in " .. directory + break + end + else + local root = fs.root_of(directory) + local parent = directory + repeat + parent = dir.dir_name(parent) + if parent == "" then + parent = root + end + until parent == root or fs.exists(parent) + if not fs.is_writable(parent) then + ok = false + err = directory.." does not exist and your user does not have write permissions in " .. parent + break + end + end + end + if ok then + return true + else + if args["local"] or cfg.local_by_default then + err = err .. " \n-- please check your permissions." + else + err = err .. " \n-- you may want to run as a privileged user or use your local tree with --local." + end + return nil, err + end +end + +--- Check whether a file is a Lua script +-- When the file can be successfully compiled by the configured +-- Lua interpreter, it's considered to be a valid Lua file. +-- @param filename filename of file to check +-- @return boolean true, if it is a Lua script, false otherwise +function fs_lua.is_lua(filename) + filename = filename:gsub([[%\]],"/") -- normalize on fw slash to prevent escaping issues + local lua = fs.Q(dir.path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter)) -- get lua interpreter configured + -- execute on configured interpreter, might not be the same as the interpreter LR is run on + local result = fs.execute_string(lua..[[ -e "if loadfile(']]..filename..[[') then os.exit(0) else os.exit(1) end"]]) + return (result == true) +end + +--- Unpack an archive. +-- Extract the contents of an archive, detecting its format by +-- filename extension. +-- @param archive string: Filename of archive. +-- @return boolean or (boolean, string): true on success, false and an error message on failure. +function fs_lua.unpack_archive(archive) + assert(type(archive) == "string") + + local ok, err + archive = fs.absolute_name(archive) + if archive:match("%.tar%.gz$") then + local tar_filename = archive:gsub("%.gz$", "") + ok, err = fs.gunzip(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.tgz$") then + local tar_filename = archive:gsub("%.tgz$", ".tar") + ok, err = fs.gunzip(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.tar%.bz2$") then + local tar_filename = archive:gsub("%.bz2$", "") + ok, err = fs.bunzip2(archive, tar_filename) + if ok then + ok, err = tar.untar(tar_filename, ".") + end + elseif archive:match("%.zip$") then + ok, err = fs.unzip(archive) + elseif archive:match("%.lua$") or archive:match("%.c$") then + -- Ignore .lua and .c files; they don't need to be extracted. + return true + else + return false, "Couldn't extract archive "..archive..": unrecognized filename extension" + end + if not ok then + return false, "Failed extracting "..archive..": "..err + end + return true +end + +return fs_lua diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fs/macosx.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/macosx.lua new file mode 100644 index 000000000..b71e7f12b --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/macosx.lua @@ -0,0 +1,50 @@ +--- macOS-specific implementation of filesystem and platform abstractions. +local macosx = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") + +function macosx.is_dir(file) + file = fs.absolute_name(file) + file = dir.normalize(file) .. "/." + local fd, _, code = io.open(file, "r") + if code == 2 then -- "No such file or directory" + return false + end + if code == 20 then -- "Not a directory", regardless of permissions + return false + end + if code == 13 then -- "Permission denied", but is a directory + return true + end + if fd then + local _, _, ecode = fd:read(1) + fd:close() + if ecode == 21 then -- "Is a directory" + return true + end + end + return false +end + +function macosx.is_file(file) + file = fs.absolute_name(file) + if fs.is_dir(file) then + return false + end + file = dir.normalize(file) + local fd, _, code = io.open(file, "r") + if code == 2 then -- "No such file or directory" + return false + end + if code == 13 then -- "Permission denied", but it exists + return true + end + if fd then + fd:close() + return true + end + return false +end + +return macosx diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fs/tools.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/tools.lua new file mode 100644 index 000000000..5c317f65a --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/tools.lua @@ -0,0 +1,214 @@ + +--- Common fs operations implemented with third-party tools. +local tools = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local cfg = require("luarocks.core.cfg") + +local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end }) + +local dir_stack = {} + +do + local tool_cache = {} + + local tool_options = { + downloader = { + desc = "downloader", + { var = "WGET", name = "wget" }, + { var = "CURL", name = "curl" }, + }, + md5checker = { + desc = "MD5 checker", + { var = "MD5SUM", name = "md5sum" }, + { var = "OPENSSL", name = "openssl", cmdarg = "md5" }, + { var = "MD5", name = "md5" }, + }, + } + + function tools.which_tool(tooltype) + local tool = tool_cache[tooltype] + local names = {} + if not tool then + for _, opt in ipairs(tool_options[tooltype]) do + table.insert(names, opt.name) + if fs.is_tool_available(vars[opt.var], opt.name) then + tool = opt + tool_cache[tooltype] = opt + break + end + end + end + if not tool then + local tool_names = table.concat(names, ", ", 1, #names - 1) .. " or " .. names[#names] + return nil, "no " .. tool_options[tooltype].desc .. " tool available," .. " please install " .. tool_names .. " in your system" + end + return tool.name, vars[tool.var] .. (tool.cmdarg and " "..tool.cmdarg or "") + end +end + +do + local cache_pwd + --- Obtain current directory. + -- Uses the module's internal directory stack. + -- @return string: the absolute pathname of the current directory. + function tools.current_dir() + local current = cache_pwd + if not current then + local pipe = io.popen(fs.quiet_stderr(vars.PWD)) + current = pipe:read("*a"):gsub("^%s*", ""):gsub("%s*$", "") + pipe:close() + cache_pwd = current + end + for _, directory in ipairs(dir_stack) do + current = fs.absolute_name(directory, current) + end + return current + end +end + +--- Change the current directory. +-- Uses the module's internal directory stack. This does not have exact +-- semantics of chdir, as it does not handle errors the same way, +-- but works well for our purposes for now. +-- @param directory string: The directory to switch to. +-- @return boolean or (nil, string): true if successful, (nil, error message) if failed. +function tools.change_dir(directory) + assert(type(directory) == "string") + if fs.is_dir(directory) then + table.insert(dir_stack, directory) + return true + end + return nil, "directory not found: "..directory +end + +--- Change directory to root. +-- Allows leaving a directory (e.g. for deleting it) in +-- a crossplatform way. +function tools.change_dir_to_root() + local curr_dir = fs.current_dir() + if not curr_dir or not fs.is_dir(curr_dir) then + return false + end + table.insert(dir_stack, "/") + return true +end + +--- Change working directory to the previous in the directory stack. +function tools.pop_dir() + local directory = table.remove(dir_stack) + return directory ~= nil +end + +--- Run the given command. +-- The command is executed in the current directory in the directory stack. +-- @param cmd string: No quoting/escaping is applied to the command. +-- @return boolean: true if command succeeds (status code 0), false +-- otherwise. +function tools.execute_string(cmd) + local current = fs.current_dir() + if not current then return false end + cmd = fs.command_at(current, cmd) + local code = os.execute(cmd) + if code == 0 or code == true then + return true + else + return false + end +end + +--- Internal implementation function for fs.dir. +-- Yields a filename on each iteration. +-- @param at string: directory to list +-- @return nil +function tools.dir_iterator(at) + local pipe = io.popen(fs.command_at(at, vars.LS, true)) + for file in pipe:lines() do + if file ~= "." and file ~= ".." then + coroutine.yield(file) + end + end + pipe:close() +end + +--- Download a remote file. +-- @param url string: URL to be fetched. +-- @param filename string or nil: this function attempts to detect the +-- resulting local filename of the remote file as the basename of the URL; +-- if that is not correct (due to a redirection, for example), the local +-- filename can be given explicitly as this second argument. +-- @param cache boolean: compare remote timestamps via HTTP HEAD prior to +-- re-downloading the file. +-- @return (boolean, string): true and the filename on success, +-- false and the error message on failure. +function tools.use_downloader(url, filename, cache) + assert(type(url) == "string") + assert(type(filename) == "string" or not filename) + + filename = fs.absolute_name(filename or dir.base_name(url)) + + local downloader, err = fs.which_tool("downloader") + if not downloader then + return nil, err + end + + local ok = false + if downloader == "wget" then + local wget_cmd = vars.WGET.." "..vars.WGETNOCERTFLAG.." --no-cache --user-agent=\""..cfg.user_agent.." via wget\" --quiet " + if cfg.connection_timeout and cfg.connection_timeout > 0 then + wget_cmd = wget_cmd .. "--timeout="..tostring(cfg.connection_timeout).." --tries=1 " + end + if cache then + -- --timestamping is incompatible with --output-document, + -- but that's not a problem for our use cases. + fs.delete(filename .. ".unixtime") + fs.change_dir(dir.dir_name(filename)) + ok = fs.execute_quiet(wget_cmd.." --timestamping ", url) + fs.pop_dir() + elseif filename then + ok = fs.execute_quiet(wget_cmd.." --output-document ", filename, url) + else + ok = fs.execute_quiet(wget_cmd, url) + end + elseif downloader == "curl" then + local curl_cmd = vars.CURL.." "..vars.CURLNOCERTFLAG.." -f -L --user-agent \""..cfg.user_agent.." via curl\" " + if cfg.connection_timeout and cfg.connection_timeout > 0 then + curl_cmd = curl_cmd .. "--connect-timeout "..tostring(cfg.connection_timeout).." " + end + if cache then + curl_cmd = curl_cmd .. " -R -z \"" .. filename .. "\" " + end + ok = fs.execute_string(fs.quiet_stderr(curl_cmd..fs.Q(url).." --output "..fs.Q(filename))) + end + if ok then + return true, filename + else + os.remove(filename) + return false, "failed downloading " .. url + end +end + +--- Get the MD5 checksum for a file. +-- @param file string: The file to be computed. +-- @return string: The MD5 checksum or nil + message +function tools.get_md5(file) + local ok, md5checker = fs.which_tool("md5checker") + if not ok then + return false, md5checker + end + + local pipe = io.popen(md5checker.." "..fs.Q(fs.absolute_name(file))) + local computed = pipe:read("*l") + pipe:close() + if computed then + computed = computed:match("("..("%x"):rep(32)..")") + end + if computed then + return computed + else + return nil, "Failed to compute MD5 hash for file "..tostring(fs.absolute_name(file)) + end +end + +return tools diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fs/unix.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/unix.lua new file mode 100644 index 000000000..61569e307 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/unix.lua @@ -0,0 +1,240 @@ + +--- Unix implementation of filesystem and platform abstractions. +local unix = {} + +local fs = require("luarocks.fs") + +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local path = require("luarocks.path") +local util = require("luarocks.util") + +--- Annotate command string for quiet execution. +-- @param cmd string: A command-line string. +-- @return string: The command-line, with silencing annotation. +function unix.quiet(cmd) + return cmd.." 1> /dev/null 2> /dev/null" +end + +--- Annotate command string for execution with quiet stderr. +-- @param cmd string: A command-line string. +-- @return string: The command-line, with stderr silencing annotation. +function unix.quiet_stderr(cmd) + return cmd.." 2> /dev/null" +end + +--- Quote argument for shell processing. +-- Adds single quotes and escapes. +-- @param arg string: Unquoted argument. +-- @return string: Quoted argument. +function unix.Q(arg) + assert(type(arg) == "string") + return "'" .. arg:gsub("'", "'\\''") .. "'" +end + +--- Return an absolute pathname from a potentially relative one. +-- @param pathname string: pathname to convert. +-- @param relative_to string or nil: path to prepend when making +-- pathname absolute, or the current dir in the dir stack if +-- not given. +-- @return string: The pathname converted to absolute. +function unix.absolute_name(pathname, relative_to) + assert(type(pathname) == "string") + assert(type(relative_to) == "string" or not relative_to) + + local unquoted = pathname:match("^['\"](.*)['\"]$") + if unquoted then + pathname = unquoted + end + + relative_to = (relative_to or fs.current_dir()):gsub("/*$", "") + if pathname:sub(1,1) == "/" then + return pathname + else + return relative_to .. "/" .. pathname + end +end + +--- Return the root directory for the given path. +-- In Unix, root is always "/". +-- @param pathname string: pathname to use. +-- @return string: The root of the given pathname. +function unix.root_of(_) + return "/" +end + +--- Create a wrapper to make a script executable from the command-line. +-- @param script string: Pathname of script to be made executable. +-- @param target string: wrapper target pathname (without wrapper suffix). +-- @param name string: rock name to be used in loader context. +-- @param version string: rock version to be used in loader context. +-- @return boolean or (nil, string): True if succeeded, or nil and +-- an error message. +function unix.wrap_script(script, target, deps_mode, name, version, ...) + assert(type(script) == "string" or not script) + assert(type(target) == "string") + assert(type(deps_mode) == "string") + assert(type(name) == "string" or not name) + assert(type(version) == "string" or not version) + + local wrapper = io.open(target, "w") + if not wrapper then + return nil, "Could not open "..target.." for writing." + end + + local lpath, lcpath = path.package_paths(deps_mode) + + local luainit = { + "package.path="..util.LQ(lpath..";").."..package.path", + "package.cpath="..util.LQ(lcpath..";").."..package.cpath", + } + + local remove_interpreter = false + local base = dir.base_name(target):gsub("%..*$", "") + if base == "luarocks" or base == "luarocks-admin" then + if cfg.is_binary then + remove_interpreter = true + end + luainit = { + "package.path="..util.LQ(package.path), + "package.cpath="..util.LQ(package.cpath), + } + end + + if name and version then + local addctx = "local k,l,_=pcall(require,"..util.LQ("luarocks.loader")..") _=k " .. + "and l.add_context("..util.LQ(name)..","..util.LQ(version)..")" + table.insert(luainit, addctx) + end + + local argv = { + fs.Q(dir.path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter)), + "-e", + fs.Q(table.concat(luainit, ";")), + script and fs.Q(script) or [[$([ "$*" ] || echo -i)]], + ... + } + if remove_interpreter then + table.remove(argv, 1) + table.remove(argv, 1) + table.remove(argv, 1) + end + + wrapper:write("#!/bin/sh\n\n") + wrapper:write("LUAROCKS_SYSCONFDIR="..fs.Q(cfg.sysconfdir) .. " ") + wrapper:write("exec "..table.concat(argv, " ")..' "$@"\n') + wrapper:close() + + if fs.set_permissions(target, "exec", "all") then + return true + else + return nil, "Could not make "..target.." executable." + end +end + +--- Check if a file (typically inside path.bin_dir) is an actual binary +-- or a Lua wrapper. +-- @param filename string: the file name with full path. +-- @return boolean: returns true if file is an actual binary +-- (or if it couldn't check) or false if it is a Lua wrapper. +function unix.is_actual_binary(filename) + if filename:match("%.lua$") then + return false + end + local file = io.open(filename) + if not file then + return true + end + local first = file:read(2) + file:close() + if not first then + util.warning("could not read "..filename) + return true + end + return first ~= "#!" +end + +function unix.copy_binary(filename, dest) + return fs.copy(filename, dest, "exec") +end + +--- Move a file on top of the other. +-- The new file ceases to exist under its original name, +-- and takes over the name of the old file. +-- On Unix this is done through a single rename operation. +-- @param old_file The name of the original file, +-- which will be the new name of new_file. +-- @param new_file The name of the new file, +-- which will replace old_file. +-- @return boolean or (nil, string): True if succeeded, or nil and +-- an error message. +function unix.replace_file(old_file, new_file) + return os.rename(new_file, old_file) +end + +function unix.tmpname() + return os.tmpname() +end + +function unix.export_cmd(var, val) + return ("export %s='%s'"):format(var, val) +end + +local octal_to_rwx = { + ["0"] = "---", + ["1"] = "--x", + ["2"] = "-w-", + ["3"] = "-wx", + ["4"] = "r--", + ["5"] = "r-x", + ["6"] = "rw-", + ["7"] = "rwx", +} +local rwx_to_octal = {} +for octal, rwx in pairs(octal_to_rwx) do + rwx_to_octal[rwx] = octal +end +--- Moderate the given permissions based on the local umask +-- @param perms string: permissions to moderate +-- @return string: the moderated permissions +function unix._unix_moderate_permissions(perms) + local umask = fs._unix_umask() + + local moderated_perms = "" + for i = 1, 3 do + local p_rwx = octal_to_rwx[perms:sub(i, i)] + local u_rwx = octal_to_rwx[umask:sub(i, i)] + local new_perm = "" + for j = 1, 3 do + local p_val = p_rwx:sub(j, j) + local u_val = u_rwx:sub(j, j) + if p_val == u_val then + new_perm = new_perm .. "-" + else + new_perm = new_perm .. p_val + end + end + moderated_perms = moderated_perms .. rwx_to_octal[new_perm] + end + return moderated_perms +end + +function unix.system_cache_dir() + if fs.is_dir("/var/cache") then + return "/var/cache" + end + return dir.path(fs.system_temp_dir(), "cache") +end + +function unix.search_in_path(program) + for d in (os.getenv("PATH") or ""):gmatch("([^:]+)") do + local fd = io.open(dir.path(d, program), "r") + if fd then + fd:close() + return true, d + end + end + return false +end + +return unix diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fs/unix/tools.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/unix/tools.lua new file mode 100644 index 000000000..b7fd4de5e --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/unix/tools.lua @@ -0,0 +1,318 @@ + +--- fs operations implemented with third-party tools for Unix platform abstractions. +local tools = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local cfg = require("luarocks.core.cfg") + +local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end }) + +--- Adds prefix to command to make it run from a directory. +-- @param directory string: Path to a directory. +-- @param cmd string: A command-line string. +-- @return string: The command-line with prefix. +function tools.command_at(directory, cmd) + return "cd " .. fs.Q(fs.absolute_name(directory)) .. " && " .. cmd +end + +--- Create a directory if it does not already exist. +-- If any of the higher levels in the path name does not exist +-- too, they are created as well. +-- @param directory string: pathname of directory to create. +-- @return boolean: true on success, false on failure. +function tools.make_dir(directory) + assert(directory) + local ok, err = fs.execute(vars.MKDIR.." -p", directory) + if not ok then + err = "failed making directory "..directory + end + return ok, err +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param directory string: pathname of directory to remove. +function tools.remove_dir_if_empty(directory) + assert(directory) + fs.execute_quiet(vars.RMDIR, directory) +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param directory string: pathname of directory to remove. +function tools.remove_dir_tree_if_empty(directory) + assert(directory) + fs.execute_quiet(vars.RMDIR, "-p", directory) +end + +--- Copy a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @param perm string ("read" or "exec") or nil: Permissions for destination +-- file or nil to use the source permissions +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function tools.copy(src, dest, perm) + assert(src and dest) + if fs.execute(vars.CP, src, dest) then + if perm then + if fs.is_dir(dest) then + dest = dir.path(dest, dir.base_name(src)) + end + if fs.set_permissions(dest, perm, "all") then + return true + else + return false, "Failed setting permissions of "..dest + end + end + return true + else + return false, "Failed copying "..src.." to "..dest + end +end + +--- Recursively copy the contents of a directory. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function tools.copy_contents(src, dest) + assert(src and dest) + if fs.execute_quiet(vars.CP.." -pPR "..fs.Q(src).."/* "..fs.Q(dest)) then + return true + else + return false, "Failed copying "..src.." to "..dest + end +end +--- Delete a file or a directory and all its contents. +-- For safety, this only accepts absolute paths. +-- @param arg string: Pathname of source +-- @return nil +function tools.delete(arg) + assert(arg) + assert(arg:sub(1,1) == "/") + fs.execute_quiet(vars.RM, "-rf", arg) +end + +--- Recursively scan the contents of a directory. +-- @param at string or nil: directory to scan (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. +function tools.find(at) + assert(type(at) == "string" or not at) + if not at then + at = fs.current_dir() + end + if not fs.is_dir(at) then + return {} + end + local result = {} + local pipe = io.popen(fs.command_at(at, fs.quiet_stderr(vars.FIND.." *"))) + for file in pipe:lines() do + table.insert(result, file) + end + pipe:close() + return result +end + +--- Compress files in a .zip archive. +-- @param zipfile string: pathname of .zip archive to be created. +-- @param ... Filenames to be stored in the archive are given as +-- additional arguments. +-- @return boolean: true on success, nil and error message on failure. +function tools.zip(zipfile, ...) + local ok, err = fs.is_tool_available(vars.ZIP, "zip") + if not ok then + return nil, err + end + if fs.execute_quiet(vars.ZIP.." -r", zipfile, ...) then + return true + else + return nil, "failed compressing " .. zipfile + end +end + +--- Uncompress files from a .zip archive. +-- @param zipfile string: pathname of .zip archive to be extracted. +-- @return boolean: true on success, nil and error message on failure. +function tools.unzip(zipfile) + assert(zipfile) + local ok, err = fs.is_tool_available(vars.UNZIP, "unzip") + if not ok then + return nil, err + end + if fs.execute_quiet(vars.UNZIP, zipfile) then + return true + else + return nil, "failed extracting " .. zipfile + end +end + +local function uncompress(default_ext, program, infile, outfile) + assert(type(infile) == "string") + assert(outfile == nil or type(outfile) == "string") + if not outfile then + outfile = infile:gsub("%."..default_ext.."$", "") + end + if fs.execute(fs.Q(program).." -c "..fs.Q(infile).." > "..fs.Q(outfile)) then + return true + else + return nil, "failed extracting " .. infile + end +end + +--- Uncompresses a .gz file. +-- @param infile string: pathname of .gz file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.gunzip(infile, outfile) + return uncompress("gz", "gunzip", infile, outfile) +end + +--- Uncompresses a .bz2 file. +-- @param infile string: pathname of .bz2 file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.bunzip2(infile, outfile) + return uncompress("bz2", "bunzip2", infile, outfile) +end + +do + local function rwx_to_octal(rwx) + return (rwx:match "r" and 4 or 0) + + (rwx:match "w" and 2 or 0) + + (rwx:match "x" and 1 or 0) + end + local umask_cache + function tools._unix_umask() + if umask_cache then + return umask_cache + end + local fd = assert(io.popen("umask -S")) + local umask = assert(fd:read("*a")) + fd:close() + local u, g, o = umask:match("u=([rwx]*),g=([rwx]*),o=([rwx]*)") + if not u then + error("invalid umask result") + end + umask_cache = string.format("%d%d%d", + 7 - rwx_to_octal(u), + 7 - rwx_to_octal(g), + 7 - rwx_to_octal(o)) + return umask_cache + end +end + +--- Set permissions for file or directory +-- @param filename string: filename whose permissions are to be modified +-- @param mode string ("read" or "exec"): permissions to set +-- @param scope string ("user" or "all"): the user(s) to whom the permission applies +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message +function tools.set_permissions(filename, mode, scope) + assert(filename and mode and scope) + + local perms + if mode == "read" and scope == "user" then + perms = fs._unix_moderate_permissions("600") + elseif mode == "exec" and scope == "user" then + perms = fs._unix_moderate_permissions("700") + elseif mode == "read" and scope == "all" then + perms = fs._unix_moderate_permissions("644") + elseif mode == "exec" and scope == "all" then + perms = fs._unix_moderate_permissions("755") + else + return false, "Invalid permission " .. mode .. " for " .. scope + end + return fs.execute(vars.CHMOD, perms, filename) +end + +function tools.browser(url) + return fs.execute(cfg.web_browser, url) +end + +-- Set access and modification times for a file. +-- @param filename File to set access and modification times for. +-- @param time may be a string or number containing the format returned +-- by os.time, or a table ready to be processed via os.time; if +-- nil, current time is assumed. +function tools.set_time(file, time) + assert(time == nil or type(time) == "table" or type(time) == "number") + file = dir.normalize(file) + local flag = "" + if type(time) == "number" then + time = os.date("*t", time) + end + if type(time) == "table" then + flag = ("-t %04d%02d%02d%02d%02d.%02d"):format(time.year, time.month, time.day, time.hour, time.min, time.sec) + end + return fs.execute(vars.TOUCH .. " " .. flag, file) +end + +--- Create a temporary directory. +-- @param name_pattern string: name pattern to use for avoiding conflicts +-- when creating temporary directory. +-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure. +function tools.make_temp_dir(name_pattern) + assert(type(name_pattern) == "string") + name_pattern = dir.normalize(name_pattern) + + local template = (os.getenv("TMPDIR") or "/tmp") .. "/luarocks_" .. name_pattern:gsub("/", "_") .. "-XXXXXX" + local pipe = io.popen(vars.MKTEMP.." -d "..fs.Q(template)) + local dirname = pipe:read("*l") + pipe:close() + if dirname and dirname:match("^/") then + return dirname + end + return nil, "Failed to create temporary directory "..tostring(dirname) +end + +--- Test is file/directory exists +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function tools.exists(file) + assert(file) + return fs.execute(vars.TEST, "-e", file) +end + +--- Test is pathname is a directory. +-- @param file string: pathname to test +-- @return boolean: true if it is a directory, false otherwise. +function tools.is_dir(file) + assert(file) + return fs.execute(vars.TEST, "-d", file) +end + +--- Test is pathname is a regular file. +-- @param file string: pathname to test +-- @return boolean: true if it is a regular file, false otherwise. +function tools.is_file(file) + assert(file) + return fs.execute(vars.TEST, "-f", file) +end + +function tools.current_user() + local user = os.getenv("USER") + if user then + return user + end + local pd = io.popen("whoami", "r") + if not pd then + return "" + end + user = pd:read("*l") + pd:close() + return user +end + +function tools.is_superuser() + return fs.current_user() == "root" +end + +return tools diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fs/win32.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/win32.lua new file mode 100644 index 000000000..6c49f4477 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/win32.lua @@ -0,0 +1,377 @@ +--- Windows implementation of filesystem and platform abstractions. +-- Download http://unxutils.sourceforge.net/ for Windows GNU utilities +-- used by this module. +local win32 = {} + +local fs = require("luarocks.fs") + +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local path = require("luarocks.path") +local util = require("luarocks.util") + +-- Monkey patch io.popen and os.execute to make sure quoting +-- works as expected. +-- See http://lua-users.org/lists/lua-l/2013-11/msg00367.html +local _prefix = "type NUL && " +local _popen, _execute = io.popen, os.execute + +-- luacheck: push globals io os +io.popen = function(cmd, ...) return _popen(_prefix..cmd, ...) end +os.execute = function(cmd, ...) return _execute(_prefix..cmd, ...) end +-- luacheck: pop + +--- Annotate command string for quiet execution. +-- @param cmd string: A command-line string. +-- @return string: The command-line, with silencing annotation. +function win32.quiet(cmd) + return cmd.." 2> NUL 1> NUL" +end + +--- Annotate command string for execution with quiet stderr. +-- @param cmd string: A command-line string. +-- @return string: The command-line, with stderr silencing annotation. +function win32.quiet_stderr(cmd) + return cmd.." 2> NUL" +end + +-- Split path into drive, root and the rest. +-- Example: "c:\\hello\\world" becomes "c:" "\\" "hello\\world" +-- if any part is missing from input, it becomes an empty string. +local function split_root(pathname) + local drive = "" + local root = "" + local rest + + local unquoted = pathname:match("^['\"](.*)['\"]$") + if unquoted then + pathname = unquoted + end + + if pathname:match("^.:") then + drive = pathname:sub(1, 2) + pathname = pathname:sub(3) + end + + if pathname:match("^[\\/]") then + root = pathname:sub(1, 1) + rest = pathname:sub(2) + else + rest = pathname + end + + return drive, root, rest +end + +--- Quote argument for shell processing. Fixes paths on Windows. +-- Adds double quotes and escapes. +-- @param arg string: Unquoted argument. +-- @return string: Quoted argument. +function win32.Q(arg) + assert(type(arg) == "string") + -- Use Windows-specific directory separator for paths. + -- Paths should be converted to absolute by now. + local drive, root, rest = split_root(arg) + if root ~= "" then + arg = arg:gsub("/", "\\") + end + if arg == "\\" then + return '\\' -- CHDIR needs special handling for root dir + end + -- URLs and anything else + arg = arg:gsub('\\(\\*)"', '\\%1%1"') + arg = arg:gsub('\\+$', '%0%0') + arg = arg:gsub('"', '\\"') + arg = arg:gsub('(\\*)%%', '%1%1"%%"') + return '"' .. arg .. '"' +end + +--- Quote argument for shell processing in batch files. +-- Adds double quotes and escapes. +-- @param arg string: Unquoted argument. +-- @return string: Quoted argument. +function win32.Qb(arg) + assert(type(arg) == "string") + -- Use Windows-specific directory separator for paths. + -- Paths should be converted to absolute by now. + local drive, root, rest = split_root(arg) + if root ~= "" then + arg = arg:gsub("/", "\\") + end + if arg == "\\" then + return '\\' -- CHDIR needs special handling for root dir + end + -- URLs and anything else + arg = arg:gsub('\\(\\*)"', '\\%1%1"') + arg = arg:gsub('\\+$', '%0%0') + arg = arg:gsub('"', '\\"') + arg = arg:gsub('%%', '%%%%') + return '"' .. arg .. '"' +end + +--- Return an absolute pathname from a potentially relative one. +-- @param pathname string: pathname to convert. +-- @param relative_to string or nil: path to prepend when making +-- pathname absolute, or the current dir in the dir stack if +-- not given. +-- @return string: The pathname converted to absolute. +function win32.absolute_name(pathname, relative_to) + assert(type(pathname) == "string") + assert(type(relative_to) == "string" or not relative_to) + + relative_to = (relative_to or fs.current_dir()):gsub("[\\/]*$", "") + local drive, root, rest = split_root(pathname) + if root:match("[\\/]$") then + -- It's an absolute path already. Ensure is not quoted. + return drive .. root .. rest + else + -- It's a relative path, join it with base path. + -- This drops drive letter from paths like "C:foo". + return relative_to .. "/" .. rest + end +end + +--- Return the root directory for the given path. +-- For example, for "c:\hello", returns "c:\" +-- @param pathname string: pathname to use. +-- @return string: The root of the given pathname. +function win32.root_of(pathname) + local drive, root, rest = split_root(fs.absolute_name(pathname)) + return drive .. root +end + +--- Create a wrapper to make a script executable from the command-line. +-- @param script string: Pathname of script to be made executable. +-- @param target string: wrapper target pathname (without wrapper suffix). +-- @param name string: rock name to be used in loader context. +-- @param version string: rock version to be used in loader context. +-- @return boolean or (nil, string): True if succeeded, or nil and +-- an error message. +function win32.wrap_script(script, target, deps_mode, name, version, ...) + assert(type(script) == "string" or not script) + assert(type(target) == "string") + assert(type(deps_mode) == "string") + assert(type(name) == "string" or not name) + assert(type(version) == "string" or not version) + + local wrapper = io.open(target, "wb") + if not wrapper then + return nil, "Could not open "..target.." for writing." + end + + local lpath, lcpath = path.package_paths(deps_mode) + + local luainit = { + "package.path="..util.LQ(lpath..";").."..package.path", + "package.cpath="..util.LQ(lcpath..";").."..package.cpath", + } + + local remove_interpreter = false + local base = dir.base_name(target):gsub("%..*$", "") + if base == "luarocks" or base == "luarocks-admin" then + if cfg.is_binary then + remove_interpreter = true + end + luainit = { + "package.path="..util.LQ(package.path), + "package.cpath="..util.LQ(package.cpath), + } + end + + if name and version then + local addctx = "local k,l,_=pcall(require,'luarocks.loader') _=k " .. + "and l.add_context('"..name.."','"..version.."')" + table.insert(luainit, addctx) + end + + local argv = { + fs.Qb(dir.path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter)), + "-e", + fs.Qb(table.concat(luainit, ";")), + script and fs.Qb(script) or "%I%", + ... + } + if remove_interpreter then + table.remove(argv, 1) + table.remove(argv, 1) + table.remove(argv, 1) + end + + wrapper:write("@echo off\r\n") + wrapper:write("setlocal\r\n") + if not script then + wrapper:write([[IF "%*"=="" (set I=-i) ELSE (set I=)]] .. "\r\n") + end + wrapper:write("set "..fs.Qb("LUAROCKS_SYSCONFDIR="..cfg.sysconfdir) .. "\r\n") + wrapper:write(table.concat(argv, " ") .. " %*\r\n") + wrapper:write("exit /b %ERRORLEVEL%\r\n") + wrapper:close() + return true +end + +function win32.is_actual_binary(name) + name = name:lower() + if name:match("%.bat$") or name:match("%.exe$") then + return true + end + return false +end + +function win32.copy_binary(filename, dest) + local ok, err = fs.copy(filename, dest) + if not ok then + return nil, err + end + local exe_pattern = "%.[Ee][Xx][Ee]$" + local base = dir.base_name(filename) + dest = dir.dir_name(dest) + if base:match(exe_pattern) then + base = base:gsub(exe_pattern, ".lua") + local helpname = dest.."/"..base + local helper = io.open(helpname, "w") + if not helper then + return nil, "Could not open "..helpname.." for writing." + end + helper:write('package.path=\"'..package.path:gsub("\\","\\\\")..';\"..package.path\n') + helper:write('package.cpath=\"'..package.path:gsub("\\","\\\\")..';\"..package.cpath\n') + helper:close() + end + return true +end + +--- Move a file on top of the other. +-- The new file ceases to exist under its original name, +-- and takes over the name of the old file. +-- On Windows this is done by removing the original file and +-- renaming the new file to its original name. +-- @param old_file The name of the original file, +-- which will be the new name of new_file. +-- @param new_file The name of the new file, +-- which will replace old_file. +-- @return boolean or (nil, string): True if succeeded, or nil and +-- an error message. +function win32.replace_file(old_file, new_file) + os.remove(old_file) + return os.rename(new_file, old_file) +end + +function win32.is_dir(file) + file = fs.absolute_name(file) + file = dir.normalize(file) + local fd, _, code = io.open(file, "r") + if code == 13 then -- directories return "Permission denied" + fd, _, code = io.open(file .. "\\", "r") + if code == 2 then -- directories return 2, files return 22 + return true + end + end + if fd then + fd:close() + end + return false +end + +function win32.is_file(file) + file = fs.absolute_name(file) + file = dir.normalize(file) + local fd, _, code = io.open(file, "r") + if code == 13 then -- if "Permission denied" + fd, _, code = io.open(file .. "\\", "r") + if code == 2 then -- directories return 2, files return 22 + return false + elseif code == 22 then + return true + end + end + if fd then + fd:close() + return true + end + return false +end + +--- Test is file/dir is writable. +-- Warning: testing if a file/dir is writable does not guarantee +-- that it will remain writable and therefore it is no replacement +-- for checking the result of subsequent operations. +-- @param file string: filename to test +-- @return boolean: true if file exists, false otherwise. +function win32.is_writable(file) + assert(file) + file = dir.normalize(file) + local result + local tmpname = 'tmpluarockstestwritable.deleteme' + if fs.is_dir(file) then + local file2 = dir.path(file, tmpname) + local fh = io.open(file2, 'wb') + result = fh ~= nil + if fh then fh:close() end + if result then + -- the above test might give a false positive when writing to + -- c:\program files\ because of VirtualStore redirection on Vista and up + -- So check whether it's really there + result = fs.exists(file2) + end + os.remove(file2) + else + local fh = io.open(file, 'r+b') + result = fh ~= nil + if fh then fh:close() end + end + return result +end + +--- Create a temporary directory. +-- @param name_pattern string: name pattern to use for avoiding conflicts +-- when creating temporary directory. +-- @return string or (nil, string): name of temporary directory or (nil, error message) on failure. +function win32.make_temp_dir(name_pattern) + assert(type(name_pattern) == "string") + name_pattern = dir.normalize(name_pattern) + + local temp_dir = os.getenv("TMP") .. "/luarocks_" .. name_pattern:gsub("/", "_") .. "-" .. tostring(math.floor(math.random() * 10000)) + local ok, err = fs.make_dir(temp_dir) + if ok then + return temp_dir + else + return nil, err + end +end + +function win32.tmpname() + local name = os.tmpname() + local tmp = os.getenv("TMP") + if tmp and name:sub(1, #tmp) ~= tmp then + name = (tmp .. "\\" .. name):gsub("\\+", "\\") + end + return name +end + +function win32.current_user() + return os.getenv("USERNAME") +end + +function win32.is_superuser() + return false +end + +function win32.export_cmd(var, val) + return ("SET %s=%s"):format(var, val) +end + +function win32.system_cache_dir() + return dir.path(fs.system_temp_dir(), "cache") +end + +function win32.search_in_path(program) + for d in (os.getenv("PATH") or ""):gmatch("([^;]+)") do + local fd = io.open(dir.path(d, program .. ".exe"), "r") + if fd then + fd:close() + return true, d + end + end + return false +end + +return win32 diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fs/win32/tools.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/win32/tools.lua new file mode 100644 index 000000000..9bd050c60 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fs/win32/tools.lua @@ -0,0 +1,319 @@ + +--- fs operations implemented with third-party tools for Windows platform abstractions. +-- Download http://unxutils.sourceforge.net/ for Windows GNU utilities +-- used by this module. +local tools = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local cfg = require("luarocks.core.cfg") + +local vars = setmetatable({}, { __index = function(_,k) return cfg.variables[k] end }) + +--- Adds prefix to command to make it run from a directory. +-- @param directory string: Path to a directory. +-- @param cmd string: A command-line string. +-- @param exit_on_error bool: Exits immediately if entering the directory failed. +-- @return string: The command-line with prefix. +function tools.command_at(directory, cmd, exit_on_error) + local drive = directory:match("^([A-Za-z]:)") + local op = " & " + if exit_on_error then + op = " && " + end + local cmd_prefixed = "cd " .. fs.Q(directory) .. op .. cmd + if drive then + cmd_prefixed = drive .. " & " .. cmd_prefixed + end + return cmd_prefixed +end + +--- Create a directory if it does not already exist. +-- If any of the higher levels in the path name does not exist +-- too, they are created as well. +-- @param directory string: pathname of directory to create. +-- @return boolean: true on success, false on failure. +function tools.make_dir(directory) + assert(directory) + directory = dir.normalize(directory) + fs.execute_quiet(vars.MKDIR, directory) + if not fs.is_dir(directory) then + return false, "failed making directory "..directory + end + return true +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param directory string: pathname of directory to remove. +function tools.remove_dir_if_empty(directory) + assert(directory) + fs.execute_quiet(vars.RMDIR, directory) +end + +--- Remove a directory if it is empty. +-- Does not return errors (for example, if directory is not empty or +-- if already does not exist) +-- @param directory string: pathname of directory to remove. +function tools.remove_dir_tree_if_empty(directory) + assert(directory) + while true do + fs.execute_quiet(vars.RMDIR, directory) + local parent = dir.dir_name(directory) + if parent ~= directory then + directory = parent + else + break + end + end +end + +--- Copy a file. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function tools.copy(src, dest) + assert(src and dest) + if dest:match("[/\\]$") then dest = dest:sub(1, -2) end + local ok = fs.execute(vars.CP, src, dest) + if ok then + return true + else + return false, "Failed copying "..src.." to "..dest + end +end + +--- Recursively copy the contents of a directory. +-- @param src string: Pathname of source +-- @param dest string: Pathname of destination +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message. +function tools.copy_contents(src, dest) + assert(src and dest) + if not fs.is_dir(src) then + return false, src .. " is not a directory" + end + if fs.make_dir(dest) and fs.execute_quiet(vars.CP, "-dR", src.."\\*.*", dest) then + return true + else + return false, "Failed copying "..src.." to "..dest + end +end + +--- Delete a file or a directory and all its contents. +-- For safety, this only accepts absolute paths. +-- @param arg string: Pathname of source +-- @return nil +function tools.delete(arg) + assert(arg) + assert(arg:match("^[a-zA-Z]?:?[\\/]")) + fs.execute_quiet("if exist "..fs.Q(arg.."\\*").." ( RMDIR /S /Q "..fs.Q(arg).." ) else ( DEL /Q /F "..fs.Q(arg).." )") +end + +--- Recursively scan the contents of a directory. +-- @param at string or nil: directory to scan (will be the current +-- directory if none is given). +-- @return table: an array of strings with the filenames representing +-- the contents of a directory. Paths are returned with forward slashes. +function tools.find(at) + assert(type(at) == "string" or not at) + if not at then + at = fs.current_dir() + end + if not fs.is_dir(at) then + return {} + end + local result = {} + local pipe = io.popen(fs.command_at(at, fs.quiet_stderr(vars.FIND), true)) + for file in pipe:lines() do + -- Windows find is a bit different + local first_two = file:sub(1,2) + if first_two == ".\\" or first_two == "./" then file=file:sub(3) end + if file ~= "." then + table.insert(result, (file:gsub("\\", "/"))) + end + end + pipe:close() + return result +end + +--- Compress files in a .zip archive. +-- @param zipfile string: pathname of .zip archive to be created. +-- @param ... Filenames to be stored in the archive are given as +-- additional arguments. +-- @return boolean: true on success, nil and error message on failure. +function tools.zip(zipfile, ...) + if fs.execute_quiet(vars.SEVENZ.." -aoa a -tzip", zipfile, ...) then + return true + else + return nil, "failed compressing " .. zipfile + end +end + +--- Uncompress files from a .zip archive. +-- @param zipfile string: pathname of .zip archive to be extracted. +-- @return boolean: true on success, nil and error message on failure. +function tools.unzip(zipfile) + assert(zipfile) + if fs.execute_quiet(vars.SEVENZ.." -aoa x", zipfile) then + return true + else + return nil, "failed extracting " .. zipfile + end +end + +local function sevenz(default_ext, infile, outfile) + assert(type(infile) == "string") + assert(outfile == nil or type(outfile) == "string") + + local dropext = infile:gsub("%."..default_ext.."$", "") + local outdir = dir.dir_name(dropext) + + infile = fs.absolute_name(infile) + + local cmdline = vars.SEVENZ.." -aoa -t* -o"..fs.Q(outdir).." x "..fs.Q(infile) + local ok, err = fs.execute_quiet(cmdline) + if not ok then + return nil, "failed extracting " .. infile + end + + if outfile then + outfile = fs.absolute_name(outfile) + dropext = fs.absolute_name(dropext) + ok, err = os.rename(dropext, outfile) + if not ok then + return nil, "failed creating new file " .. outfile + end + end + + return true +end + +--- Uncompresses a .gz file. +-- @param infile string: pathname of .gz file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.gunzip(infile, outfile) + return sevenz("gz", infile, outfile) +end + +--- Uncompresses a .bz2 file. +-- @param infile string: pathname of .bz2 file to be extracted. +-- @param outfile string or nil: pathname of output file to be produced. +-- If not given, name is derived from input file. +-- @return boolean: true on success; nil and error message on failure. +function tools.bunzip2(infile, outfile) + return sevenz("bz2", infile, outfile) +end + +--- Helper function for fs.set_permissions +-- @return table: an array of all system users +local function get_system_users() + local exclude = { + [""] = true, + ["Name"] = true, + ["\128\164\172\168\173\168\225\226\224\160\226\174\224"] = true, -- Administrator in cp866 + ["Administrator"] = true, + } + local result = {} + local fd = assert(io.popen("wmic UserAccount get name")) + for user in fd:lines() do + user = user:gsub("%s+$", "") + if not exclude[user] then + table.insert(result, user) + end + end + return result +end + +--- Set permissions for file or directory +-- @param filename string: filename whose permissions are to be modified +-- @param mode string ("read" or "exec"): permission to set +-- @param scope string ("user" or "all"): the user(s) to whom the permission applies +-- @return boolean or (boolean, string): true on success, false on failure, +-- plus an error message +function tools.set_permissions(filename, mode, scope) + assert(filename and mode and scope) + + if scope == "user" then + local perms + if mode == "read" then + perms = "(R,W,M)" + elseif mode == "exec" then + perms = "(F)" + end + + local ok + -- Take ownership of the given file + ok = fs.execute_quiet("takeown /f " .. fs.Q(filename)) + if not ok then + return false, "Could not take ownership of the given file" + end + local username = os.getenv('USERNAME') + -- Grant the current user the proper rights + ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant:r " .. fs.Q(username) .. ":" .. perms) + if not ok then + return false, "Failed setting permission " .. mode .. " for " .. scope + end + -- Finally, remove all the other users from the ACL in order to deny them access to the file + for _, user in pairs(get_system_users()) do + if username ~= user then + local ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /remove " .. fs.Q(user)) + if not ok then + return false, "Failed setting permission " .. mode .. " for " .. scope + end + end + end + elseif scope == "all" then + local my_perms, others_perms + if mode == "read" then + my_perms = "(R,W,M)" + others_perms = "(R)" + elseif mode == "exec" then + my_perms = "(F)" + others_perms = "(RX)" + end + + local ok + -- Grant permissions available to all users + ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant:r *S-1-1-0:" .. others_perms) + if not ok then + return false, "Failed setting permission " .. mode .. " for " .. scope + end + + -- Grant permissions available only to the current user + ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant \"%USERNAME%\":" .. my_perms) + + -- This may not be necessary if the above syntax is correct, + -- but I couldn't really test the extra quotes above, so if that + -- fails we try again with the syntax used in previous releases + -- just to be on the safe side + if not ok then + ok = fs.execute_quiet(vars.ICACLS .. " " .. fs.Q(filename) .. " /inheritance:d /grant %USERNAME%:" .. my_perms) + end + + if not ok then + return false, "Failed setting permission " .. mode .. " for " .. scope + end + end + + return true +end + +function tools.browser(url) + return fs.execute(cfg.web_browser..' "Starting docs..." '..fs.Q(url)) +end + +-- Set access and modification times for a file. +-- @param filename File to set access and modification times for. +-- @param time may be a string or number containing the format returned +-- by os.time, or a table ready to be processed via os.time; if +-- nil, current time is assumed. +function tools.set_time(filename, time) + return true -- FIXME +end + +return tools diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/fun.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/fun.lua new file mode 100644 index 000000000..80bf7c206 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/fun.lua @@ -0,0 +1,143 @@ + +--- A set of basic functional utilities +local fun = {} + +local unpack = table.unpack or unpack + +function fun.concat(xs, ys) + local rs = {} + local n = #xs + for i = 1, n do + rs[i] = xs[i] + end + for i = 1, #ys do + rs[i + n] = ys[i] + end + return rs +end + +function fun.contains(xs, v) + for _, x in ipairs(xs) do + if v == x then + return true + end + end + return false +end + +function fun.map(xs, f) + local rs = {} + for i = 1, #xs do + rs[i] = f(xs[i]) + end + return rs +end + +function fun.filter(xs, f) + local rs = {} + for i = 1, #xs do + local v = xs[i] + if f(v) then + rs[#rs+1] = v + end + end + return rs +end + +function fun.traverse(t, f) + return fun.map(t, function(x) + return type(x) == "table" and fun.traverse(x, f) or f(x) + end) +end + +function fun.reverse_in(t) + for i = 1, math.floor(#t/2) do + local m, n = i, #t - i + 1 + local a, b = t[m], t[n] + t[m] = b + t[n] = a + end + return t +end + +function fun.sort_in(t, f) + table.sort(t, f) + return t +end + +function fun.flip(f) + return function(a, b) + return f(b, a) + end +end + +function fun.find(xs, f) + if type(xs) == "function" then + for v in xs do + local x = f(v) + if x then + return x + end + end + elseif type(xs) == "table" then + for _, v in ipairs(xs) do + local x = f(v) + if x then + return x + end + end + end +end + +function fun.partial(f, ...) + local n = select("#", ...) + if n == 1 then + local a = ... + return function(...) + return f(a, ...) + end + elseif n == 2 then + local a, b = ... + return function(...) + return f(a, b, ...) + end + else + local pargs = { n = n, ... } + return function(...) + local m = select("#", ...) + local fargs = { ... } + local args = {} + for i = 1, n do + args[i] = pargs[i] + end + for i = 1, m do + args[i+n] = fargs[i] + end + return f(unpack(args, 1, n+m)) + end + end +end + +function fun.memoize(fn) + local memory = setmetatable({}, { __mode = "k" }) + local errors = setmetatable({}, { __mode = "k" }) + local NIL = {} + return function(arg) + if memory[arg] then + if memory[arg] == NIL then + return nil, errors[arg] + end + return memory[arg] + end + local ret1, ret2 = fn(arg) + if ret1 ~= nil then + memory[arg] = ret1 + else + memory[arg] = NIL + errors[arg] = ret2 + end + return ret1, ret2 + end +end + +return fun diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/loader.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/loader.lua new file mode 100644 index 000000000..c409a1da9 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/loader.lua @@ -0,0 +1,248 @@ +--- A module which installs a Lua package loader that is LuaRocks-aware. +-- This loader uses dependency information from the LuaRocks tree to load +-- correct versions of modules. It does this by constructing a "context" +-- table in the environment, which records which versions of packages were +-- used to load previous modules, so that the loader chooses versions +-- that are declared to be compatible with the ones loaded earlier. + +-- luacheck: globals luarocks + +local loaders = package.loaders or package.searchers +local require, ipairs, table, type, next, tostring, error = + require, ipairs, table, type, next, tostring, error +local unpack = unpack or table.unpack + +local loader = {} + +local is_clean = not package.loaded["luarocks.core.cfg"] + +-- This loader module depends only on core modules. +local cfg = require("luarocks.core.cfg") +local cfg_ok, err = cfg.init() +if cfg_ok then + cfg.init_package_paths() +end + +local path = require("luarocks.core.path") +local manif = require("luarocks.core.manif") +local vers = require("luarocks.core.vers") +local require = nil -- luacheck: ignore 411 +-------------------------------------------------------------------------------- + +local temporary_global = false + +loader.context = {} + +--- Process the dependencies of a package to determine its dependency +-- chain for loading modules. +-- @param name string: The name of an installed rock. +-- @param version string: The version of the rock, in string format +function loader.add_context(name, version) + -- assert(type(name) == "string") + -- assert(type(version) == "string") + + if temporary_global then + -- The first thing a wrapper does is to call add_context. + -- From here on, it's safe to clean the global environment. + luarocks = nil + temporary_global = false + end + + local tree_manifests = manif.load_rocks_tree_manifests() + if not tree_manifests then + return nil + end + + return manif.scan_dependencies(name, version, tree_manifests, loader.context) +end + +--- Internal sorting function. +-- @param a table: A provider table. +-- @param b table: Another provider table. +-- @return boolean: True if the version of a is greater than that of b. +local function sort_versions(a,b) + return a.version > b.version +end + +--- Request module to be loaded through other loaders, +-- once the proper name of the module has been determined. +-- For example, in case the module "socket.core" has been requested +-- to the LuaRocks loader and it determined based on context that +-- the version 2.0.2 needs to be loaded and it is not the current +-- version, the module requested for the other loaders will be +-- "socket.core_2_0_2". +-- @param module The module name requested by the user, such as "socket.core" +-- @param name The rock name, such as "luasocket" +-- @param version The rock version, such as "2.0.2-1" +-- @param module_name The actual module name, such as "socket.core" or "socket.core_2_0_2". +-- @return table or (nil, string): The module table as returned by some other loader, +-- or nil followed by an error message if no other loader managed to load the module. +local function call_other_loaders(module, name, version, module_name) + for _, a_loader in ipairs(loaders) do + if a_loader ~= loader.luarocks_loader then + local results = { a_loader(module_name) } + if type(results[1]) == "function" then + return unpack(results) + end + end + end + return "Failed loading module "..module.." in LuaRocks rock "..name.." "..version +end + +local function add_providers(providers, entries, tree, module, filter_file_name) + for i, entry in ipairs(entries) do + local name, version = entry:match("^([^/]*)/(.*)$") + local file_name = tree.manifest.repository[name][version][1].modules[module] + if type(file_name) ~= "string" then + error("Invalid data in manifest file for module "..tostring(module).." (invalid data for "..tostring(name).." "..tostring(version)..")") + end + file_name = filter_file_name(file_name, name, version, tree.tree, i) + if loader.context[name] == version then + return name, version, file_name + end + version = vers.parse_version(version) + table.insert(providers, {name = name, version = version, module_name = file_name, tree = tree}) + end +end + +--- Search for a module in the rocks trees +-- @param module string: module name (eg. "socket.core") +-- @param filter_file_name function(string, string, string, string, number): +-- a function that takes the module file name (eg "socket/core.so"), the rock name +-- (eg "luasocket"), the version (eg "2.0.2-1"), the path of the rocks tree +-- (eg "/usr/local"), and the numeric index of the matching entry, so the +-- filter function can know if the matching module was the first entry or not. +-- @return string, string, string, (string or table): +-- * name of the rock containing the module (eg. "luasocket") +-- * version of the rock (eg. "2.0.2-1") +-- * return value of filter_file_name +-- * tree of the module (string or table in `tree_manifests` format) +local function select_module(module, filter_file_name) + --assert(type(module) == "string") + --assert(type(filter_module_name) == "function") + + local tree_manifests = manif.load_rocks_tree_manifests() + if not tree_manifests then + return nil + end + + local providers = {} + local initmodule + for _, tree in ipairs(tree_manifests) do + local entries = tree.manifest.modules[module] + if entries then + local n, v, f = add_providers(providers, entries, tree, module, filter_file_name) + if n then + return n, v, f + end + else + initmodule = initmodule or module .. ".init" + entries = tree.manifest.modules[initmodule] + if entries then + local n, v, f = add_providers(providers, entries, tree, initmodule, filter_file_name) + if n then + return n, v, f + end + end + end + end + + if next(providers) then + table.sort(providers, sort_versions) + local first = providers[1] + return first.name, first.version.string, first.module_name, first.tree + end +end + +--- Search for a module +-- @param module string: module name (eg. "socket.core") +-- @return string, string, string, (string or table): +-- * name of the rock containing the module (eg. "luasocket") +-- * version of the rock (eg. "2.0.2-1") +-- * name of the module (eg. "socket.core", or "socket.core_2_0_2" if file is stored versioned). +-- * tree of the module (string or table in `tree_manifests` format) +local function pick_module(module) + return + select_module(module, function(file_name, name, version, tree, i) + if i > 1 then + file_name = path.versioned_name(file_name, "", name, version) + end + return path.path_to_module(file_name) + end) +end + +--- Return the pathname of the file that would be loaded for a module. +-- @param module string: module name (eg. "socket.core") +-- @param where string: places to look for the module. If `where` contains +-- "l", it will search using the LuaRocks loader; if it contains "p", +-- it will look in the filesystem using package.path and package.cpath. +-- You can use both at the same time. +-- @return If successful, it will return four values. +-- * If found using the LuaRocks loader, it will return: +-- * filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so"), +-- * rock name +-- * rock version +-- * "l" to indicate the match comes from the loader. +-- * If found scanning package.path and package.cpath, it will return: +-- * filename of the module (eg. "/usr/local/lib/lua/5.1/socket/core.so"), +-- * "path" or "cpath" +-- * nil +-- * "p" to indicate the match comes from scanning package.path and cpath. +-- If unsuccessful, nothing is returned. +function loader.which(module, where) + where = where or "l" + if where:match("l") then + local rock_name, rock_version, file_name = select_module(module, path.which_i) + if rock_name then + local fd = io.open(file_name) + if fd then + fd:close() + return file_name, rock_name, rock_version, "l" + end + end + end + if where:match("p") then + local modpath = module:gsub("%.", "/") + for _, v in ipairs({"path", "cpath"}) do + for p in package[v]:gmatch("([^;]+)") do + local file_name = p:gsub("%?", modpath) -- luacheck: ignore 421 + local fd = io.open(file_name) + if fd then + fd:close() + return file_name, v, nil, "p" + end + end + end + end +end + +--- Package loader for LuaRocks support. +-- A module is searched in installed rocks that match the +-- current LuaRocks context. If module is not part of the +-- context, or if a context has not yet been set, the module +-- in the package with the highest version is used. +-- @param module string: The module name, like in plain require(). +-- @return table: The module table (typically), like in plain +-- require(). See require() +-- in the Lua reference manual for details. +function loader.luarocks_loader(module) + local name, version, module_name = pick_module(module) + if not name then + return "No LuaRocks module found for "..module + else + loader.add_context(name, version) + return call_other_loaders(module, name, version, module_name) + end +end + +table.insert(loaders, 1, loader.luarocks_loader) + +if is_clean then + for modname, _ in pairs(package.loaded) do + if modname:match("^luarocks%.") then + package.loaded[modname] = nil + end + end +end + +return loader diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/manif.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/manif.lua new file mode 100644 index 000000000..a4ddda11e --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/manif.lua @@ -0,0 +1,225 @@ +--- Module for handling manifest files and tables. +-- Manifest files describe the contents of a LuaRocks tree or server. +-- They are loaded into manifest tables, which are then used for +-- performing searches, matching dependencies, etc. +local manif = {} + +local core = require("luarocks.core.manif") +local persist = require("luarocks.persist") +local fetch = require("luarocks.fetch") +local dir = require("luarocks.dir") +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local path = require("luarocks.path") +local util = require("luarocks.util") +local queries = require("luarocks.queries") +local type_manifest = require("luarocks.type.manifest") + +manif.cache_manifest = core.cache_manifest +manif.load_rocks_tree_manifests = core.load_rocks_tree_manifests +manif.scan_dependencies = core.scan_dependencies + +manif.rock_manifest_cache = {} + +local function check_manifest(repo_url, manifest, globals) + local ok, err = type_manifest.check(manifest, globals) + if not ok then + core.cache_manifest(repo_url, cfg.lua_version, nil) + return nil, "Error checking manifest: "..err, "type" + end + return manifest +end + +local postprocess_dependencies +do + local postprocess_check = setmetatable({}, { __mode = "k" }) + postprocess_dependencies = function(manifest) + if postprocess_check[manifest] then + return + end + if manifest.dependencies then + for name, versions in pairs(manifest.dependencies) do + for version, entries in pairs(versions) do + for k, v in pairs(entries) do + entries[k] = queries.from_persisted_table(v) + end + end + end + end + postprocess_check[manifest] = true + end +end + +function manif.load_rock_manifest(name, version, root) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local name_version = name.."/"..version + if manif.rock_manifest_cache[name_version] then + return manif.rock_manifest_cache[name_version].rock_manifest + end + local pathname = path.rock_manifest_file(name, version, root) + local rock_manifest = persist.load_into_table(pathname) + if not rock_manifest then + return nil, "rock_manifest file not found for "..name.." "..version.." - not a LuaRocks tree?" + end + manif.rock_manifest_cache[name_version] = rock_manifest + return rock_manifest.rock_manifest +end + +--- Load a local or remote manifest describing a repository. +-- All functions that use manifest tables assume they were obtained +-- through this function. +-- @param repo_url string: URL or pathname for the repository. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @param versioned_only boolean: If true, do not fall back to the main manifest +-- if a versioned manifest was not found. +-- @return table or (nil, string, [string]): A table representing the manifest, +-- or nil followed by an error message and an optional error code. +function manif.load_manifest(repo_url, lua_version, versioned_only) + assert(type(repo_url) == "string") + assert(type(lua_version) == "string" or not lua_version) + lua_version = lua_version or cfg.lua_version + + local cached_manifest = core.get_cached_manifest(repo_url, lua_version) + if cached_manifest then + postprocess_dependencies(cached_manifest) + return cached_manifest + end + + local filenames = { + "manifest-"..lua_version..".zip", + "manifest-"..lua_version, + not versioned_only and "manifest" or nil, + } + + local protocol, repodir = dir.split_url(repo_url) + local pathname, from_cache + if protocol == "file" then + for _, filename in ipairs(filenames) do + pathname = dir.path(repodir, filename) + if fs.exists(pathname) then + break + end + end + else + local err, errcode + for _, filename in ipairs(filenames) do + pathname, err, errcode, from_cache = fetch.fetch_caching(dir.path(repo_url, filename), "no_mirror") + if pathname then + break + end + end + if not pathname then + return nil, err, errcode + end + end + if pathname:match(".*%.zip$") then + pathname = fs.absolute_name(pathname) + local nozip = pathname:match("(.*)%.zip$") + if not from_cache then + local dirname = dir.dir_name(pathname) + fs.change_dir(dirname) + fs.delete(nozip) + local ok, err = fs.unzip(pathname) + fs.pop_dir() + if not ok then + fs.delete(pathname) + fs.delete(pathname..".timestamp") + return nil, "Failed extracting manifest file: " .. err + end + end + pathname = nozip + end + local manifest, err, errcode = core.manifest_loader(pathname, repo_url, lua_version) + if not manifest then + return nil, err, errcode + end + + postprocess_dependencies(manifest) + return check_manifest(repo_url, manifest, err) +end + +--- Get type and name of an item (a module or a command) provided by a file. +-- @param deploy_type string: rock manifest subtree the file comes from ("bin", "lua", or "lib"). +-- @param file_path string: path to the file relatively to deploy_type subdirectory. +-- @return (string, string): item type ("module" or "command") and name. +function manif.get_provided_item(deploy_type, file_path) + assert(type(deploy_type) == "string") + assert(type(file_path) == "string") + local item_type = deploy_type == "bin" and "command" or "module" + local item_name = item_type == "command" and file_path or path.path_to_module(file_path) + return item_type, item_name +end + +local function get_providers(item_type, item_name, repo) + assert(type(item_type) == "string") + assert(type(item_name) == "string") + local rocks_dir = path.rocks_dir(repo or cfg.root_dir) + local manifest = manif.load_manifest(rocks_dir) + return manifest and manifest[item_type .. "s"][item_name] +end + +--- Given a name of a module or a command, figure out which rock name and version +-- correspond to it in the rock tree manifest. +-- @param item_type string: "module" or "command". +-- @param item_name string: module or command name. +-- @param root string or nil: A local root dir for a rocks tree. If not given, the default is used. +-- @return (string, string) or nil: name and version of the provider rock or nil if there +-- is no provider. +function manif.get_current_provider(item_type, item_name, repo) + local providers = get_providers(item_type, item_name, repo) + if providers then + return providers[1]:match("([^/]*)/([^/]*)") + end +end + +function manif.get_next_provider(item_type, item_name, repo) + local providers = get_providers(item_type, item_name, repo) + if providers and providers[2] then + return providers[2]:match("([^/]*)/([^/]*)") + end +end + +--- Get all versions of a package listed in a manifest file. +-- @param name string: a package name. +-- @param deps_mode string: "one", to use only the currently +-- configured tree; "order" to select trees based on order +-- (use the current tree and all trees below it on the list) +-- or "all", to use all trees. +-- @return table: An array of strings listing installed +-- versions of a package, and a table indicating where they are found. +function manif.get_versions(dep, deps_mode) + assert(type(dep) == "table") + assert(type(deps_mode) == "string") + + local name = dep.name + local namespace = dep.namespace + + local version_set = {} + path.map_trees(deps_mode, function(tree) + local manifest = manif.load_manifest(path.rocks_dir(tree)) + + if manifest and manifest.repository[name] then + for version in pairs(manifest.repository[name]) do + if dep.namespace then + local ns_file = path.rock_namespace_file(name, version, tree) + local fd = io.open(ns_file, "r") + if fd then + local ns = fd:read("*a") + fd:close() + if ns == namespace then + version_set[version] = tree + end + end + else + version_set[version] = tree + end + end + end + end) + + return util.keys(version_set), version_set +end + +return manif diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/manif/writer.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/manif/writer.lua new file mode 100644 index 000000000..a435d29ed --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/manif/writer.lua @@ -0,0 +1,494 @@ + +local writer = {} + +local cfg = require("luarocks.core.cfg") +local search = require("luarocks.search") +local repos = require("luarocks.repos") +local deps = require("luarocks.deps") +local vers = require("luarocks.core.vers") +local fs = require("luarocks.fs") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local fetch = require("luarocks.fetch") +local path = require("luarocks.path") +local persist = require("luarocks.persist") +local manif = require("luarocks.manif") +local queries = require("luarocks.queries") + +--- Update storage table to account for items provided by a package. +-- @param storage table: a table storing items in the following format: +-- keys are item names and values are arrays of packages providing each item, +-- where a package is specified as string `name/version`. +-- @param items table: a table mapping item names to paths. +-- @param name string: package name. +-- @param version string: package version. +local function store_package_items(storage, name, version, items) + assert(type(storage) == "table") + assert(type(items) == "table") + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local package_identifier = name.."/"..version + + for item_name, path in pairs(items) do -- luacheck: ignore 431 + if not storage[item_name] then + storage[item_name] = {} + end + + table.insert(storage[item_name], package_identifier) + end +end + +--- Update storage table removing items provided by a package. +-- @param storage table: a table storing items in the following format: +-- keys are item names and values are arrays of packages providing each item, +-- where a package is specified as string `name/version`. +-- @param items table: a table mapping item names to paths. +-- @param name string: package name. +-- @param version string: package version. +local function remove_package_items(storage, name, version, items) + assert(type(storage) == "table") + assert(type(items) == "table") + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local package_identifier = name.."/"..version + + for item_name, path in pairs(items) do -- luacheck: ignore 431 + local key = item_name + local all_identifiers = storage[key] + if not all_identifiers then + key = key .. ".init" + all_identifiers = storage[key] + end + + if all_identifiers then + for i, identifier in ipairs(all_identifiers) do + if identifier == package_identifier then + table.remove(all_identifiers, i) + break + end + end + + if #all_identifiers == 0 then + storage[key] = nil + end + else + util.warning("Cannot find entry for " .. item_name .. " in manifest -- corrupted manifest?") + end + end +end + +--- Process the dependencies of a manifest table to determine its dependency +-- chains for loading modules. The manifest dependencies information is filled +-- and any dependency inconsistencies or missing dependencies are reported to +-- standard error. +-- @param manifest table: a manifest table. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for no trees. +local function update_dependencies(manifest, deps_mode) + assert(type(manifest) == "table") + assert(type(deps_mode) == "string") + + for pkg, versions in pairs(manifest.repository) do + for version, repositories in pairs(versions) do + for _, repo in ipairs(repositories) do + if repo.arch == "installed" then + repo.dependencies = {} + deps.scan_deps(repo.dependencies, manifest, pkg, version, deps_mode) + repo.dependencies[pkg] = nil + end + end + end + end +end + + + +--- Sort function for ordering rock identifiers in a manifest's +-- modules table. Rocks are ordered alphabetically by name, and then +-- by version which greater first. +-- @param a string: Version to compare. +-- @param b string: Version to compare. +-- @return boolean: The comparison result, according to the +-- rule outlined above. +local function sort_pkgs(a, b) + assert(type(a) == "string") + assert(type(b) == "string") + + local na, va = a:match("(.*)/(.*)$") + local nb, vb = b:match("(.*)/(.*)$") + + return (na == nb) and vers.compare_versions(va, vb) or na < nb +end + +--- Sort items of a package matching table by version number (higher versions first). +-- @param tbl table: the package matching table: keys should be strings +-- and values arrays of strings with packages names in "name/version" format. +local function sort_package_matching_table(tbl) + assert(type(tbl) == "table") + + if next(tbl) then + for item, pkgs in pairs(tbl) do + if #pkgs > 1 then + table.sort(pkgs, sort_pkgs) + -- Remove duplicates from the sorted array. + local prev = nil + local i = 1 + while pkgs[i] do + local curr = pkgs[i] + if curr == prev then + table.remove(pkgs, i) + else + prev = curr + i = i + 1 + end + end + end + end + end +end + +--- Filter manifest table by Lua version, removing rockspecs whose Lua version +-- does not match. +-- @param manifest table: a manifest table. +-- @param lua_version string or nil: filter by Lua version +-- @param repodir string: directory of repository being scanned +-- @param cache table: temporary rockspec cache table +local function filter_by_lua_version(manifest, lua_version, repodir, cache) + assert(type(manifest) == "table") + assert(type(repodir) == "string") + assert((not cache) or type(cache) == "table") + + cache = cache or {} + lua_version = vers.parse_version(lua_version) + for pkg, versions in pairs(manifest.repository) do + local to_remove = {} + for version, repositories in pairs(versions) do + for _, repo in ipairs(repositories) do + if repo.arch == "rockspec" then + local pathname = dir.path(repodir, pkg.."-"..version..".rockspec") + local rockspec, err = cache[pathname] + if not rockspec then + rockspec, err = fetch.load_local_rockspec(pathname, true) + end + if rockspec then + cache[pathname] = rockspec + for _, dep in ipairs(rockspec.dependencies) do + if dep.name == "lua" then + if not vers.match_constraints(lua_version, dep.constraints) then + table.insert(to_remove, version) + end + break + end + end + else + util.printerr("Error loading rockspec for "..pkg.." "..version..": "..err) + end + end + end + end + if next(to_remove) then + for _, incompat in ipairs(to_remove) do + versions[incompat] = nil + end + if not next(versions) then + manifest.repository[pkg] = nil + end + end + end +end + +--- Store search results in a manifest table. +-- @param results table: The search results as returned by search.disk_search. +-- @param manifest table: A manifest table (must contain repository, modules, commands tables). +-- It will be altered to include the search results. +-- @return boolean or (nil, string): true in case of success, or nil followed by an error message. +local function store_results(results, manifest) + assert(type(results) == "table") + assert(type(manifest) == "table") + + for name, versions in pairs(results) do + local pkgtable = manifest.repository[name] or {} + for version, entries in pairs(versions) do + local versiontable = {} + for _, entry in ipairs(entries) do + local entrytable = {} + entrytable.arch = entry.arch + if entry.arch == "installed" then + local rock_manifest, err = manif.load_rock_manifest(name, version) + if not rock_manifest then return nil, err end + + entrytable.modules = repos.package_modules(name, version) + store_package_items(manifest.modules, name, version, entrytable.modules) + entrytable.commands = repos.package_commands(name, version) + store_package_items(manifest.commands, name, version, entrytable.commands) + end + table.insert(versiontable, entrytable) + end + pkgtable[version] = versiontable + end + manifest.repository[name] = pkgtable + end + sort_package_matching_table(manifest.modules) + sort_package_matching_table(manifest.commands) + return true +end + +--- Commit a table to disk in given local path. +-- @param where string: The directory where the table should be saved. +-- @param name string: The filename. +-- @param tbl table: The table to be saved. +-- @return boolean or (nil, string): true if successful, or nil and a +-- message in case of errors. +local function save_table(where, name, tbl) + assert(type(where) == "string") + assert(type(name) == "string" and not name:match("/")) + assert(type(tbl) == "table") + + local filename = dir.path(where, name) + local ok, err = persist.save_from_table(filename..".tmp", tbl) + if ok then + ok, err = fs.replace_file(filename, filename..".tmp") + end + return ok, err +end + +function writer.make_rock_manifest(name, version) + local install_dir = path.install_dir(name, version) + local tree = {} + for _, file in ipairs(fs.find(install_dir)) do + local full_path = dir.path(install_dir, file) + local walk = tree + local last + local last_name + for filename in file:gmatch("[^/]+") do + local next = walk[filename] + if not next then + next = {} + walk[filename] = next + end + last = walk + last_name = filename + walk = next + end + if fs.is_file(full_path) then + local sum, err = fs.get_md5(full_path) + if not sum then + return nil, "Failed producing checksum: "..tostring(err) + end + last[last_name] = sum + end + end + local rock_manifest = { rock_manifest=tree } + manif.rock_manifest_cache[name.."/"..version] = rock_manifest + save_table(install_dir, "rock_manifest", rock_manifest ) + return true +end + +-- Writes a 'rock_namespace' file in a locally installed rock directory. +-- @param name string: the rock name, without a namespace +-- @param version string: the rock version +-- @param namespace string?: the namespace +-- @return true if successful (or unnecessary, if there is no namespace), +-- or nil and an error message. +function writer.make_namespace_file(name, version, namespace) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(namespace) == "string" or not namespace) + if not namespace then + return true + end + local fd, err = io.open(path.rock_namespace_file(name, version), "w") + if not fd then + return nil, err + end + local ok, err = fd:write(namespace) + if not ok then + return nil, err + end + fd:close() + return true +end + +--- Scan a LuaRocks repository and output a manifest file. +-- A file called 'manifest' will be written in the root of the given +-- repository directory. +-- @param repo A local repository directory. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for the default dependency mode from the configuration. +-- @param remote boolean: 'true' if making a manifest for a rocks server. +-- @return boolean or (nil, string): True if manifest was generated, +-- or nil and an error message. +function writer.make_manifest(repo, deps_mode, remote) + assert(type(repo) == "string") + assert(type(deps_mode) == "string") + + if deps_mode == "none" then deps_mode = cfg.deps_mode end + + if not fs.is_dir(repo) then + return nil, "Cannot access repository at "..repo + end + + local query = queries.all("any") + local results = search.disk_search(repo, query) + local manifest = { repository = {}, modules = {}, commands = {} } + + manif.cache_manifest(repo, nil, manifest) + + local ok, err = store_results(results, manifest) + if not ok then return nil, err end + + if remote then + local cache = {} + for luaver in util.lua_versions() do + local vmanifest = { repository = {}, modules = {}, commands = {} } + local ok, err = store_results(results, vmanifest) + filter_by_lua_version(vmanifest, luaver, repo, cache) + if not cfg.no_manifest then + save_table(repo, "manifest-"..luaver, vmanifest) + end + end + else + update_dependencies(manifest, deps_mode) + end + + if cfg.no_manifest then + -- We want to have cache updated; but exit before save_table is called + return true + end + return save_table(repo, "manifest", manifest) +end + +--- Update manifest file for a local repository +-- adding information about a version of a package installed in that repository. +-- @param name string: Name of a package from the repository. +-- @param version string: Version of a package from the repository. +-- @param repo string or nil: Pathname of a local repository. If not given, +-- the default local repository is used. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for using the default dependency mode from the configuration. +-- @return boolean or (nil, string): True if manifest was updated successfully, +-- or nil and an error message. +function writer.add_to_manifest(name, version, repo, deps_mode) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + local rocks_dir = path.rocks_dir(repo or cfg.root_dir) + assert(type(deps_mode) == "string") + + if deps_mode == "none" then deps_mode = cfg.deps_mode end + + local manifest, err = manif.load_manifest(rocks_dir) + if not manifest then + util.printerr("No existing manifest. Attempting to rebuild...") + -- Manifest built by `writer.make_manifest` should already + -- include information about given name and version, + -- no need to update it. + return writer.make_manifest(rocks_dir, deps_mode) + end + + local results = {[name] = {[version] = {{arch = "installed", repo = rocks_dir}}}} + + local ok, err = store_results(results, manifest) + if not ok then return nil, err end + + update_dependencies(manifest, deps_mode) + + if cfg.no_manifest then + return true + end + return save_table(rocks_dir, "manifest", manifest) +end + +--- Update manifest file for a local repository +-- removing information about a version of a package. +-- @param name string: Name of a package removed from the repository. +-- @param version string: Version of a package removed from the repository. +-- @param repo string or nil: Pathname of a local repository. If not given, +-- the default local repository is used. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for using the default dependency mode from the configuration. +-- @return boolean or (nil, string): True if manifest was updated successfully, +-- or nil and an error message. +function writer.remove_from_manifest(name, version, repo, deps_mode) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + local rocks_dir = path.rocks_dir(repo or cfg.root_dir) + assert(type(deps_mode) == "string") + + if deps_mode == "none" then deps_mode = cfg.deps_mode end + + local manifest, err = manif.load_manifest(rocks_dir) + if not manifest then + util.printerr("No existing manifest. Attempting to rebuild...") + -- Manifest built by `writer.make_manifest` should already + -- include up-to-date information, no need to update it. + return writer.make_manifest(rocks_dir, deps_mode) + end + + local package_entry = manifest.repository[name] + if package_entry == nil or package_entry[version] == nil then + -- entry is already missing from repository, no need to do anything + return true + end + + local version_entry = package_entry[version][1] + if not version_entry then + -- manifest looks corrupted, rebuild + return writer.make_manifest(rocks_dir, deps_mode) + end + + remove_package_items(manifest.modules, name, version, version_entry.modules) + remove_package_items(manifest.commands, name, version, version_entry.commands) + + package_entry[version] = nil + manifest.dependencies[name][version] = nil + + if not next(package_entry) then + -- No more versions of this package. + manifest.repository[name] = nil + manifest.dependencies[name] = nil + end + + update_dependencies(manifest, deps_mode) + + if cfg.no_manifest then + return true + end + return save_table(rocks_dir, "manifest", manifest) +end + +--- Report missing dependencies for all rocks installed in a repository. +-- @param repo string or nil: Pathname of a local repository. If not given, +-- the default local repository is used. +-- @param deps_mode string: Dependency mode: "one" for the current default tree, +-- "all" for all trees, "order" for all trees with priority >= the current default, +-- "none" for using the default dependency mode from the configuration. +function writer.check_dependencies(repo, deps_mode) + local rocks_dir = path.rocks_dir(repo or cfg.root_dir) + assert(type(deps_mode) == "string") + if deps_mode == "none" then deps_mode = cfg.deps_mode end + + local manifest = manif.load_manifest(rocks_dir) + if not manifest then + return + end + + for name, versions in util.sortedpairs(manifest.repository) do + for version, version_entries in util.sortedpairs(versions, vers.compare_versions) do + for _, entry in ipairs(version_entries) do + if entry.arch == "installed" then + if manifest.dependencies[name] and manifest.dependencies[name][version] then + deps.report_missing_dependencies(name, version, manifest.dependencies[name][version], deps_mode, util.get_rocks_provided()) + end + end + end + end + end +end + +return writer diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/pack.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/pack.lua new file mode 100644 index 000000000..5d70a854c --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/pack.lua @@ -0,0 +1,183 @@ + +-- Create rock files, packing sources or binaries. +local pack = {} + +local unpack = unpack or table.unpack + +local queries = require("luarocks.queries") +local path = require("luarocks.path") +local repos = require("luarocks.repos") +local fetch = require("luarocks.fetch") +local fs = require("luarocks.fs") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local manif = require("luarocks.manif") +local search = require("luarocks.search") +local signing = require("luarocks.signing") + +--- Create a source rock. +-- Packages a rockspec and its required source files in a rock +-- file with the .src.rock extension, which can later be built and +-- installed with the "build" command. +-- @param rockspec_file string: An URL or pathname for a rockspec file. +-- @return string or (nil, string): The filename of the resulting +-- .src.rock file; or nil and an error message. +function pack.pack_source_rock(rockspec_file) + assert(type(rockspec_file) == "string") + + local rockspec, err = fetch.load_rockspec(rockspec_file) + if err then + return nil, "Error loading rockspec: "..err + end + rockspec_file = rockspec.local_abs_filename + + local name_version = rockspec.name .. "-" .. rockspec.version + local rock_file = fs.absolute_name(name_version .. ".src.rock") + + local temp_dir, err = fs.make_temp_dir("pack-"..name_version) + if not temp_dir then + return nil, "Failed creating temporary directory: "..err + end + util.schedule_function(fs.delete, temp_dir) + + local source_file, source_dir = fetch.fetch_sources(rockspec, true, temp_dir) + if not source_file then + return nil, source_dir + end + local ok, err = fs.change_dir(source_dir) + if not ok then return nil, err end + + fs.delete(rock_file) + fs.copy(rockspec_file, source_dir, "read") + ok, err = fs.zip(rock_file, dir.base_name(rockspec_file), dir.base_name(source_file)) + if not ok then + return nil, "Failed packing "..rock_file.." - "..err + end + fs.pop_dir() + + return rock_file +end + +local function copy_back_files(name, version, file_tree, deploy_dir, pack_dir, perms) + local ok, err = fs.make_dir(pack_dir) + if not ok then return nil, err end + for file, sub in pairs(file_tree) do + local source = dir.path(deploy_dir, file) + local target = dir.path(pack_dir, file) + if type(sub) == "table" then + local ok, err = copy_back_files(name, version, sub, source, target) + if not ok then return nil, err end + else + local versioned = path.versioned_name(source, deploy_dir, name, version) + if fs.exists(versioned) then + fs.copy(versioned, target, perms) + else + fs.copy(source, target, perms) + end + end + end + return true +end + +-- @param name string: Name of package to pack. +-- @param version string or nil: A version number may also be passed. +-- @param tree string or nil: An optional tree to pick the package from. +-- @return string or (nil, string): The filename of the resulting +-- .src.rock file; or nil and an error message. +function pack.pack_installed_rock(query, tree) + + local name, version, repo, repo_url = search.pick_installed_rock(query, tree) + if not name then + return nil, version + end + + local root = path.root_from_rocks_dir(repo_url) + local prefix = path.install_dir(name, version, root) + if not fs.exists(prefix) then + return nil, "'"..name.." "..version.."' does not seem to be an installed rock." + end + + local rock_manifest, err = manif.load_rock_manifest(name, version, root) + if not rock_manifest then return nil, err end + + local name_version = name .. "-" .. version + local rock_file = fs.absolute_name(name_version .. "."..cfg.arch..".rock") + + local temp_dir = fs.make_temp_dir("pack") + fs.copy_contents(prefix, temp_dir) + + local is_binary = false + if rock_manifest.lib then + local ok, err = copy_back_files(name, version, rock_manifest.lib, path.deploy_lib_dir(repo), dir.path(temp_dir, "lib"), "exec") + if not ok then return nil, "Failed copying back files: " .. err end + is_binary = true + end + if rock_manifest.lua then + local ok, err = copy_back_files(name, version, rock_manifest.lua, path.deploy_lua_dir(repo), dir.path(temp_dir, "lua"), "read") + if not ok then return nil, "Failed copying back files: " .. err end + end + + local ok, err = fs.change_dir(temp_dir) + if not ok then return nil, err end + if not is_binary and not repos.has_binaries(name, version) then + rock_file = rock_file:gsub("%."..cfg.arch:gsub("%-","%%-").."%.", ".all.") + end + fs.delete(rock_file) + if not fs.zip(rock_file, unpack(fs.list_dir())) then + return nil, "Failed packing "..rock_file + end + fs.pop_dir() + fs.delete(temp_dir) + return rock_file +end + +function pack.report_and_sign_local_file(file, err, sign) + if err then + return nil, err + end + local sigfile + if sign then + sigfile, err = signing.sign_file(file) + util.printout() + end + util.printout("Packed: "..file) + if sigfile then + util.printout("Signature stored in: "..sigfile) + end + if err then + return nil, err + end + return true +end + +function pack.pack_binary_rock(name, namespace, version, sign, cmd) + + -- The --pack-binary-rock option for "luarocks build" basically performs + -- "luarocks build" on a temporary tree and then "luarocks pack". The + -- alternative would require refactoring parts of luarocks.build and + -- luarocks.pack, which would save a few file operations: the idea would be + -- to shave off the final deploy steps from the build phase and the initial + -- collect steps from the pack phase. + + local temp_dir, err = fs.make_temp_dir("luarocks-build-pack-"..dir.base_name(name)) + if not temp_dir then + return nil, "Failed creating temporary directory: "..err + end + util.schedule_function(fs.delete, temp_dir) + + path.use_tree(temp_dir) + local ok, err = cmd() + if not ok then + return nil, err + end + local rname, rversion = path.parse_name(name) + if not rname then + rname, rversion = name, version + end + local query = queries.new(rname, namespace, rversion) + local file, err = pack.pack_installed_rock(query, temp_dir) + return pack.report_and_sign_local_file(file, err, sign) +end + +return pack diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/path.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/path.lua new file mode 100644 index 000000000..a9f04ef05 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/path.lua @@ -0,0 +1,262 @@ + +--- LuaRocks-specific path handling functions. +-- All paths are configured in this module, making it a single +-- point where the layout of the local installation is defined in LuaRocks. +local path = {} + +local core = require("luarocks.core.path") +local dir = require("luarocks.dir") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") + +path.rocks_dir = core.rocks_dir +path.versioned_name = core.versioned_name +path.path_to_module = core.path_to_module +path.deploy_lua_dir = core.deploy_lua_dir +path.deploy_lib_dir = core.deploy_lib_dir +path.map_trees = core.map_trees +path.rocks_tree_to_string = core.rocks_tree_to_string + +--- Infer rockspec filename from a rock filename. +-- @param rock_name string: Pathname of a rock file. +-- @return string: Filename of the rockspec, without path. +function path.rockspec_name_from_rock(rock_name) + assert(type(rock_name) == "string") + local base_name = dir.base_name(rock_name) + return base_name:match("(.*)%.[^.]*.rock") .. ".rockspec" +end + +function path.root_from_rocks_dir(rocks_dir) + assert(type(rocks_dir) == "string") + return rocks_dir:match("(.*)" .. util.matchquote(cfg.rocks_subdir) .. ".*$") +end + +function path.root_dir(tree) + if type(tree) == "string" then + return tree + else + assert(type(tree) == "table") + return tree.root + end +end + +function path.deploy_bin_dir(tree) + return dir.path(path.root_dir(tree), "bin") +end + +function path.manifest_file(tree) + return dir.path(path.rocks_dir(tree), "manifest") +end + +--- Get the directory for all versions of a package in a tree. +-- @param name string: The package name. +-- @return string: The resulting path -- does not guarantee that +-- @param tree string or nil: If given, specifies the local tree to use. +-- the package (and by extension, the path) exists. +function path.versions_dir(name, tree) + assert(type(name) == "string" and not name:match("/")) + return dir.path(path.rocks_dir(tree), name) +end + +--- Get the local installation directory (prefix) for a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.install_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version) +end + +--- Get the local filename of the rockspec of an installed rock. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the file) exists. +function path.rockspec_file(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, name.."-"..version..".rockspec") +end + +--- Get the local filename of the rock_manifest file of an installed rock. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the file) exists. +function path.rock_manifest_file(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "rock_manifest") +end + +--- Get the local filename of the rock_namespace file of an installed rock. +-- @param name string: The package name (without a namespace). +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the file) exists. +function path.rock_namespace_file(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "rock_namespace") +end + +--- Get the local installation directory for C libraries of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.lib_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "lib") +end + +--- Get the local installation directory for Lua modules of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.lua_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "lua") +end + +--- Get the local installation directory for documentation of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.doc_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "doc") +end + +--- Get the local installation directory for configuration files of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.conf_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "conf") +end + +--- Get the local installation directory for command-line scripts +-- of a package. +-- @param name string: The package name. +-- @param version string: The package version. +-- @param tree string or nil: If given, specifies the local tree to use. +-- @return string: The resulting path -- does not guarantee that +-- the package (and by extension, the path) exists. +function path.bin_dir(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + return dir.path(path.rocks_dir(tree), name, version, "bin") +end + +--- Extract name, version and arch of a rock filename, +-- or name, version and "rockspec" from a rockspec name. +-- @param file_name string: pathname of a rock or rockspec +-- @return (string, string, string) or nil: name, version and arch +-- or nil if name could not be parsed +function path.parse_name(file_name) + assert(type(file_name) == "string") + if file_name:match("%.rock$") then + return dir.base_name(file_name):match("(.*)-([^-]+-%d+)%.([^.]+)%.rock$") + else + return dir.base_name(file_name):match("(.*)-([^-]+-%d+)%.(rockspec)") + end +end + +--- Make a rockspec or rock URL. +-- @param pathname string: Base URL or pathname. +-- @param name string: Package name. +-- @param version string: Package version. +-- @param arch string: Architecture identifier, or "rockspec" or "installed". +-- @return string: A URL or pathname following LuaRocks naming conventions. +function path.make_url(pathname, name, version, arch) + assert(type(pathname) == "string") + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(arch) == "string") + + local filename = name.."-"..version + if arch == "installed" then + filename = dir.path(name, version, filename..".rockspec") + elseif arch == "rockspec" then + filename = filename..".rockspec" + else + filename = filename.."."..arch..".rock" + end + return dir.path(pathname, filename) +end + +--- Obtain the directory name where a module should be stored. +-- For example, on Unix, "foo.bar.baz" will return "foo/bar". +-- @param mod string: A module name in Lua dot-separated format. +-- @return string: A directory name using the platform's separator. +function path.module_to_path(mod) + assert(type(mod) == "string") + return (mod:gsub("[^.]*$", ""):gsub("%.", "/")) +end + +function path.use_tree(tree) + cfg.root_dir = tree + cfg.rocks_dir = path.rocks_dir(tree) + cfg.deploy_bin_dir = path.deploy_bin_dir(tree) + cfg.deploy_lua_dir = path.deploy_lua_dir(tree) + cfg.deploy_lib_dir = path.deploy_lib_dir(tree) + + -- TODO Do we want LuaRocks itself to use whatever tree is in use? + -- package.path = dir.path(path.deploy_lua_dir(tree), "?.lua") .. ";" + -- .. dir.path(path.deploy_lua_dir(tree), "?/init.lua") .. ";" + -- .. package.path + -- package.cpath = dir.path(path.deploy_lib_dir(tree), "?." .. cfg.lib_extension) .. ";" + -- .. package.cpath +end + +--- Get the namespace of a locally-installed rock, if any. +-- @param name string: The rock name, without a namespace. +-- @param version string: The rock version. +-- @param tree string: The local tree to use. +-- @return string?: The namespace if it exists, or nil. +function path.read_namespace(name, version, tree) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(tree) == "string") + + local namespace + local fd = io.open(path.rock_namespace_file(name, version, tree), "r") + if fd then + namespace = fd:read("*a") + fd:close() + end + return namespace +end + +function path.package_paths(deps_mode) + local lpaths = {} + local lcpaths = {} + path.map_trees(deps_mode, function(tree) + local root = path.root_dir(tree) + table.insert(lpaths, dir.path(root, cfg.lua_modules_path, "?.lua")) + table.insert(lpaths, dir.path(root, cfg.lua_modules_path, "?/init.lua")) + table.insert(lcpaths, dir.path(root, cfg.lib_modules_path, "?." .. cfg.lib_extension)) + end) + return table.concat(lpaths, ";"), table.concat(lcpaths, ";") +end + +return path diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/persist.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/persist.lua new file mode 100644 index 000000000..4dcd930a9 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/persist.lua @@ -0,0 +1,259 @@ + +--- Utility module for loading files into tables and +-- saving tables into files. +local persist = {} + +local core = require("luarocks.core.persist") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local fs = require("luarocks.fs") + +persist.run_file = core.run_file +persist.load_into_table = core.load_into_table + +local write_table + +--- Write a value as Lua code. +-- This function handles only numbers and strings, invoking write_table +-- to write tables. +-- @param out table or userdata: a writer object supporting :write() method. +-- @param v: the value to be written. +-- @param level number: the indentation level +-- @param sub_order table: optional prioritization table +-- @see write_table +function persist.write_value(out, v, level, sub_order) + if type(v) == "table" then + level = level or 0 + write_table(out, v, level + 1, sub_order) + elseif type(v) == "string" then + if v:match("[\r\n]") then + local open, close = "[[", "]]" + local equals = 0 + local v_with_bracket = v.."]" + while v_with_bracket:find(close, 1, true) do + equals = equals + 1 + local eqs = ("="):rep(equals) + open, close = "["..eqs.."[", "]"..eqs.."]" + end + out:write(open.."\n"..v..close) + else + out:write(("%q"):format(v)) + end + else + out:write(tostring(v)) + end +end + +local is_valid_plain_key +do + local keywords = { + ["and"] = true, + ["break"] = true, + ["do"] = true, + ["else"] = true, + ["elseif"] = true, + ["end"] = true, + ["false"] = true, + ["for"] = true, + ["function"] = true, + ["goto"] = true, + ["if"] = true, + ["in"] = true, + ["local"] = true, + ["nil"] = true, + ["not"] = true, + ["or"] = true, + ["repeat"] = true, + ["return"] = true, + ["then"] = true, + ["true"] = true, + ["until"] = true, + ["while"] = true, + } + function is_valid_plain_key(k) + return type(k) == "string" + and k:match("^[a-zA-Z_][a-zA-Z0-9_]*$") + and not keywords[k] + end +end + +local function write_table_key_assignment(out, k, level) + if is_valid_plain_key(k) then + out:write(k) + else + out:write("[") + persist.write_value(out, k, level) + out:write("]") + end + + out:write(" = ") +end + +--- Write a table as Lua code in curly brackets notation to a writer object. +-- Only numbers, strings and tables (containing numbers, strings +-- or other recursively processed tables) are supported. +-- @param out table or userdata: a writer object supporting :write() method. +-- @param tbl table: the table to be written. +-- @param level number: the indentation level +-- @param field_order table: optional prioritization table +write_table = function(out, tbl, level, field_order) + out:write("{") + local sep = "\n" + local indentation = " " + local indent = true + local i = 1 + for k, v, sub_order in util.sortedpairs(tbl, field_order) do + out:write(sep) + if indent then + for _ = 1, level do out:write(indentation) end + end + + if k == i then + i = i + 1 + else + write_table_key_assignment(out, k, level) + end + + persist.write_value(out, v, level, sub_order) + if type(v) == "number" then + sep = ", " + indent = false + else + sep = ",\n" + indent = true + end + end + if sep ~= "\n" then + out:write("\n") + for _ = 1, level - 1 do out:write(indentation) end + end + out:write("}") +end + +--- Write a table as series of assignments to a writer object. +-- @param out table or userdata: a writer object supporting :write() method. +-- @param tbl table: the table to be written. +-- @param field_order table: optional prioritization table +-- @return true if successful; nil and error message if failed. +local function write_table_as_assignments(out, tbl, field_order) + for k, v, sub_order in util.sortedpairs(tbl, field_order) do + if not is_valid_plain_key(k) then + return nil, "cannot store '"..tostring(k).."' as a plain key." + end + out:write(k.." = ") + persist.write_value(out, v, 0, sub_order) + out:write("\n") + end + return true +end + +--- Write a table using Lua table syntax to a writer object. +-- @param out table or userdata: a writer object supporting :write() method. +-- @param tbl table: the table to be written. +local function write_table_as_table(out, tbl) + out:write("return {\n") + for k, v, sub_order in util.sortedpairs(tbl) do + out:write(" ") + write_table_key_assignment(out, k, 1) + persist.write_value(out, v, 1, sub_order) + out:write(",\n") + end + out:write("}\n") +end + +--- Save the contents of a table to a string. +-- Each element of the table is saved as a global assignment. +-- Only numbers, strings and tables (containing numbers, strings +-- or other recursively processed tables) are supported. +-- @param tbl table: the table containing the data to be written +-- @param field_order table: an optional array indicating the order of top-level fields. +-- @return persisted data as string; or nil and an error message +function persist.save_from_table_to_string(tbl, field_order) + local out = {buffer = {}} + function out:write(data) table.insert(self.buffer, data) end + local ok, err = write_table_as_assignments(out, tbl, field_order) + if not ok then + return nil, err + end + return table.concat(out.buffer) +end + +--- Save the contents of a table in a file. +-- Each element of the table is saved as a global assignment. +-- Only numbers, strings and tables (containing numbers, strings +-- or other recursively processed tables) are supported. +-- @param filename string: the output filename +-- @param tbl table: the table containing the data to be written +-- @param field_order table: an optional array indicating the order of top-level fields. +-- @return boolean or (nil, string): true if successful, or nil and a +-- message in case of errors. +function persist.save_from_table(filename, tbl, field_order) + local prefix = dir.dir_name(filename) + fs.make_dir(prefix) + local out = io.open(filename, "w") + if not out then + return nil, "Cannot create file at "..filename + end + local ok, err = write_table_as_assignments(out, tbl, field_order) + out:close() + if not ok then + return nil, err + end + return true +end + +--- Save the contents of a table as a module. +-- The module contains a 'return' statement that returns the table. +-- Only numbers, strings and tables (containing numbers, strings +-- or other recursively processed tables) are supported. +-- @param filename string: the output filename +-- @param tbl table: the table containing the data to be written +-- @return boolean or (nil, string): true if successful, or nil and a +-- message in case of errors. +function persist.save_as_module(filename, tbl) + local out = io.open(filename, "w") + if not out then + return nil, "Cannot create file at "..filename + end + write_table_as_table(out, tbl) + out:close() + return true +end + +function persist.load_config_file_if_basic(filename, cfg) + local env = { + home = cfg.home + } + local result, err, errcode = persist.load_into_table(filename, env) + if errcode == "load" or errcode == "run" then + -- bad config file or depends on env, so error out + return nil, "Could not read existing config file " .. filename + end + + local tbl + if errcode == "open" then + -- could not open, maybe file does not exist + tbl = {} + else + tbl = result + tbl.home = nil + end + + return tbl +end + +function persist.save_default_lua_version(prefix, lua_version) + local ok, err = fs.make_dir(prefix) + if not ok then + return nil, err + end + local fd, err = io.open(dir.path(prefix, "default-lua-version.lua"), "w") + if not fd then + return nil, err + end + fd:write('return "' .. lua_version .. '"\n') + fd:close() + return true +end + +return persist diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/queries.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/queries.lua new file mode 100644 index 000000000..6df61296e --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/queries.lua @@ -0,0 +1,215 @@ + +local queries = {} + +local vers = require("luarocks.core.vers") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") + +local query_mt = {} + +query_mt.__index = query_mt + +function query_mt.type() + return "query" +end + +-- Fallback default value for the `arch` field, if not explicitly set. +query_mt.arch = { + src = true, + all = true, + rockspec = true, + installed = true, + -- [cfg.arch] = true, -- this is set later +} + +-- Fallback default value for the `substring` field, if not explicitly set. +query_mt.substring = false + +--- Convert the arch field of a query table to table format. +-- @param input string, table or nil +local function arch_to_table(input) + if type(input) == "table" then + return input + elseif type(input) == "string" then + local arch = {} + for a in input:gmatch("[%w_-]+") do + arch[a] = true + end + return arch + end +end + +--- Prepare a query in dependency table format. +-- @param name string: the package name. +-- @param namespace string?: the package namespace. +-- @param version string?: the package version. +-- @param substring boolean?: match substrings of the name +-- (default is false, match full name) +-- @param arch string?: a string with pipe-separated accepted arch values +-- @param operator string?: operator for version matching (default is "==") +-- @return table: A query in table format +function queries.new(name, namespace, version, substring, arch, operator) + assert(type(name) == "string") + assert(type(namespace) == "string" or not namespace) + assert(type(version) == "string" or not version) + assert(type(substring) == "boolean" or not substring) + assert(type(arch) == "string" or not arch) + assert(type(operator) == "string" or not operator) + + operator = operator or "==" + + local self = { + name = name, + namespace = namespace, + constraints = {}, + substring = substring, + arch = arch_to_table(arch), + } + if version then + table.insert(self.constraints, { op = operator, version = vers.parse_version(version)}) + end + + query_mt.arch[cfg.arch] = true + return setmetatable(self, query_mt) +end + +-- Query for all packages +-- @param arch string (optional) +function queries.all(arch) + assert(type(arch) == "string" or not arch) + + return queries.new("", nil, nil, true, arch) +end + +do + local parse_constraints + do + local parse_constraint + do + local operators = { + ["=="] = "==", + ["~="] = "~=", + [">"] = ">", + ["<"] = "<", + [">="] = ">=", + ["<="] = "<=", + ["~>"] = "~>", + -- plus some convenience translations + [""] = "==", + ["="] = "==", + ["!="] = "~=" + } + + --- Consumes a constraint from a string, converting it to table format. + -- For example, a string ">= 1.0, > 2.0" is converted to a table in the + -- format {op = ">=", version={1,0}} and the rest, "> 2.0", is returned + -- back to the caller. + -- @param input string: A list of constraints in string format. + -- @return (table, string) or nil: A table representing the same + -- constraints and the string with the unused input, or nil if the + -- input string is invalid. + parse_constraint = function(input) + assert(type(input) == "string") + + local no_upgrade, op, version, rest = input:match("^(@?)([<>=~!]*)%s*([%w%.%_%-]+)[%s,]*(.*)") + local _op = operators[op] + version = vers.parse_version(version) + if not _op then + return nil, "Encountered bad constraint operator: '"..tostring(op).."' in '"..input.."'" + end + if not version then + return nil, "Could not parse version from constraint: '"..input.."'" + end + return { op = _op, version = version, no_upgrade = no_upgrade=="@" and true or nil }, rest + end + end + + --- Convert a list of constraints from string to table format. + -- For example, a string ">= 1.0, < 2.0" is converted to a table in the format + -- {{op = ">=", version={1,0}}, {op = "<", version={2,0}}}. + -- Version tables use a metatable allowing later comparison through + -- relational operators. + -- @param input string: A list of constraints in string format. + -- @return table or nil: A table representing the same constraints, + -- or nil if the input string is invalid. + parse_constraints = function(input) + assert(type(input) == "string") + + local constraints, oinput, constraint = {}, input + while #input > 0 do + constraint, input = parse_constraint(input) + if constraint then + table.insert(constraints, constraint) + else + return nil, "Failed to parse constraint '"..tostring(oinput).."' with error: ".. input + end + end + return constraints + end + end + + --- Prepare a query in dependency table format. + -- @param depstr string: A dependency in string format + -- as entered in rockspec files. + -- @return table: A query in table format, or nil and an error message in case of errors. + function queries.from_dep_string(depstr) + assert(type(depstr) == "string") + + local ns_name, rest = depstr:match("^%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)") + if not ns_name then + return nil, "failed to extract dependency name from '"..depstr.."'" + end + + local constraints, err = parse_constraints(rest) + if not constraints then + return nil, err + end + + local name, namespace = util.split_namespace(ns_name) + + local self = { + name = name, + namespace = namespace, + constraints = constraints, + } + + query_mt.arch[cfg.arch] = true + return setmetatable(self, query_mt) + end +end + +function queries.from_persisted_table(tbl) + query_mt.arch[cfg.arch] = true + return setmetatable(tbl, query_mt) +end + +--- Build a string representation of a query package name. +-- Includes namespace, name and version, but not arch or constraints. +-- @param query table: a query table +-- @return string: a result such as `my_user/my_rock 1.0` or `my_rock`. +function query_mt:__tostring() + local out = {} + if self.namespace then + table.insert(out, self.namespace) + table.insert(out, "/") + end + table.insert(out, self.name) + + if #self.constraints > 0 then + local pretty = {} + for _, c in ipairs(self.constraints) do + local v = c.version.string + if c.op == "==" then + table.insert(pretty, v) + else + table.insert(pretty, c.op .. " " .. v) + end + end + table.insert(out, " ") + table.insert(out, table.concat(pretty, ", ")) + end + + return table.concat(out) +end + +return queries diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/remove.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/remove.lua new file mode 100644 index 000000000..a24b54ba8 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/remove.lua @@ -0,0 +1,135 @@ +local remove = {} + +local search = require("luarocks.search") +local deps = require("luarocks.deps") +local fetch = require("luarocks.fetch") +local repos = require("luarocks.repos") +local path = require("luarocks.path") +local util = require("luarocks.util") +local cfg = require("luarocks.core.cfg") +local manif = require("luarocks.manif") +local queries = require("luarocks.queries") + +--- Obtain a list of packages that depend on the given set of packages +-- (where all packages of the set are versions of one program). +-- @param name string: the name of a program +-- @param versions array of string: the versions to be deleted. +-- @return array of string: an empty table if no packages depend on any +-- of the given list, or an array of strings in "name/version" format. +local function check_dependents(name, versions, deps_mode) + local dependents = {} + + local skip_set = {} + skip_set[name] = {} + for version, _ in pairs(versions) do + skip_set[name][version] = true + end + + local local_rocks = {} + local query_all = queries.all() + search.local_manifest_search(local_rocks, cfg.rocks_dir, query_all) + local_rocks[name] = nil + for rock_name, rock_versions in pairs(local_rocks) do + for rock_version, _ in pairs(rock_versions) do + local rockspec, err = fetch.load_rockspec(path.rockspec_file(rock_name, rock_version)) + if rockspec then + local _, missing = deps.match_deps(rockspec.dependencies, rockspec.rocks_provided, skip_set, deps_mode) + if missing[name] then + table.insert(dependents, { name = rock_name, version = rock_version }) + end + end + end + end + + return dependents +end + +--- Delete given versions of a program. +-- @param name string: the name of a program +-- @param versions array of string: the versions to be deleted. +-- @param deps_mode: string: Which trees to check dependencies for: +-- "one" for the current default tree, "all" for all trees, +-- "order" for all trees with priority >= the current default, "none" for no trees. +-- @return boolean or (nil, string): true on success or nil and an error message. +local function delete_versions(name, versions, deps_mode) + + for version, _ in pairs(versions) do + util.printout("Removing "..name.." "..version.."...") + local ok, err = repos.delete_version(name, version, deps_mode) + if not ok then return nil, err end + end + + return true +end + +function remove.remove_search_results(results, name, deps_mode, force, fast) + local versions = results[name] + + local version = next(versions) + local second = next(versions, version) + + local dependents = {} + if not fast then + util.printout("Checking stability of dependencies in the absence of") + util.printout(name.." "..table.concat(util.keys(versions), ", ").."...") + util.printout() + dependents = check_dependents(name, versions, deps_mode) + end + + if #dependents > 0 then + if force or fast then + util.printerr("The following packages may be broken by this forced removal:") + for _, dependent in ipairs(dependents) do + util.printerr(dependent.name.." "..dependent.version) + end + util.printerr() + else + if not second then + util.printerr("Will not remove "..name.." "..version..".") + util.printerr("Removing it would break dependencies for: ") + else + util.printerr("Will not remove installed versions of "..name..".") + util.printerr("Removing them would break dependencies for: ") + end + for _, dependent in ipairs(dependents) do + util.printerr(dependent.name.." "..dependent.version) + end + util.printerr() + util.printerr("Use --force to force removal (warning: this may break modules).") + return nil, "Failed removing." + end + end + + local ok, err = delete_versions(name, versions, deps_mode) + if not ok then return nil, err end + + util.printout("Removal successful.") + return true +end + +function remove.remove_other_versions(name, version, force, fast) + local results = {} + local query = queries.new(name, nil, version, false, nil, "~=") + search.local_manifest_search(results, cfg.rocks_dir, query) + local warn + if results[name] then + local ok, err = remove.remove_search_results(results, name, cfg.deps_mode, force, fast) + if not ok then -- downgrade failure to a warning + warn = err + end + end + + if not fast then + -- since we're not using --keep, this means that all files of the rock being installed + -- should be available as non-versioned variants. Double-check that: + local rock_manifest, load_err = manif.load_rock_manifest(name, version) + local ok, err = repos.check_everything_is_installed(name, version, rock_manifest, cfg.root_dir, false) + if not ok then + return nil, err + end + end + + return true, nil, warn +end + +return remove diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/repos.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/repos.lua new file mode 100644 index 000000000..764fe3ad6 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/repos.lua @@ -0,0 +1,697 @@ + +--- Functions for managing the repository on disk. +local repos = {} + +local fs = require("luarocks.fs") +local path = require("luarocks.path") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local dir = require("luarocks.dir") +local manif = require("luarocks.manif") +local vers = require("luarocks.core.vers") + +local unpack = unpack or table.unpack -- luacheck: ignore 211 + +--- Get type and name of an item (a module or a command) provided by a file. +-- @param deploy_type string: rock manifest subtree the file comes from ("bin", "lua", or "lib"). +-- @param file_path string: path to the file relatively to deploy_type subdirectory. +-- @return (string, string): item type ("module" or "command") and name. +local function get_provided_item(deploy_type, file_path) + assert(type(deploy_type) == "string") + assert(type(file_path) == "string") + local item_type = deploy_type == "bin" and "command" or "module" + local item_name = item_type == "command" and file_path or path.path_to_module(file_path) + return item_type, item_name +end + +-- Tree of files installed by a package are stored +-- in its rock manifest. Some of these files have to +-- be deployed to locations where Lua can load them as +-- modules or where they can be used as commands. +-- These files are characterised by pair +-- (deploy_type, file_path), where deploy_type is the first +-- component of the file path and file_path is the rest of the +-- path. Only files with deploy_type in {"lua", "lib", "bin"} +-- are deployed somewhere. +-- Each deployed file provides an "item". An item is +-- characterised by pair (item_type, item_name). +-- item_type is "command" for files with deploy_type +-- "bin" and "module" for deploy_type in {"lua", "lib"}. +-- item_name is same as file_path for commands +-- and is produced using path.path_to_module(file_path) +-- for modules. + +--- Get all installed versions of a package. +-- @param name string: a package name. +-- @return table or nil: An array of strings listing installed +-- versions of a package, or nil if none is available. +local function get_installed_versions(name) + assert(type(name) == "string" and not name:match("/")) + + local dirs = fs.list_dir(path.versions_dir(name)) + return (dirs and #dirs > 0) and dirs or nil +end + +--- Check if a package exists in a local repository. +-- Version numbers are compared as exact string comparison. +-- @param name string: name of package +-- @param version string: package version in string format +-- @return boolean: true if a package is installed, +-- false otherwise. +function repos.is_installed(name, version) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + return fs.is_dir(path.install_dir(name, version)) +end + +function repos.recurse_rock_manifest_entry(entry, action) + assert(type(action) == "function") + + if entry == nil then + return true + end + + local function do_recurse_rock_manifest_entry(tree, parent_path) + + for file, sub in pairs(tree) do + local sub_path = (parent_path and (parent_path .. "/") or "") .. file + local ok, err -- luacheck: ignore 231 + + if type(sub) == "table" then + ok, err = do_recurse_rock_manifest_entry(sub, sub_path) + else + ok, err = action(sub_path) + end + + if err then return nil, err end + end + return true + end + return do_recurse_rock_manifest_entry(entry) +end + +local function store_package_data(result, rock_manifest, deploy_type) + if rock_manifest[deploy_type] then + repos.recurse_rock_manifest_entry(rock_manifest[deploy_type], function(file_path) + local _, item_name = get_provided_item(deploy_type, file_path) + result[item_name] = file_path + return true + end) + end +end + +--- Obtain a table of modules within an installed package. +-- @param name string: The package name; for example "luasocket" +-- @param version string: The exact version number including revision; +-- for example "2.0.1-1". +-- @return table: A table of modules where keys are module names +-- and values are file paths of files providing modules +-- relative to "lib" or "lua" rock manifest subtree. +-- If no modules are found or if package name or version +-- are invalid, an empty table is returned. +function repos.package_modules(name, version) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local result = {} + local rock_manifest = manif.load_rock_manifest(name, version) + if not rock_manifest then return result end + store_package_data(result, rock_manifest, "lib") + store_package_data(result, rock_manifest, "lua") + return result +end + +--- Obtain a table of command-line scripts within an installed package. +-- @param name string: The package name; for example "luasocket" +-- @param version string: The exact version number including revision; +-- for example "2.0.1-1". +-- @return table: A table of commands where keys and values are command names +-- as strings - file paths of files providing commands +-- relative to "bin" rock manifest subtree. +-- If no commands are found or if package name or version +-- are invalid, an empty table is returned. +function repos.package_commands(name, version) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local result = {} + local rock_manifest = manif.load_rock_manifest(name, version) + if not rock_manifest then return result end + store_package_data(result, rock_manifest, "bin") + return result +end + + +--- Check if a rock contains binary executables. +-- @param name string: name of an installed rock +-- @param version string: version of an installed rock +-- @return boolean: returns true if rock contains platform-specific +-- binary executables, or false if it is a pure-Lua rock. +function repos.has_binaries(name, version) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + + local rock_manifest = manif.load_rock_manifest(name, version) + if rock_manifest and rock_manifest.bin then + for bin_name, md5 in pairs(rock_manifest.bin) do + -- TODO verify that it is the same file. If it isn't, find the actual command. + if fs.is_actual_binary(dir.path(cfg.deploy_bin_dir, bin_name)) then + return true + end + end + end + return false +end + +function repos.run_hook(rockspec, hook_name) + assert(rockspec:type() == "rockspec") + assert(type(hook_name) == "string") + + local hooks = rockspec.hooks + if not hooks then + return true + end + + if cfg.hooks_enabled == false then + return nil, "This rockspec contains hooks, which are blocked by the 'hooks_enabled' setting in your LuaRocks configuration." + end + + if not hooks.substituted_variables then + util.variable_substitutions(hooks, rockspec.variables) + hooks.substituted_variables = true + end + local hook = hooks[hook_name] + if hook then + util.printout(hook) + if not fs.execute(hook) then + return nil, "Failed running "..hook_name.." hook." + end + end + return true +end + +function repos.should_wrap_bin_scripts(rockspec) + assert(rockspec:type() == "rockspec") + + if cfg.wrap_bin_scripts ~= nil then + return cfg.wrap_bin_scripts + end + if rockspec.deploy and rockspec.deploy.wrap_bin_scripts == false then + return false + end + return true +end + +local function find_suffixed(file, suffix) + local filenames = {file} + if suffix and suffix ~= "" then + table.insert(filenames, 1, file .. suffix) + end + + for _, filename in ipairs(filenames) do + if fs.exists(filename) then + return filename + end + end + + return nil, table.concat(filenames, ", ") .. " not found" +end + +local function check_suffix(filename, suffix) + local suffixed_filename, err = find_suffixed(filename, suffix) + if not suffixed_filename then + return "" + end + return suffixed_filename:sub(#filename + 1) +end + +-- Files can be deployed using versioned and non-versioned names. +-- Several items with same type and name can exist if they are +-- provided by different packages or versions. In any case +-- item from the newest version of lexicographically smallest package +-- is deployed using non-versioned name and others use versioned names. + +local function get_deploy_paths(name, version, deploy_type, file_path, repo) + assert(type(name) == "string") + assert(type(version) == "string") + assert(type(deploy_type) == "string") + assert(type(file_path) == "string") + + repo = repo or cfg.root_dir + local deploy_dir = path["deploy_" .. deploy_type .. "_dir"](repo) + local non_versioned = dir.path(deploy_dir, file_path) + local versioned = path.versioned_name(non_versioned, deploy_dir, name, version) + return { nv = non_versioned, v = versioned } +end + +local function check_spot_if_available(name, version, deploy_type, file_path) + local item_type, item_name = get_provided_item(deploy_type, file_path) + local cur_name, cur_version = manif.get_current_provider(item_type, item_name) + + -- older versions of LuaRocks (< 3) registered "foo.init" files as "foo" + -- (which caused problems, so that behavior was changed). But look for that + -- in the manifest anyway for backward compatibility. + if not cur_name and deploy_type == "lua" and item_name:match("%.init$") then + cur_name, cur_version = manif.get_current_provider(item_type, (item_name:gsub("%.init$", ""))) + end + + if (not cur_name) + or (name < cur_name) + or (name == cur_name and (version == cur_version + or vers.compare_versions(version, cur_version))) then + return "nv", cur_name, cur_version, item_name + else + -- Existing version has priority, deploy new version using versioned name. + return "v", cur_name, cur_version, item_name + end +end + +local function backup_existing(should_backup, target) + if not should_backup then + fs.delete(target) + return + end + if fs.exists(target) then + local backup = target + repeat + backup = backup.."~" + until not fs.exists(backup) -- Slight race condition here, but shouldn't be a problem. + + util.warning(target.." is not tracked by this installation of LuaRocks. Moving it to "..backup) + local move_ok, move_err = os.rename(target, backup) + if not move_ok then + return nil, move_err + end + return backup + end +end + +local function prepare_op_install() + local mkdirs = {} + local rmdirs = {} + + local function memoize_mkdir(d) + if mkdirs[d] then + return true + end + local ok, err = fs.make_dir(d) + if not ok then + return nil, err + end + mkdirs[d] = true + return true + end + + local function op_install(op) + local ok, err = memoize_mkdir(dir.dir_name(op.dst)) + if not ok then + return nil, err + end + + local backup, err = backup_existing(op.backup, op.dst) + if err then + return nil, err + end + if backup then + op.backup_file = backup + end + + ok, err = op.fn(op.src, op.dst, op.backup) + if not ok then + return nil, err + end + + rmdirs[dir.dir_name(op.src)] = true + return true + end + + local function done_op_install() + for d, _ in pairs(rmdirs) do + fs.remove_dir_tree_if_empty(d) + end + end + + return op_install, done_op_install +end + +local function rollback_install(op) + fs.delete(op.dst) + if op.backup_file then + os.rename(op.backup_file, op.dst) + end + fs.remove_dir_tree_if_empty(dir.dir_name(op.dst)) + return true +end + +local function op_rename(op) + if op.suffix then + local suffix = check_suffix(op.src, op.suffix) + op.src = op.src .. suffix + op.dst = op.dst .. suffix + end + + if fs.exists(op.src) then + fs.make_dir(dir.dir_name(op.dst)) + fs.delete(op.dst) + local ok, err = os.rename(op.src, op.dst) + fs.remove_dir_tree_if_empty(dir.dir_name(op.src)) + return ok, err + else + return true + end +end + +local function rollback_rename(op) + return op_rename({ src = op.dst, dst = op.src }) +end + +local function prepare_op_delete() + local deletes = {} + local rmdirs = {} + + local function done_op_delete() + for _, f in ipairs(deletes) do + os.remove(f) + end + + for d, _ in pairs(rmdirs) do + fs.remove_dir_tree_if_empty(d) + end + end + + local function op_delete(op) + if op.suffix then + local suffix = check_suffix(op.name, op.suffix) + op.name = op.name .. suffix + end + + table.insert(deletes, op.name) + + rmdirs[dir.dir_name(op.name)] = true + end + + return op_delete, done_op_delete +end + +local function rollback_ops(ops, op_fn, n) + for i = 1, n do + op_fn(ops[i]) + end +end + +--- Double check that all files referenced in `rock_manifest` are installed in `repo`. +function repos.check_everything_is_installed(name, version, rock_manifest, repo, accept_versioned) + local missing = {} + local suffix = cfg.wrapper_suffix or "" + for _, category in ipairs({"bin", "lua", "lib"}) do + if rock_manifest[category] then + repos.recurse_rock_manifest_entry(rock_manifest[category], function(file_path) + local paths = get_deploy_paths(name, version, category, file_path, repo) + if category == "bin" then + if (fs.exists(paths.nv) or fs.exists(paths.nv .. suffix)) + or (accept_versioned and (fs.exists(paths.v) or fs.exists(paths.v .. suffix))) then + return + end + else + if fs.exists(paths.nv) or (accept_versioned and fs.exists(paths.v)) then + return + end + end + table.insert(missing, paths.nv) + end) + end + end + if #missing > 0 then + return nil, "failed deploying files. " .. + "The following files were not installed:\n" .. + table.concat(missing, "\n") + end + return true +end + +--- Deploy a package from the rocks subdirectory. +-- @param name string: name of package +-- @param version string: exact package version in string format +-- @param wrap_bin_scripts bool: whether commands written in Lua should be wrapped. +-- @param deps_mode: string: Which trees to check dependencies for: +-- "one" for the current default tree, "all" for all trees, +-- "order" for all trees with priority >= the current default, "none" for no trees. +function repos.deploy_files(name, version, wrap_bin_scripts, deps_mode) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(wrap_bin_scripts) == "boolean") + + local rock_manifest, load_err = manif.load_rock_manifest(name, version) + if not rock_manifest then return nil, load_err end + + local repo = cfg.root_dir + local renames = {} + local installs = {} + + local function install_binary(source, target) + if wrap_bin_scripts and fs.is_lua(source) then + return fs.wrap_script(source, target, deps_mode, name, version) + else + return fs.copy_binary(source, target) + end + end + + local function move_lua(source, target) + return fs.move(source, target, "read") + end + + local function move_lib(source, target) + return fs.move(source, target, "exec") + end + + if rock_manifest.bin then + local source_dir = path.bin_dir(name, version) + repos.recurse_rock_manifest_entry(rock_manifest.bin, function(file_path) + local source = dir.path(source_dir, file_path) + local paths = get_deploy_paths(name, version, "bin", file_path, repo) + local mode, cur_name, cur_version = check_spot_if_available(name, version, "bin", file_path) + + if mode == "nv" and cur_name then + local cur_paths = get_deploy_paths(cur_name, cur_version, "bin", file_path, repo) + table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v, suffix = cfg.wrapper_suffix }) + end + local target = mode == "nv" and paths.nv or paths.v + local backup = name ~= cur_name or version ~= cur_version + if wrap_bin_scripts and fs.is_lua(source) then + target = target .. (cfg.wrapper_suffix or "") + end + table.insert(installs, { fn = install_binary, src = source, dst = target, backup = backup }) + end) + end + + if rock_manifest.lua then + local source_dir = path.lua_dir(name, version) + repos.recurse_rock_manifest_entry(rock_manifest.lua, function(file_path) + local source = dir.path(source_dir, file_path) + local paths = get_deploy_paths(name, version, "lua", file_path, repo) + local mode, cur_name, cur_version = check_spot_if_available(name, version, "lua", file_path) + + if mode == "nv" and cur_name then + local cur_paths = get_deploy_paths(cur_name, cur_version, "lua", file_path, repo) + table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v }) + cur_paths = get_deploy_paths(cur_name, cur_version, "lib", file_path:gsub("%.lua$", "." .. cfg.lib_extension), repo) + table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v }) + end + local target = mode == "nv" and paths.nv or paths.v + local backup = name ~= cur_name or version ~= cur_version + table.insert(installs, { fn = move_lua, src = source, dst = target, backup = backup }) + end) + end + + if rock_manifest.lib then + local source_dir = path.lib_dir(name, version) + repos.recurse_rock_manifest_entry(rock_manifest.lib, function(file_path) + local source = dir.path(source_dir, file_path) + local paths = get_deploy_paths(name, version, "lib", file_path, repo) + local mode, cur_name, cur_version = check_spot_if_available(name, version, "lib", file_path) + + if mode == "nv" and cur_name then + local cur_paths = get_deploy_paths(cur_name, cur_version, "lua", file_path:gsub("%.[^.]+$", ".lua"), repo) + table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v }) + cur_paths = get_deploy_paths(cur_name, cur_version, "lib", file_path, repo) + table.insert(renames, { src = cur_paths.nv, dst = cur_paths.v }) + end + local target = mode == "nv" and paths.nv or paths.v + local backup = name ~= cur_name or version ~= cur_version + table.insert(installs, { fn = move_lib, src = source, dst = target, backup = backup }) + end) + end + + for i, op in ipairs(renames) do + local ok, err = op_rename(op) + if not ok then + rollback_ops(renames, rollback_rename, i - 1) + return nil, err + end + end + local op_install, done_op_install = prepare_op_install() + for i, op in ipairs(installs) do + local ok, err = op_install(op) + if not ok then + rollback_ops(installs, rollback_install, i - 1) + rollback_ops(renames, rollback_rename, #renames) + return nil, err + end + end + done_op_install() + + local ok, err = repos.check_everything_is_installed(name, version, rock_manifest, repo, true) + if not ok then + return nil, err + end + + local writer = require("luarocks.manif.writer") + return writer.add_to_manifest(name, version, nil, deps_mode) +end + +local function add_to_double_checks(double_checks, name, version) + double_checks[name] = double_checks[name] or {} + double_checks[name][version] = true +end + +local function double_check_all(double_checks, repo) + local errs = {} + for next_name, versions in pairs(double_checks) do + for next_version in pairs(versions) do + local rock_manifest, load_err = manif.load_rock_manifest(next_name, next_version) + local ok, err = repos.check_everything_is_installed(next_name, next_version, rock_manifest, repo, true) + if not ok then + table.insert(errs, err) + end + end + end + if next(errs) then + return nil, table.concat(errs, "\n") + end + return true +end + +--- Delete a package from the local repository. +-- @param name string: name of package +-- @param version string: exact package version in string format +-- @param deps_mode: string: Which trees to check dependencies for: +-- "one" for the current default tree, "all" for all trees, +-- "order" for all trees with priority >= the current default, "none" for no trees. +-- @param quick boolean: do not try to fix the versioned name +-- of another version that provides the same module that +-- was deleted. This is used during 'purge', as every module +-- will be eventually deleted. +function repos.delete_version(name, version, deps_mode, quick) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(deps_mode) == "string") + + local rock_manifest, load_err = manif.load_rock_manifest(name, version) + if not rock_manifest then + if not quick then + local writer = require("luarocks.manif.writer") + writer.remove_from_manifest(name, version, nil, deps_mode) + return nil, "rock_manifest file not found for "..name.." "..version.." - removed entry from the manifest" + end + return nil, load_err + end + + local repo = cfg.root_dir + local renames = {} + local deletes = {} + + local double_checks = {} + + if rock_manifest.bin then + repos.recurse_rock_manifest_entry(rock_manifest.bin, function(file_path) + local paths = get_deploy_paths(name, version, "bin", file_path, repo) + local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "bin", file_path) + if mode == "v" then + table.insert(deletes, { name = paths.v, suffix = cfg.wrapper_suffix }) + else + table.insert(deletes, { name = paths.nv, suffix = cfg.wrapper_suffix }) + + local next_name, next_version = manif.get_next_provider("command", item_name) + if next_name then + add_to_double_checks(double_checks, next_name, next_version) + local next_paths = get_deploy_paths(next_name, next_version, "bin", file_path, repo) + table.insert(renames, { src = next_paths.v, dst = next_paths.nv, suffix = cfg.wrapper_suffix }) + end + end + end) + end + + if rock_manifest.lua then + repos.recurse_rock_manifest_entry(rock_manifest.lua, function(file_path) + local paths = get_deploy_paths(name, version, "lua", file_path, repo) + local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "lua", file_path) + if mode == "v" then + table.insert(deletes, { name = paths.v }) + else + table.insert(deletes, { name = paths.nv }) + + local next_name, next_version = manif.get_next_provider("module", item_name) + if next_name then + add_to_double_checks(double_checks, next_name, next_version) + local next_lua_paths = get_deploy_paths(next_name, next_version, "lua", file_path, repo) + table.insert(renames, { src = next_lua_paths.v, dst = next_lua_paths.nv }) + local next_lib_paths = get_deploy_paths(next_name, next_version, "lib", file_path:gsub("%.[^.]+$", ".lua"), repo) + table.insert(renames, { src = next_lib_paths.v, dst = next_lib_paths.nv }) + end + end + end) + end + + if rock_manifest.lib then + repos.recurse_rock_manifest_entry(rock_manifest.lib, function(file_path) + local paths = get_deploy_paths(name, version, "lib", file_path, repo) + local mode, cur_name, cur_version, item_name = check_spot_if_available(name, version, "lib", file_path) + if mode == "v" then + table.insert(deletes, { name = paths.v }) + else + table.insert(deletes, { name = paths.nv }) + + local next_name, next_version = manif.get_next_provider("module", item_name) + if next_name then + add_to_double_checks(double_checks, next_name, next_version) + local next_lua_paths = get_deploy_paths(next_name, next_version, "lua", file_path:gsub("%.[^.]+$", ".lua"), repo) + table.insert(renames, { src = next_lua_paths.v, dst = next_lua_paths.nv }) + local next_lib_paths = get_deploy_paths(next_name, next_version, "lib", file_path, repo) + table.insert(renames, { src = next_lib_paths.v, dst = next_lib_paths.nv }) + end + end + end) + end + + local op_delete, done_op_delete = prepare_op_delete() + for _, op in ipairs(deletes) do + op_delete(op) + end + done_op_delete() + + if not quick then + for _, op in ipairs(renames) do + op_rename(op) + end + + local ok, err = double_check_all(double_checks, repo) + if not ok then + return nil, err + end + end + + fs.delete(path.install_dir(name, version)) + if not get_installed_versions(name) then + fs.delete(dir.path(cfg.rocks_dir, name)) + end + + if quick then + return true + end + + local writer = require("luarocks.manif.writer") + return writer.remove_from_manifest(name, version, nil, deps_mode) +end + +return repos diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/require.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/require.lua new file mode 100644 index 000000000..902bd1a3c --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/require.lua @@ -0,0 +1,2 @@ +--- Retained for compatibility reasons only. Use luarocks.loader instead. +return require("luarocks.loader") diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/results.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/results.lua new file mode 100644 index 000000000..c14862de7 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/results.lua @@ -0,0 +1,62 @@ +local results = {} + +local vers = require("luarocks.core.vers") +local util = require("luarocks.util") + +local result_mt = {} + +result_mt.__index = result_mt + +function result_mt.type() + return "result" +end + +function results.new(name, version, repo, arch, namespace) + assert(type(name) == "string" and not name:match("/")) + assert(type(version) == "string") + assert(type(repo) == "string") + assert(type(arch) == "string" or not arch) + assert(type(namespace) == "string" or not namespace) + + if not namespace then + name, namespace = util.split_namespace(name) + end + + local self = { + name = name, + version = version, + namespace = namespace, + arch = arch, + repo = repo, + } + + return setmetatable(self, result_mt) +end + +--- Test the name field of a query. +-- If query has a boolean field substring set to true, +-- then substring match is performed; otherwise, exact string +-- comparison is done. +-- @param query table: A query in dependency table format. +-- @param name string: A package name. +-- @return boolean: True if names match, false otherwise. +local function match_name(query, name) + if query.substring then + return name:find(query.name, 0, true) and true or false + else + return name == query.name + end +end + +--- Returns true if the result satisfies a given query. +-- @param query: a query. +-- @return boolean. +function result_mt:satisfies(query) + assert(query:type() == "query") + return match_name(query, self.name) + and (query.arch[self.arch] or query.arch["any"]) + and ((not query.namespace) or (query.namespace == self.namespace)) + and vers.match_constraints(vers.parse_version(self.version), query.constraints) +end + +return results diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/rockspecs.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/rockspecs.lua new file mode 100644 index 000000000..94462951a --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/rockspecs.lua @@ -0,0 +1,161 @@ +local rockspecs = {} + +local cfg = require("luarocks.core.cfg") +local dir = require("luarocks.dir") +local path = require("luarocks.path") +local queries = require("luarocks.queries") +local type_rockspec = require("luarocks.type.rockspec") +local util = require("luarocks.util") +local vers = require("luarocks.core.vers") + +local rockspec_mt = {} + +rockspec_mt.__index = rockspec_mt + +function rockspec_mt.type() + return "rockspec" +end + +--- Perform platform-specific overrides on a table. +-- Overrides values of table with the contents of the appropriate +-- subset of its "platforms" field. The "platforms" field should +-- be a table containing subtables keyed with strings representing +-- platform names. Names that match the contents of the global +-- detected platforms setting are used. For example, if +-- platform "unix" is detected, then the fields of +-- tbl.platforms.unix will overwrite those of tbl with the same +-- names. For table values, the operation is performed recursively +-- (tbl.platforms.foo.x.y.z overrides tbl.x.y.z; other contents of +-- tbl.x are preserved). +-- @param tbl table or nil: Table which may contain a "platforms" field; +-- if it doesn't (or if nil is passed), this function does nothing. +local function platform_overrides(tbl) + assert(type(tbl) == "table" or not tbl) + + if not tbl then return end + + if tbl.platforms then + for platform in cfg.each_platform() do + local platform_tbl = tbl.platforms[platform] + if platform_tbl then + util.deep_merge(tbl, platform_tbl) + end + end + end + tbl.platforms = nil +end + +local function convert_dependencies(rockspec, key) + if rockspec[key] then + for i = 1, #rockspec[key] do + local parsed, err = queries.from_dep_string(rockspec[key][i]) + if not parsed then + return nil, "Parse error processing dependency '"..rockspec[key][i].."': "..tostring(err) + end + rockspec[key][i] = parsed + end + else + rockspec[key] = {} + end + return true +end + +--- Set up path-related variables for a given rock. +-- Create a "variables" table in the rockspec table, containing +-- adjusted variables according to the configuration file. +-- @param rockspec table: The rockspec table. +local function configure_paths(rockspec) + local vars = {} + for k,v in pairs(cfg.variables) do + vars[k] = v + end + local name, version = rockspec.name, rockspec.version + vars.PREFIX = path.install_dir(name, version) + vars.LUADIR = path.lua_dir(name, version) + vars.LIBDIR = path.lib_dir(name, version) + vars.CONFDIR = path.conf_dir(name, version) + vars.BINDIR = path.bin_dir(name, version) + vars.DOCDIR = path.doc_dir(name, version) + rockspec.variables = vars +end + +function rockspecs.from_persisted_table(filename, rockspec, globals, quick) + assert(type(rockspec) == "table") + assert(type(globals) == "table" or globals == nil) + assert(type(filename) == "string") + assert(type(quick) == "boolean" or quick == nil) + + if rockspec.rockspec_format then + if vers.compare_versions(rockspec.rockspec_format, type_rockspec.rockspec_format) then + return nil, "Rockspec format "..rockspec.rockspec_format.." is not supported, please upgrade LuaRocks." + end + end + + if not quick then + local ok, err = type_rockspec.check(rockspec, globals or {}) + if not ok then + return nil, err + end + end + + --- Check if rockspec format version satisfies version requirement. + -- @param rockspec table: The rockspec table. + -- @param version string: required version. + -- @return boolean: true if rockspec format matches version or is newer, false otherwise. + do + local parsed_format = vers.parse_version(rockspec.rockspec_format or "1.0") + rockspec.format_is_at_least = function(self, version) + return parsed_format >= vers.parse_version(version) + end + end + + platform_overrides(rockspec.build) + platform_overrides(rockspec.dependencies) + platform_overrides(rockspec.build_dependencies) + platform_overrides(rockspec.test_dependencies) + platform_overrides(rockspec.external_dependencies) + platform_overrides(rockspec.source) + platform_overrides(rockspec.hooks) + platform_overrides(rockspec.test) + + rockspec.name = rockspec.package:lower() + + local protocol, pathname = dir.split_url(rockspec.source.url) + if dir.is_basic_protocol(protocol) then + rockspec.source.file = rockspec.source.file or dir.base_name(rockspec.source.url) + end + rockspec.source.protocol, rockspec.source.pathname = protocol, pathname + + -- Temporary compatibility + if rockspec.source.cvs_module then rockspec.source.module = rockspec.source.cvs_module end + if rockspec.source.cvs_tag then rockspec.source.tag = rockspec.source.cvs_tag end + + rockspec.local_abs_filename = filename + local filebase = rockspec.source.file or rockspec.source.url + local base = dir.deduce_base_dir(filebase) + rockspec.source.dir_set = rockspec.source.dir ~= nil + rockspec.source.dir = rockspec.source.dir + or rockspec.source.module + or ( (filebase:match("%.lua$") or filebase:match("%.c$")) + and (rockspec:format_is_at_least("3.0") + and (dir.is_basic_protocol(protocol) and "." or base) + or ".") ) + or base + + rockspec.rocks_provided = util.get_rocks_provided(rockspec) + + for _, key in ipairs({"dependencies", "build_dependencies", "test_dependencies"}) do + local ok, err = convert_dependencies(rockspec, key) + if not ok then + return nil, err + end + end + + if not quick then + configure_paths(rockspec) + end + + return setmetatable(rockspec, rockspec_mt) +end + +return rockspecs diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/search.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/search.lua new file mode 100644 index 000000000..b4c712d92 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/search.lua @@ -0,0 +1,403 @@ +local search = {} + +local dir = require("luarocks.dir") +local path = require("luarocks.path") +local manif = require("luarocks.manif") +local vers = require("luarocks.core.vers") +local cfg = require("luarocks.core.cfg") +local util = require("luarocks.util") +local queries = require("luarocks.queries") +local results = require("luarocks.results") + +--- Store a search result (a rock or rockspec) in the result tree. +-- @param result_tree table: The result tree, where keys are package names and +-- values are tables matching version strings to arrays of +-- tables with fields "arch" and "repo". +-- @param result table: A result. +function search.store_result(result_tree, result) + assert(type(result_tree) == "table") + assert(result:type() == "result") + + local name = result.name + local version = result.version + + if not result_tree[name] then result_tree[name] = {} end + if not result_tree[name][version] then result_tree[name][version] = {} end + table.insert(result_tree[name][version], { + arch = result.arch, + repo = result.repo, + namespace = result.namespace, + }) +end + +--- Store a match in a result tree if version matches query. +-- Name, version, arch and repository path are stored in a given +-- table, optionally checking if version and arch (if given) match +-- a query. +-- @param result_tree table: The result tree, where keys are package names and +-- values are tables matching version strings to arrays of +-- tables with fields "arch" and "repo". +-- @param result table: a result object. +-- @param query table: a query object. +local function store_if_match(result_tree, result, query) + assert(result:type() == "result") + assert(query:type() == "query") + + if result:satisfies(query) then + search.store_result(result_tree, result) + end +end + +--- Perform search on a local repository. +-- @param repo string: The pathname of the local repository. +-- @param query table: a query object. +-- @param result_tree table or nil: If given, this table will store the +-- result tree; if not given, a new table will be created. +-- @return table: The result tree, where keys are package names and +-- values are tables matching version strings to arrays of +-- tables with fields "arch" and "repo". +-- If a table was given in the "result_tree" parameter, that is the result value. +function search.disk_search(repo, query, result_tree) + assert(type(repo) == "string") + assert(query:type() == "query") + assert(type(result_tree) == "table" or not result_tree) + + local fs = require("luarocks.fs") + + if not result_tree then + result_tree = {} + end + + for name in fs.dir(repo) do + local pathname = dir.path(repo, name) + local rname, rversion, rarch = path.parse_name(name) + + if rname and (pathname:match(".rockspec$") or pathname:match(".rock$")) then + local result = results.new(rname, rversion, repo, rarch) + store_if_match(result_tree, result, query) + elseif fs.is_dir(pathname) then + for version in fs.dir(pathname) do + if version:match("-%d+$") then + local namespace = path.read_namespace(name, version, repo) + local result = results.new(name, version, repo, "installed", namespace) + store_if_match(result_tree, result, query) + end + end + end + end + return result_tree +end + +--- Perform search on a rocks server or tree. +-- @param result_tree table: The result tree, where keys are package names and +-- values are tables matching version strings to arrays of +-- tables with fields "arch" and "repo". +-- @param repo string: The URL of a rocks server or +-- the pathname of a rocks tree (as returned by path.rocks_dir()). +-- @param query table: a query object. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @param is_local boolean +-- @return true or, in case of errors, nil, an error message and an optional error code. +local function manifest_search(result_tree, repo, query, lua_version, is_local) + assert(type(result_tree) == "table") + assert(type(repo) == "string") + assert(query:type() == "query") + + -- FIXME do not add this in local repos + if (not is_local) and query.namespace then + repo = repo .. "/manifests/" .. query.namespace + end + + local manifest, err, errcode = manif.load_manifest(repo, lua_version, false) + if not manifest then + return nil, err, errcode + end + for name, versions in pairs(manifest.repository) do + for version, items in pairs(versions) do + local namespace = is_local and path.read_namespace(name, version, repo) or query.namespace + for _, item in ipairs(items) do + local result = results.new(name, version, repo, item.arch, namespace) + store_if_match(result_tree, result, query) + end + end + end + return true +end + +local function remote_manifest_search(result_tree, repo, query, lua_version) + return manifest_search(result_tree, repo, query, lua_version, false) +end + +function search.local_manifest_search(result_tree, repo, query, lua_version) + return manifest_search(result_tree, repo, query, lua_version, true) +end + +--- Search on all configured rocks servers. +-- @param query table: a query object. +-- @param lua_version string: Lua version in "5.x" format, defaults to installed version. +-- @return table: A table where keys are package names +-- and values are tables matching version strings to arrays of +-- tables with fields "arch" and "repo". +function search.search_repos(query, lua_version) + assert(query:type() == "query") + local query_version = nil + for _, constraint in pairs(query.constraints) do + if constraint.op == '==' then + query_version = constraint.version.string + break + end + end + + local result_tree = {} + for _, repo in ipairs(cfg.rocks_servers) do + if type(repo) == "string" then + repo = { repo } + end + for _, mirror in ipairs(repo) do + if not cfg.disabled_servers[mirror] then + local protocol, pathname = dir.split_url(mirror) + if protocol == "file" then + mirror = pathname + end + local ok, err, errcode = remote_manifest_search(result_tree, mirror, query, lua_version) + if errcode == "network" then + cfg.disabled_servers[mirror] = true + end + if ok then + break + else + util.warning("Failed searching manifest: "..err) + end + end + end + + -- stop searching repos if exact match was found + local result = result_tree[query.name] + if result and result[query_version] then + break + end + end + -- search through rocks in rocks_provided + local provided_repo = "provided by VM or rocks_provided" + for name, version in pairs(util.get_rocks_provided()) do + local result = results.new(name, version, provided_repo, "installed") + store_if_match(result_tree, result, query) + end + return result_tree +end + +--- Get the URL for the latest in a set of versions. +-- @param name string: The package name to be used in the URL. +-- @param versions table: An array of version informations, as stored +-- in search result trees. +-- @return string or nil: the URL for the latest version if one could +-- be picked, or nil. +local function pick_latest_version(name, versions) + assert(type(name) == "string" and not name:match("/")) + assert(type(versions) == "table") + + local vtables = {} + for v, _ in pairs(versions) do + table.insert(vtables, vers.parse_version(v)) + end + table.sort(vtables) + local version = vtables[#vtables].string + local items = versions[version] + if items then + local pick = 1 + for i, item in ipairs(items) do + if (item.arch == 'src' and items[pick].arch == 'rockspec') + or (item.arch ~= 'src' and item.arch ~= 'rockspec') then + pick = i + end + end + return path.make_url(items[pick].repo, name, version, items[pick].arch) + end + return nil +end + +-- Find out which other Lua versions provide rock versions matching a query, +-- @param query table: a query object. +-- @return table: array of Lua versions supported, in "5.x" format. +local function supported_lua_versions(query) + assert(query:type() == "query") + local result_tree = {} + + for lua_version in util.lua_versions() do + if lua_version ~= cfg.lua_version then + util.printout("Checking for Lua " .. lua_version .. "...") + if search.search_repos(query, lua_version)[query.name] then + table.insert(result_tree, lua_version) + end + end + end + + return result_tree +end + +--- Attempt to get a single URL for a given search for a rock. +-- @param query table: a query object. +-- @return string or (nil, string, string): URL for latest matching version +-- of the rock if it was found, or nil followed by an error message +-- and an error code. +function search.find_suitable_rock(query) + assert(query:type() == "query") + + local rocks_provided = util.get_rocks_provided() + + if rocks_provided[query.name] ~= nil then + -- Do not install versions listed in rocks_provided. + return nil, "Rock "..query.name.." "..rocks_provided[query.name].. + " is already provided by VM or via 'rocks_provided' in the config file.", "provided" + end + + local result_tree = search.search_repos(query) + local first_rock = next(result_tree) + if not first_rock then + return nil, "No results matching query were found for Lua " .. cfg.lua_version .. ".", "notfound" + elseif next(result_tree, first_rock) then + -- Shouldn't happen as query must match only one package. + return nil, "Several rocks matched query.", "manyfound" + else + return pick_latest_version(query.name, result_tree[first_rock]) + end +end + +function search.find_src_or_rockspec(name, namespace, version, check_lua_versions) + local query = queries.new(name, namespace, version, false, "src|rockspec") + local url, err = search.find_rock_checking_lua_versions(query, check_lua_versions) + if not url then + return nil, "Could not find a result named "..tostring(query)..": "..err + end + return url +end + +function search.find_rock_checking_lua_versions(query, check_lua_versions) + local url, err, errcode = search.find_suitable_rock(query) + if url then + return url + end + + if errcode == "notfound" then + local add + if check_lua_versions then + util.printout(query.name .. " not found for Lua " .. cfg.lua_version .. ".") + util.printout("Checking if available for other Lua versions...") + + -- Check if constraints are satisfiable with other Lua versions. + local lua_versions = supported_lua_versions(query) + + if #lua_versions ~= 0 then + -- Build a nice message in "only Lua 5.x and 5.y but not 5.z." format + for i, lua_version in ipairs(lua_versions) do + lua_versions[i] = "Lua "..lua_version + end + + local versions_message = "only "..table.concat(lua_versions, " and ").. + " but not Lua "..cfg.lua_version.."." + + if #query.constraints == 0 then + add = query.name.." supports "..versions_message + elseif #query.constraints == 1 and query.constraints[1].op == "==" then + add = query.name.." "..query.constraints[1].version.string.." supports "..versions_message + else + add = "Matching "..query.name.." versions support "..versions_message + end + else + add = query.name.." is not available for any Lua versions." + end + else + add = "To check if it is available for other Lua versions, use --check-lua-versions." + end + err = err .. "\n" .. add + end + + return nil, err +end + +--- Print a list of rocks/rockspecs on standard output. +-- @param result_tree table: A result tree. +-- @param porcelain boolean or nil: A flag to force machine-friendly output. +function search.print_result_tree(result_tree, porcelain) + assert(type(result_tree) == "table") + assert(type(porcelain) == "boolean" or not porcelain) + + if porcelain then + for package, versions in util.sortedpairs(result_tree) do + for version, repos in util.sortedpairs(versions, vers.compare_versions) do + for _, repo in ipairs(repos) do + local nrepo = dir.normalize(repo.repo) + util.printout(package, version, repo.arch, nrepo, repo.namespace) + end + end + end + return + end + + for package, versions in util.sortedpairs(result_tree) do + local namespaces = {} + for version, repos in util.sortedpairs(versions, vers.compare_versions) do + for _, repo in ipairs(repos) do + local key = repo.namespace or "" + local list = namespaces[key] or {} + namespaces[key] = list + + repo.repo = dir.normalize(repo.repo) + table.insert(list, " "..version.." ("..repo.arch..") - "..path.root_dir(repo.repo)) + end + end + for key, list in util.sortedpairs(namespaces) do + util.printout(key == "" and package or key .. "/" .. package) + for _, line in ipairs(list) do + util.printout(line) + end + util.printout() + end + end +end + +function search.pick_installed_rock(query, given_tree) + assert(query:type() == "query") + + local result_tree = {} + local tree_map = {} + local trees = cfg.rocks_trees + if given_tree then + trees = { given_tree } + end + for _, tree in ipairs(trees) do + local rocks_dir = path.rocks_dir(tree) + tree_map[rocks_dir] = tree + search.local_manifest_search(result_tree, rocks_dir, query) + end + if not next(result_tree) then + return nil, "cannot find package "..tostring(query).."\nUse 'list' to find installed rocks." + end + + if not result_tree[query.name] and next(result_tree, next(result_tree)) then + local out = { "multiple installed packages match the name '"..tostring(query).."':\n\n" } + for name, _ in util.sortedpairs(result_tree) do + table.insert(out, " " .. name .. "\n") + end + table.insert(out, "\nPlease specify a single rock.\n") + return nil, table.concat(out) + end + + local repo_url + + local name, versions + if result_tree[query.name] then + name, versions = query.name, result_tree[query.name] + else + name, versions = util.sortedpairs(result_tree)() + end + + local version, repositories = util.sortedpairs(versions, vers.compare_versions)() + for _, rp in ipairs(repositories) do repo_url = rp.repo end + + local repo = tree_map[repo_url] + return name, version, repo, repo_url +end + +return search + diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/signing.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/signing.lua new file mode 100644 index 000000000..cb91643a1 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/signing.lua @@ -0,0 +1,48 @@ +local signing = {} + +local cfg = require("luarocks.core.cfg") +local fs = require("luarocks.fs") + +local function get_gpg() + local vars = cfg.variables + local gpg = vars.GPG + local gpg_ok, err = fs.is_tool_available(gpg, "gpg") + if not gpg_ok then + return nil, err + end + return gpg +end + +function signing.signature_url(url) + return url .. ".asc" +end + +function signing.sign_file(file) + local gpg, err = get_gpg() + if not gpg then + return nil, err + end + + local sigfile = file .. ".asc" + if fs.execute(gpg, "--armor", "--output", sigfile, "--detach-sign", file) then + return sigfile + else + return nil, "failed running " .. gpg .. " to sign " .. file + end +end + +function signing.verify_signature(file, sigfile) + local gpg, err = get_gpg() + if not gpg then + return nil, err + end + + if fs.execute(gpg, "--verify", sigfile, file) then + return true + else + return nil, "GPG returned a verification error" + end + +end + +return signing diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/test.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/test.lua new file mode 100644 index 000000000..d074b9509 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/test.lua @@ -0,0 +1,100 @@ + +local test = {} + +local fetch = require("luarocks.fetch") +local deps = require("luarocks.deps") +local util = require("luarocks.util") + +local test_types = { + "busted", + "command", +} + +local test_modules = {} + +for _, test_type in ipairs(test_types) do + local mod = require("luarocks.test." .. test_type) + table.insert(test_modules, mod) + test_modules[test_type] = mod + test_modules[mod] = test_type +end + +local function get_test_type(rockspec) + if rockspec.test and rockspec.test.type then + return rockspec.test.type + end + + for _, test_module in ipairs(test_modules) do + if test_module.detect_type() then + return test_modules[test_module] + end + end + + return nil, "could not detect test type -- no test suite for " .. rockspec.package .. "?" +end + +-- Run test suite as configured in rockspec in the current directory. +function test.run_test_suite(rockspec_arg, test_type, args, prepare) + local rockspec + if type(rockspec_arg) == "string" then + local err, errcode + rockspec, err, errcode = fetch.load_rockspec(rockspec_arg) + if err then + return nil, err, errcode + end + else + assert(type(rockspec_arg) == "table") + rockspec = rockspec_arg + end + + if not test_type then + local err + test_type, err = get_test_type(rockspec, test_type) + if not test_type then + return nil, err + end + end + assert(test_type) + + local all_deps = { + "dependencies", + "build_dependencies", + "test_dependencies", + } + for _, dep_kind in ipairs(all_deps) do + if rockspec[dep_kind] and next(rockspec[dep_kind]) then + local ok, err, errcode = deps.fulfill_dependencies(rockspec, dep_kind, "all") + if err then + return nil, err, errcode + end + end + end + + local mod_name = "luarocks.test." .. test_type + local pok, test_mod = pcall(require, mod_name) + if not pok then + return nil, "failed loading test execution module " .. mod_name + end + + if prepare then + if test_type == "busted" then + return test_mod.run_tests(rockspec_arg, {"--version"}) + else + return true + end + else + local flags = rockspec.test and rockspec.test.flags + if type(flags) == "table" then + util.variable_substitutions(flags, rockspec.variables) + + -- insert any flags given in test.flags at the front of args + for i = 1, #flags do + table.insert(args, i, flags[i]) + end + end + + return test_mod.run_tests(rockspec.test, args) + end +end + +return test diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/test/busted.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/test/busted.lua new file mode 100644 index 000000000..c73909cf0 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/test/busted.lua @@ -0,0 +1,53 @@ + +local busted = {} + +local fs = require("luarocks.fs") +local deps = require("luarocks.deps") +local path = require("luarocks.path") +local dir = require("luarocks.dir") +local queries = require("luarocks.queries") + +local unpack = table.unpack or unpack + +function busted.detect_type() + if fs.exists(".busted") then + return true + end + return false +end + +function busted.run_tests(test, args) + if not test then + test = {} + end + + local ok, bustedver, where = deps.fulfill_dependency(queries.new("busted"), nil, nil, nil, "test_dependencies") + if not ok then + return nil, bustedver + end + + local busted_exe + if test.busted_executable then + busted_exe = test.busted_executable + else + busted_exe = dir.path(path.root_dir(where), "bin", "busted") + + -- Windows fallback + local busted_bat = dir.path(path.root_dir(where), "bin", "busted.bat") + + if not fs.exists(busted_exe) and not fs.exists(busted_bat) then + return nil, "'busted' executable failed to be installed" + end + end + + local err + ok, err = fs.execute(busted_exe, unpack(args)) + if ok then + return true + else + return nil, err or "test suite failed." + end +end + + +return busted diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/test/command.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/test/command.lua new file mode 100644 index 000000000..58fa22cbd --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/test/command.lua @@ -0,0 +1,47 @@ + +local command = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local cfg = require("luarocks.core.cfg") + +local unpack = table.unpack or unpack + +function command.detect_type() + if fs.exists("test.lua") then + return true + end + return false +end + +function command.run_tests(test, args) + if not test then + test = { + script = "test.lua" + } + end + + if not test.script and not test.command then + test.script = "test.lua" + end + + local ok + + if test.script then + if not fs.exists(test.script) then + return nil, "Test script " .. test.script .. " does not exist" + end + local lua = fs.Q(dir.path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter)) -- get lua interpreter configured + ok = fs.execute(lua, test.script, unpack(args)) + elseif test.command then + ok = fs.execute(test.command, unpack(args)) + end + + if ok then + return true + else + return nil, "tests failed with non-zero exit code" + end +end + +return command diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/tools/patch.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/tools/patch.lua new file mode 100644 index 000000000..6f36d7131 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/tools/patch.lua @@ -0,0 +1,716 @@ +--- Patch utility to apply unified diffs. +-- +-- http://lua-users.org/wiki/LuaPatch +-- +-- (c) 2008 David Manura, Licensed under the same terms as Lua (MIT license). +-- Code is heavily based on the Python-based patch.py version 8.06-1 +-- Copyright (c) 2008 rainforce.org, MIT License +-- Project home: http://code.google.com/p/python-patch/ . +-- Version 0.1 + +local patch = {} + +local fs = require("luarocks.fs") +local fun = require("luarocks.fun") + +local io = io +local os = os +local string = string +local table = table +local format = string.format + +-- logging +local debugmode = false +local function debug(_) end +local function info(_) end +local function warning(s) io.stderr:write(s .. '\n') end + +-- Returns boolean whether string s2 starts with string s. +local function startswith(s, s2) + return s:sub(1, #s2) == s2 +end + +-- Returns boolean whether string s2 ends with string s. +local function endswith(s, s2) + return #s >= #s2 and s:sub(#s-#s2+1) == s2 +end + +-- Returns string s after filtering out any new-line characters from end. +local function endlstrip(s) + return s:gsub('[\r\n]+$', '') +end + +-- Returns shallow copy of table t. +local function table_copy(t) + local t2 = {} + for k,v in pairs(t) do t2[k] = v end + return t2 +end + +local function exists(filename) + local fh = io.open(filename) + local result = fh ~= nil + if fh then fh:close() end + return result +end +local function isfile() return true end --FIX? + +local function string_as_file(s) + return { + at = 0, + str = s, + len = #s, + eof = false, + read = function(self, n) + if self.eof then return nil end + local chunk = self.str:sub(self.at, self.at + n - 1) + self.at = self.at + n + if self.at > self.len then + self.eof = true + end + return chunk + end, + close = function(self) + self.eof = true + end, + } +end + +-- +-- file_lines(f) is similar to f:lines() for file f. +-- The main difference is that read_lines includes +-- new-line character sequences ("\n", "\r\n", "\r"), +-- if any, at the end of each line. Embedded "\0" are also handled. +-- Caution: The newline behavior can depend on whether f is opened +-- in binary or ASCII mode. +-- (file_lines - version 20080913) +-- +local function file_lines(f) + local CHUNK_SIZE = 1024 + local buffer = "" + local pos_beg = 1 + return function() + local pos, chars + while 1 do + pos, chars = buffer:match('()([\r\n].)', pos_beg) + if pos or not f then + break + elseif f then + local chunk = f:read(CHUNK_SIZE) + if chunk then + buffer = buffer:sub(pos_beg) .. chunk + pos_beg = 1 + else + f = nil + end + end + end + if not pos then + pos = #buffer + elseif chars == '\r\n' then + pos = pos + 1 + end + local line = buffer:sub(pos_beg, pos) + pos_beg = pos + 1 + if #line > 0 then + return line + end + end +end + +local function match_linerange(line) + local m1, m2, m3, m4 = line:match("^@@ %-(%d+),(%d+) %+(%d+),(%d+)") + if not m1 then m1, m3, m4 = line:match("^@@ %-(%d+) %+(%d+),(%d+)") end + if not m1 then m1, m2, m3 = line:match("^@@ %-(%d+),(%d+) %+(%d+)") end + if not m1 then m1, m3 = line:match("^@@ %-(%d+) %+(%d+)") end + return m1, m2, m3, m4 +end + +local function match_epoch(str) + return str:match("[^0-9]1969[^0-9]") or str:match("[^0-9]1970[^0-9]") +end + +function patch.read_patch(filename, data) + -- define possible file regions that will direct the parser flow + local state = 'header' + -- 'header' - comments before the patch body + -- 'filenames' - lines starting with --- and +++ + -- 'hunkhead' - @@ -R +R @@ sequence + -- 'hunkbody' + -- 'hunkskip' - skipping invalid hunk mode + + local all_ok = true + local lineends = {lf=0, crlf=0, cr=0} + local files = {source={}, target={}, epoch={}, hunks={}, fileends={}, hunkends={}} + local nextfileno = 0 + local nexthunkno = 0 --: even if index starts with 0 user messages + -- number hunks from 1 + + -- hunkinfo holds parsed values, hunkactual - calculated + local hunkinfo = { + startsrc=nil, linessrc=nil, starttgt=nil, linestgt=nil, + invalid=false, text={} + } + local hunkactual = {linessrc=nil, linestgt=nil} + + info(format("reading patch %s", filename)) + + local fp + if data then + fp = string_as_file(data) + else + fp = filename == '-' and io.stdin or assert(io.open(filename, "rb")) + end + local lineno = 0 + + for line in file_lines(fp) do + lineno = lineno + 1 + if state == 'header' then + if startswith(line, "--- ") then + state = 'filenames' + end + -- state is 'header' or 'filenames' + end + if state == 'hunkbody' then + -- skip hunkskip and hunkbody code until definition of hunkhead read + + if line:match"^[\r\n]*$" then + -- prepend space to empty lines to interpret them as context properly + line = " " .. line + end + + -- process line first + if line:match"^[- +\\]" then + -- gather stats about line endings + local he = files.hunkends[nextfileno] + if endswith(line, "\r\n") then + he.crlf = he.crlf + 1 + elseif endswith(line, "\n") then + he.lf = he.lf + 1 + elseif endswith(line, "\r") then + he.cr = he.cr + 1 + end + if startswith(line, "-") then + hunkactual.linessrc = hunkactual.linessrc + 1 + elseif startswith(line, "+") then + hunkactual.linestgt = hunkactual.linestgt + 1 + elseif startswith(line, "\\") then + -- nothing + else + hunkactual.linessrc = hunkactual.linessrc + 1 + hunkactual.linestgt = hunkactual.linestgt + 1 + end + table.insert(hunkinfo.text, line) + -- todo: handle \ No newline cases + else + warning(format("invalid hunk no.%d at %d for target file %s", + nexthunkno, lineno, files.target[nextfileno])) + -- add hunk status node + table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) + files.hunks[nextfileno][nexthunkno].invalid = true + all_ok = false + state = 'hunkskip' + end + + -- check exit conditions + if hunkactual.linessrc > hunkinfo.linessrc or + hunkactual.linestgt > hunkinfo.linestgt + then + warning(format("extra hunk no.%d lines at %d for target %s", + nexthunkno, lineno, files.target[nextfileno])) + -- add hunk status node + table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) + files.hunks[nextfileno][nexthunkno].invalid = true + state = 'hunkskip' + elseif hunkinfo.linessrc == hunkactual.linessrc and + hunkinfo.linestgt == hunkactual.linestgt + then + table.insert(files.hunks[nextfileno], table_copy(hunkinfo)) + state = 'hunkskip' + + -- detect mixed window/unix line ends + local ends = files.hunkends[nextfileno] + if (ends.cr~=0 and 1 or 0) + (ends.crlf~=0 and 1 or 0) + + (ends.lf~=0 and 1 or 0) > 1 + then + warning(format("inconsistent line ends in patch hunks for %s", + files.source[nextfileno])) + end + end + -- state is 'hunkbody' or 'hunkskip' + end + + if state == 'hunkskip' then + if match_linerange(line) then + state = 'hunkhead' + elseif startswith(line, "--- ") then + state = 'filenames' + if debugmode and #files.source > 0 then + debug(format("- %2d hunks for %s", #files.hunks[nextfileno], + files.source[nextfileno])) + end + end + -- state is 'hunkskip', 'hunkhead', or 'filenames' + end + local advance + if state == 'filenames' then + if startswith(line, "--- ") then + if fun.contains(files.source, nextfileno) then + all_ok = false + warning(format("skipping invalid patch for %s", + files.source[nextfileno+1])) + table.remove(files.source, nextfileno+1) + -- double source filename line is encountered + -- attempt to restart from this second line + end + -- Accept a space as a terminator, like GNU patch does. + -- Breaks patches containing filenames with spaces... + -- FIXME Figure out what does GNU patch do in those cases. + local match, rest = line:match("^%-%-%- ([^ \t\r\n]+)(.*)") + if not match then + all_ok = false + warning(format("skipping invalid filename at line %d", lineno+1)) + state = 'header' + else + if match_epoch(rest) then + files.epoch[nextfileno + 1] = true + end + table.insert(files.source, match) + end + elseif not startswith(line, "+++ ") then + if fun.contains(files.source, nextfileno) then + all_ok = false + warning(format("skipping invalid patch with no target for %s", + files.source[nextfileno+1])) + table.remove(files.source, nextfileno+1) + else + -- this should be unreachable + warning("skipping invalid target patch") + end + state = 'header' + else + if fun.contains(files.target, nextfileno) then + all_ok = false + warning(format("skipping invalid patch - double target at line %d", + lineno+1)) + table.remove(files.source, nextfileno+1) + table.remove(files.target, nextfileno+1) + nextfileno = nextfileno - 1 + -- double target filename line is encountered + -- switch back to header state + state = 'header' + else + -- Accept a space as a terminator, like GNU patch does. + -- Breaks patches containing filenames with spaces... + -- FIXME Figure out what does GNU patch do in those cases. + local re_filename = "^%+%+%+ ([^ \t\r\n]+)(.*)$" + local match, rest = line:match(re_filename) + if not match then + all_ok = false + warning(format( + "skipping invalid patch - no target filename at line %d", + lineno+1)) + state = 'header' + else + table.insert(files.target, match) + nextfileno = nextfileno + 1 + if match_epoch(rest) then + files.epoch[nextfileno] = true + end + nexthunkno = 0 + table.insert(files.hunks, {}) + table.insert(files.hunkends, table_copy(lineends)) + table.insert(files.fileends, table_copy(lineends)) + state = 'hunkhead' + advance = true + end + end + end + -- state is 'filenames', 'header', or ('hunkhead' with advance) + end + if not advance and state == 'hunkhead' then + local m1, m2, m3, m4 = match_linerange(line) + if not m1 then + if not fun.contains(files.hunks, nextfileno-1) then + all_ok = false + warning(format("skipping invalid patch with no hunks for file %s", + files.target[nextfileno])) + end + state = 'header' + else + hunkinfo.startsrc = tonumber(m1) + hunkinfo.linessrc = tonumber(m2 or 1) + hunkinfo.starttgt = tonumber(m3) + hunkinfo.linestgt = tonumber(m4 or 1) + hunkinfo.invalid = false + hunkinfo.text = {} + + hunkactual.linessrc = 0 + hunkactual.linestgt = 0 + + state = 'hunkbody' + nexthunkno = nexthunkno + 1 + end + -- state is 'header' or 'hunkbody' + end + end + if state ~= 'hunkskip' then + warning(format("patch file incomplete - %s", filename)) + all_ok = false + -- os.exit(?) + else + -- duplicated message when an eof is reached + if debugmode and #files.source > 0 then + debug(format("- %2d hunks for %s", #files.hunks[nextfileno], + files.source[nextfileno])) + end + end + + local sum = 0; for _,hset in ipairs(files.hunks) do sum = sum + #hset end + info(format("total files: %d total hunks: %d", #files.source, sum)) + fp:close() + return files, all_ok +end + +local function find_hunk(file, h, hno) + for fuzz=0,2 do + local lineno = h.startsrc + for i=0,#file do + local found = true + local location = lineno + for l, hline in ipairs(h.text) do + if l > fuzz then + -- todo: \ No newline at the end of file + if startswith(hline, " ") or startswith(hline, "-") then + local line = file[lineno] + lineno = lineno + 1 + if not line or #line == 0 then + found = false + break + end + if endlstrip(line) ~= endlstrip(hline:sub(2)) then + found = false + break + end + end + end + end + if found then + local offset = location - h.startsrc - fuzz + if offset ~= 0 then + warning(format("Hunk %d found at offset %d%s...", hno, offset, fuzz == 0 and "" or format(" (fuzz %d)", fuzz))) + end + h.startsrc = location + h.starttgt = h.starttgt + offset + for _=1,fuzz do + table.remove(h.text, 1) + table.remove(h.text, #h.text) + end + return true + end + lineno = i + end + end + return false +end + +local function load_file(filename) + local fp = assert(io.open(filename)) + local file = {} + local readline = file_lines(fp) + while true do + local line = readline() + if not line then break end + table.insert(file, line) + end + fp:close() + return file +end + +local function find_hunks(file, hunks) + for hno, h in ipairs(hunks) do + find_hunk(file, h, hno) + end +end + +local function check_patched(file, hunks) + local lineno = 1 + local ok, err = pcall(function() + if #file == 0 then + error('nomatch', 0) + end + for hno, h in ipairs(hunks) do + -- skip to line just before hunk starts + if #file < h.starttgt then + error('nomatch', 0) + end + lineno = h.starttgt + for _, hline in ipairs(h.text) do + -- todo: \ No newline at the end of file + if not startswith(hline, "-") and not startswith(hline, "\\") then + local line = file[lineno] + lineno = lineno + 1 + if #line == 0 then + error('nomatch', 0) + end + if endlstrip(line) ~= endlstrip(hline:sub(2)) then + warning(format("file is not patched - failed hunk: %d", hno)) + error('nomatch', 0) + end + end + end + end + end) + -- todo: display failed hunk, i.e. expected/found + return err ~= 'nomatch' +end + +local function patch_hunks(srcname, tgtname, hunks) + local src = assert(io.open(srcname, "rb")) + local tgt = assert(io.open(tgtname, "wb")) + + local src_readline = file_lines(src) + + -- todo: detect linefeeds early - in apply_files routine + -- to handle cases when patch starts right from the first + -- line and no lines are processed. At the moment substituted + -- lineends may not be the same at the start and at the end + -- of patching. Also issue a warning about mixed lineends + + local srclineno = 1 + local lineends = {['\n']=0, ['\r\n']=0, ['\r']=0} + for hno, h in ipairs(hunks) do + debug(format("processing hunk %d for file %s", hno, tgtname)) + -- skip to line just before hunk starts + while srclineno < h.startsrc do + local line = src_readline() + -- Python 'U' mode works only with text files + if endswith(line, "\r\n") then + lineends["\r\n"] = lineends["\r\n"] + 1 + elseif endswith(line, "\n") then + lineends["\n"] = lineends["\n"] + 1 + elseif endswith(line, "\r") then + lineends["\r"] = lineends["\r"] + 1 + end + tgt:write(line) + srclineno = srclineno + 1 + end + + for _,hline in ipairs(h.text) do + -- todo: check \ No newline at the end of file + if startswith(hline, "-") or startswith(hline, "\\") then + src_readline() + srclineno = srclineno + 1 + else + if not startswith(hline, "+") then + src_readline() + srclineno = srclineno + 1 + end + local line2write = hline:sub(2) + -- detect if line ends are consistent in source file + local sum = 0 + for _,v in pairs(lineends) do if v > 0 then sum=sum+1 end end + if sum == 1 then + local newline + for k,v in pairs(lineends) do if v ~= 0 then newline = k end end + tgt:write(endlstrip(line2write) .. newline) + else -- newlines are mixed or unknown + tgt:write(line2write) + end + end + end + end + for line in src_readline do + tgt:write(line) + end + tgt:close() + src:close() + return true +end + +local function strip_dirs(filename, strip) + if strip == nil then return filename end + for _=1,strip do + filename=filename:gsub("^[^/]*/", "") + end + return filename +end + +local function write_new_file(filename, hunk) + local fh = io.open(filename, "wb") + if not fh then return false end + for _, hline in ipairs(hunk.text) do + local c = hline:sub(1,1) + if c ~= "+" and c ~= "-" and c ~= " " then + return false, "malformed patch" + end + fh:write(hline:sub(2)) + end + fh:close() + return true +end + +local function patch_file(source, target, epoch, hunks, strip, create_delete) + local create_file = false + if create_delete then + local is_src_epoch = epoch and #hunks == 1 and hunks[1].startsrc == 0 and hunks[1].linessrc == 0 + if is_src_epoch or source == "/dev/null" then + info(format("will create %s", target)) + create_file = true + end + end + if create_file then + return write_new_file(fs.absolute_name(strip_dirs(target, strip)), hunks[1]) + end + source = strip_dirs(source, strip) + local f2patch = source + if not exists(f2patch) then + f2patch = strip_dirs(target, strip) + f2patch = fs.absolute_name(f2patch) + if not exists(f2patch) then --FIX:if f2patch nil + warning(format("source/target file does not exist\n--- %s\n+++ %s", + source, f2patch)) + return false + end + end + if not isfile(f2patch) then + warning(format("not a file - %s", f2patch)) + return false + end + + source = f2patch + + -- validate before patching + local file = load_file(source) + local hunkno = 1 + local hunk = hunks[hunkno] + local hunkfind = {} + local validhunks = 0 + local canpatch = false + local hunklineno + if not file then + return nil, "failed reading file " .. source + end + + if create_delete then + if epoch and #hunks == 1 and hunks[1].starttgt == 0 and hunks[1].linestgt == 0 then + local ok = os.remove(source) + if not ok then + return false + end + info(format("successfully removed %s", source)) + return true + end + end + + find_hunks(file, hunks) + + local function process_line(line, lineno) + if not hunk or lineno < hunk.startsrc then + return false + end + if lineno == hunk.startsrc then + hunkfind = {} + for _,x in ipairs(hunk.text) do + if x:sub(1,1) == ' ' or x:sub(1,1) == '-' then + hunkfind[#hunkfind+1] = endlstrip(x:sub(2)) + end + end + hunklineno = 1 + + -- todo \ No newline at end of file + end + -- check hunks in source file + if lineno < hunk.startsrc + #hunkfind - 1 then + if endlstrip(line) == hunkfind[hunklineno] then + hunklineno = hunklineno + 1 + else + debug(format("hunk no.%d doesn't match source file %s", + hunkno, source)) + -- file may be already patched, but check other hunks anyway + hunkno = hunkno + 1 + if hunkno <= #hunks then + hunk = hunks[hunkno] + return false + else + return true + end + end + end + -- check if processed line is the last line + if lineno == hunk.startsrc + #hunkfind - 1 then + debug(format("file %s hunk no.%d -- is ready to be patched", + source, hunkno)) + hunkno = hunkno + 1 + validhunks = validhunks + 1 + if hunkno <= #hunks then + hunk = hunks[hunkno] + else + if validhunks == #hunks then + -- patch file + canpatch = true + return true + end + end + end + return false + end + + local done = false + for lineno, line in ipairs(file) do + done = process_line(line, lineno) + if done then + break + end + end + if not done then + if hunkno <= #hunks and not create_file then + warning(format("premature end of source file %s at hunk %d", + source, hunkno)) + return false + end + end + if validhunks < #hunks then + if check_patched(file, hunks) then + warning(format("already patched %s", source)) + elseif not create_file then + warning(format("source file is different - %s", source)) + return false + end + end + if not canpatch then + return true + end + local backupname = source .. ".orig" + if exists(backupname) then + warning(format("can't backup original file to %s - aborting", + backupname)) + return false + end + local ok = os.rename(source, backupname) + if not ok then + warning(format("failed backing up %s when patching", source)) + return false + end + patch_hunks(backupname, source, hunks) + info(format("successfully patched %s", source)) + os.remove(backupname) + return true +end + +function patch.apply_patch(the_patch, strip, create_delete) + local all_ok = true + local total = #the_patch.source + for fileno, source in ipairs(the_patch.source) do + local target = the_patch.target[fileno] + local hunks = the_patch.hunks[fileno] + local epoch = the_patch.epoch[fileno] + info(format("processing %d/%d:\t %s", fileno, total, source)) + local ok = patch_file(source, target, epoch, hunks, strip, create_delete) + all_ok = all_ok and ok + end + -- todo: check for premature eof + return all_ok +end + +return patch diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/tools/tar.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/tools/tar.lua new file mode 100644 index 000000000..21d5920b0 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/tools/tar.lua @@ -0,0 +1,174 @@ + +--- A pure-Lua implementation of untar (unpacking .tar archives) +local tar = {} + +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local fun = require("luarocks.fun") + +local blocksize = 512 + +local function get_typeflag(flag) + if flag == "0" or flag == "\0" then return "file" + elseif flag == "1" then return "link" + elseif flag == "2" then return "symlink" -- "reserved" in POSIX, "symlink" in GNU + elseif flag == "3" then return "character" + elseif flag == "4" then return "block" + elseif flag == "5" then return "directory" + elseif flag == "6" then return "fifo" + elseif flag == "7" then return "contiguous" -- "reserved" in POSIX, "contiguous" in GNU + elseif flag == "x" then return "next file" + elseif flag == "g" then return "global extended header" + elseif flag == "L" then return "long name" + elseif flag == "K" then return "long link name" + end + return "unknown" +end + +local function octal_to_number(octal) + local exp = 0 + local number = 0 + octal = octal:gsub("%s", "") + for i = #octal,1,-1 do + local digit = tonumber(octal:sub(i,i)) + if not digit then + break + end + number = number + (digit * 8^exp) + exp = exp + 1 + end + return number +end + +local function checksum_header(block) + local sum = 256 + for i = 1,148 do + local b = block:byte(i) or 0 + sum = sum + b + end + for i = 157,500 do + local b = block:byte(i) or 0 + sum = sum + b + end + return sum +end + +local function nullterm(s) + return s:match("^[^%z]*") +end + +local function read_header_block(block) + local header = {} + header.name = nullterm(block:sub(1,100)) + header.mode = nullterm(block:sub(101,108)):gsub(" ", "") + header.uid = octal_to_number(nullterm(block:sub(109,116))) + header.gid = octal_to_number(nullterm(block:sub(117,124))) + header.size = octal_to_number(nullterm(block:sub(125,136))) + header.mtime = octal_to_number(nullterm(block:sub(137,148))) + header.chksum = octal_to_number(nullterm(block:sub(149,156))) + header.typeflag = get_typeflag(block:sub(157,157)) + header.linkname = nullterm(block:sub(158,257)) + header.magic = block:sub(258,263) + header.version = block:sub(264,265) + header.uname = nullterm(block:sub(266,297)) + header.gname = nullterm(block:sub(298,329)) + header.devmajor = octal_to_number(nullterm(block:sub(330,337))) + header.devminor = octal_to_number(nullterm(block:sub(338,345))) + header.prefix = block:sub(346,500) + -- if header.magic ~= "ustar " and header.magic ~= "ustar\0" then + -- return false, ("Invalid header magic %6x"):format(bestring_to_number(header.magic)) + -- end + -- if header.version ~= "00" and header.version ~= " \0" then + -- return false, "Unknown version "..header.version + -- end + if checksum_header(block) ~= header.chksum then + return false, "Failed header checksum" + end + return header +end + +function tar.untar(filename, destdir) + assert(type(filename) == "string") + assert(type(destdir) == "string") + + local tar_handle = io.open(filename, "rb") + if not tar_handle then return nil, "Error opening file "..filename end + + local long_name, long_link_name + local ok, err + local make_dir = fun.memoize(fs.make_dir) + while true do + local block + repeat + block = tar_handle:read(blocksize) + until (not block) or checksum_header(block) > 256 + if not block then break end + if #block < blocksize then + ok, err = nil, "Invalid block size -- corrupted file?" + break + end + local header + header, err = read_header_block(block) + if not header then + ok = false + break + end + + local file_data = tar_handle:read(math.ceil(header.size / blocksize) * blocksize):sub(1,header.size) + + if header.typeflag == "long name" then + long_name = nullterm(file_data) + elseif header.typeflag == "long link name" then + long_link_name = nullterm(file_data) + else + if long_name then + header.name = long_name + long_name = nil + end + if long_link_name then + header.name = long_link_name + long_link_name = nil + end + end + local pathname = dir.path(destdir, header.name) + pathname = fs.absolute_name(pathname) + if header.typeflag == "directory" then + ok, err = make_dir(pathname) + if not ok then + break + end + elseif header.typeflag == "file" then + local dirname = dir.dir_name(pathname) + if dirname ~= "" then + ok, err = make_dir(dirname) + if not ok then + break + end + end + local file_handle + file_handle, err = io.open(pathname, "wb") + if not file_handle then + ok = nil + break + end + file_handle:write(file_data) + file_handle:close() + fs.set_time(pathname, header.mtime) + if header.mode:match("[75]") then + fs.set_permissions(pathname, "exec", "all") + else + fs.set_permissions(pathname, "read", "all") + end + end + --[[ + for k,v in pairs(header) do + util.printout("[\""..tostring(k).."\"] = "..(type(v)=="number" and v or "\""..v:gsub("%z", "\\0").."\"")) + end + util.printout() + --]] + end + tar_handle:close() + return ok, err +end + +return tar diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/tools/zip.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/tools/zip.lua new file mode 100644 index 000000000..82d582fa8 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/tools/zip.lua @@ -0,0 +1,531 @@ + +--- A Lua implementation of .zip and .gz file compression and decompression, +-- using only lzlib or lua-lzib. +local zip = {} + +local zlib = require("zlib") +local fs = require("luarocks.fs") +local fun = require("luarocks.fun") +local dir = require("luarocks.dir") + +local pack = table.pack or function(...) return { n = select("#", ...), ... } end + +local function shr(n, m) + return math.floor(n / 2^m) +end + +local function shl(n, m) + return n * 2^m +end +local function lowbits(n, m) + return n % 2^m +end + +local function mode_to_windowbits(mode) + if mode == "gzip" then + return 31 + elseif mode == "zlib" then + return 0 + elseif mode == "raw" then + return -15 + end +end + +-- zlib module can be provided by both lzlib and lua-lzib packages. +-- Create a compatibility layer. +local zlib_compress, zlib_uncompress, zlib_crc32 +if zlib._VERSION:match "^lua%-zlib" then + function zlib_compress(data, mode) + return (zlib.deflate(6, mode_to_windowbits(mode))(data, "finish")) + end + + function zlib_uncompress(data, mode) + return (zlib.inflate(mode_to_windowbits(mode))(data)) + end + + function zlib_crc32(data) + return zlib.crc32()(data) + end +elseif zlib._VERSION:match "^lzlib" then + function zlib_compress(data, mode) + return zlib.compress(data, -1, nil, mode_to_windowbits(mode)) + end + + function zlib_uncompress(data, mode) + return zlib.decompress(data, mode_to_windowbits(mode)) + end + + function zlib_crc32(data) + return zlib.crc32(zlib.crc32(), data) + end +else + error("unknown zlib library", 0) +end + +local function number_to_lestring(number, nbytes) + local out = {} + for _ = 1, nbytes do + local byte = number % 256 + table.insert(out, string.char(byte)) + number = (number - byte) / 256 + end + return table.concat(out) +end + +local function lestring_to_number(str) + local n = 0 + local bytes = { string.byte(str, 1, #str) } + for b = 1, #str do + n = n + shl(bytes[b], (b-1)*8) + end + return math.floor(n) +end + +local LOCAL_FILE_HEADER_SIGNATURE = number_to_lestring(0x04034b50, 4) +local DATA_DESCRIPTOR_SIGNATURE = number_to_lestring(0x08074b50, 4) +local CENTRAL_DIRECTORY_SIGNATURE = number_to_lestring(0x02014b50, 4) +local END_OF_CENTRAL_DIR_SIGNATURE = number_to_lestring(0x06054b50, 4) + +--- Begin a new file to be stored inside the zipfile. +-- @param self handle of the zipfile being written. +-- @param filename filenome of the file to be added to the zipfile. +-- @return true if succeeded, nil in case of failure. +local function zipwriter_open_new_file_in_zip(self, filename) + if self.in_open_file then + self:close_file_in_zip() + return nil + end + local lfh = {} + self.local_file_header = lfh + lfh.last_mod_file_time = 0 -- TODO + lfh.last_mod_file_date = 0 -- TODO + lfh.file_name_length = #filename + lfh.extra_field_length = 0 + lfh.file_name = filename:gsub("\\", "/") + lfh.external_attr = shl(493, 16) -- TODO proper permissions + self.in_open_file = true + return true +end + +--- Write data to the file currently being stored in the zipfile. +-- @param self handle of the zipfile being written. +-- @param data string containing full contents of the file. +-- @return true if succeeded, nil in case of failure. +local function zipwriter_write_file_in_zip(self, data) + if not self.in_open_file then + return nil + end + local lfh = self.local_file_header + local compressed = zlib_compress(data, "raw") + lfh.crc32 = zlib_crc32(data) + lfh.compressed_size = #compressed + lfh.uncompressed_size = #data + self.data = compressed + return true +end + +--- Complete the writing of a file stored in the zipfile. +-- @param self handle of the zipfile being written. +-- @return true if succeeded, nil in case of failure. +local function zipwriter_close_file_in_zip(self) + local zh = self.ziphandle + + if not self.in_open_file then + return nil + end + + -- Local file header + local lfh = self.local_file_header + lfh.offset = zh:seek() + zh:write(LOCAL_FILE_HEADER_SIGNATURE) + zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0 + zh:write(number_to_lestring(4, 2)) -- general purpose bit flag + zh:write(number_to_lestring(8, 2)) -- compression method: deflate + zh:write(number_to_lestring(lfh.last_mod_file_time, 2)) + zh:write(number_to_lestring(lfh.last_mod_file_date, 2)) + zh:write(number_to_lestring(lfh.crc32, 4)) + zh:write(number_to_lestring(lfh.compressed_size, 4)) + zh:write(number_to_lestring(lfh.uncompressed_size, 4)) + zh:write(number_to_lestring(lfh.file_name_length, 2)) + zh:write(number_to_lestring(lfh.extra_field_length, 2)) + zh:write(lfh.file_name) + + -- File data + zh:write(self.data) + + -- Data descriptor + zh:write(DATA_DESCRIPTOR_SIGNATURE) + zh:write(number_to_lestring(lfh.crc32, 4)) + zh:write(number_to_lestring(lfh.compressed_size, 4)) + zh:write(number_to_lestring(lfh.uncompressed_size, 4)) + + table.insert(self.files, lfh) + self.in_open_file = false + + return true +end + +-- @return boolean or (boolean, string): true on success, +-- false and an error message on failure. +local function zipwriter_add(self, file) + local fin + local ok, err = self:open_new_file_in_zip(file) + if not ok then + err = "error in opening "..file.." in zipfile" + else + fin = io.open(fs.absolute_name(file), "rb") + if not fin then + ok = false + err = "error opening "..file.." for reading" + end + end + if ok then + local data = fin:read("*a") + if not data then + err = "error reading "..file + ok = false + else + ok = self:write_file_in_zip(data) + if not ok then + err = "error in writing "..file.." in the zipfile" + end + end + end + if fin then + fin:close() + end + if ok then + ok = self:close_file_in_zip() + if not ok then + err = "error in writing "..file.." in the zipfile" + end + end + return ok == true, err +end + +--- Complete the writing of the zipfile. +-- @param self handle of the zipfile being written. +-- @return true if succeeded, nil in case of failure. +local function zipwriter_close(self) + local zh = self.ziphandle + + local central_directory_offset = zh:seek() + + local size_of_central_directory = 0 + -- Central directory structure + for _, lfh in ipairs(self.files) do + zh:write(CENTRAL_DIRECTORY_SIGNATURE) -- signature + zh:write(number_to_lestring(3, 2)) -- version made by: UNIX + zh:write(number_to_lestring(20, 2)) -- version needed to extract: 2.0 + zh:write(number_to_lestring(0, 2)) -- general purpose bit flag + zh:write(number_to_lestring(8, 2)) -- compression method: deflate + zh:write(number_to_lestring(lfh.last_mod_file_time, 2)) + zh:write(number_to_lestring(lfh.last_mod_file_date, 2)) + zh:write(number_to_lestring(lfh.crc32, 4)) + zh:write(number_to_lestring(lfh.compressed_size, 4)) + zh:write(number_to_lestring(lfh.uncompressed_size, 4)) + zh:write(number_to_lestring(lfh.file_name_length, 2)) + zh:write(number_to_lestring(lfh.extra_field_length, 2)) + zh:write(number_to_lestring(0, 2)) -- file comment length + zh:write(number_to_lestring(0, 2)) -- disk number start + zh:write(number_to_lestring(0, 2)) -- internal file attributes + zh:write(number_to_lestring(lfh.external_attr, 4)) -- external file attributes + zh:write(number_to_lestring(lfh.offset, 4)) -- relative offset of local header + zh:write(lfh.file_name) + size_of_central_directory = size_of_central_directory + 46 + lfh.file_name_length + end + + -- End of central directory record + zh:write(END_OF_CENTRAL_DIR_SIGNATURE) -- signature + zh:write(number_to_lestring(0, 2)) -- number of this disk + zh:write(number_to_lestring(0, 2)) -- number of disk with start of central directory + zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir on this disk + zh:write(number_to_lestring(#self.files, 2)) -- total number of entries in the central dir + zh:write(number_to_lestring(size_of_central_directory, 4)) + zh:write(number_to_lestring(central_directory_offset, 4)) + zh:write(number_to_lestring(0, 2)) -- zip file comment length + zh:close() + + return true +end + +--- Return a zip handle open for writing. +-- @param name filename of the zipfile to be created. +-- @return a zip handle, or nil in case of error. +function zip.new_zipwriter(name) + + local zw = {} + + zw.ziphandle = io.open(fs.absolute_name(name), "wb") + if not zw.ziphandle then + return nil + end + zw.files = {} + zw.in_open_file = false + + zw.add = zipwriter_add + zw.close = zipwriter_close + zw.open_new_file_in_zip = zipwriter_open_new_file_in_zip + zw.write_file_in_zip = zipwriter_write_file_in_zip + zw.close_file_in_zip = zipwriter_close_file_in_zip + + return zw +end + +--- Compress files in a .zip archive. +-- @param zipfile string: pathname of .zip archive to be created. +-- @param ... Filenames to be stored in the archive are given as +-- additional arguments. +-- @return boolean or (boolean, string): true on success, +-- false and an error message on failure. +function zip.zip(zipfile, ...) + local zw = zip.new_zipwriter(zipfile) + if not zw then + return nil, "error opening "..zipfile + end + + local args = pack(...) + local ok, err + for i=1, args.n do + local file = args[i] + if fs.is_dir(file) then + for _, entry in pairs(fs.find(file)) do + local fullname = dir.path(file, entry) + if fs.is_file(fullname) then + ok, err = zw:add(fullname) + if not ok then break end + end + end + else + ok, err = zw:add(file) + if not ok then break end + end + end + + zw:close() + return ok, err +end + + +local function ziptime_to_luatime(ztime, zdate) + local date = { + year = shr(zdate, 9) + 1980, + month = shr(lowbits(zdate, 9), 5), + day = lowbits(zdate, 5), + hour = shr(ztime, 11), + min = shr(lowbits(ztime, 11), 5), + sec = lowbits(ztime, 5) * 2, + } + + if date.month == 0 then date.month = 1 end + if date.day == 0 then date.day = 1 end + + return date +end + +local function read_file_in_zip(zh, cdr) + local sig = zh:read(4) + if sig ~= LOCAL_FILE_HEADER_SIGNATURE then + return nil, "failed reading Local File Header signature" + end + + -- Skip over the rest of the zip file header. See + -- zipwriter_close_file_in_zip for the format. + zh:seek("cur", 22) + local file_name_length = lestring_to_number(zh:read(2)) + local extra_field_length = lestring_to_number(zh:read(2)) + zh:read(file_name_length) + zh:read(extra_field_length) + + local data = zh:read(cdr.compressed_size) + + local uncompressed + if cdr.compression_method == 8 then + uncompressed = zlib_uncompress(data, "raw") + elseif cdr.compression_method == 0 then + uncompressed = data + else + return nil, "unknown compression method " .. cdr.compression_method + end + + if #uncompressed ~= cdr.uncompressed_size then + return nil, "uncompressed size doesn't match" + end + if cdr.crc32 ~= zlib_crc32(uncompressed) then + return nil, "crc32 failed (expected " .. cdr.crc32 .. ") - data: " .. uncompressed + end + + return uncompressed +end + +local function process_end_of_central_dir(zh) + local at, err = zh:seek("end", -22) + if not at then + return nil, err + end + + while true do + local sig = zh:read(4) + if sig == END_OF_CENTRAL_DIR_SIGNATURE then + break + end + at = at - 1 + local at1, err = zh:seek("set", at) + if at1 ~= at then + return nil, "Could not find End of Central Directory signature" + end + end + + -- number of this disk (2 bytes) + -- number of the disk with the start of the central directory (2 bytes) + -- total number of entries in the central directory on this disk (2 bytes) + -- total number of entries in the central directory (2 bytes) + zh:seek("cur", 6) + + local central_directory_entries = lestring_to_number(zh:read(2)) + + -- central directory size (4 bytes) + zh:seek("cur", 4) + + local central_directory_offset = lestring_to_number(zh:read(4)) + + return central_directory_entries, central_directory_offset +end + +local function process_central_dir(zh, cd_entries) + + local files = {} + + for i = 1, cd_entries do + local sig = zh:read(4) + if sig ~= CENTRAL_DIRECTORY_SIGNATURE then + return nil, "failed reading Central Directory signature" + end + + local cdr = {} + files[i] = cdr + + cdr.version_made_by = lestring_to_number(zh:read(2)) + cdr.version_needed = lestring_to_number(zh:read(2)) + cdr.bitflag = lestring_to_number(zh:read(2)) + cdr.compression_method = lestring_to_number(zh:read(2)) + cdr.last_mod_file_time = lestring_to_number(zh:read(2)) + cdr.last_mod_file_date = lestring_to_number(zh:read(2)) + cdr.last_mod_luatime = ziptime_to_luatime(cdr.last_mod_file_time, cdr.last_mod_file_date) + cdr.crc32 = lestring_to_number(zh:read(4)) + cdr.compressed_size = lestring_to_number(zh:read(4)) + cdr.uncompressed_size = lestring_to_number(zh:read(4)) + cdr.file_name_length = lestring_to_number(zh:read(2)) + cdr.extra_field_length = lestring_to_number(zh:read(2)) + cdr.file_comment_length = lestring_to_number(zh:read(2)) + cdr.disk_number_start = lestring_to_number(zh:read(2)) + cdr.internal_attr = lestring_to_number(zh:read(2)) + cdr.external_attr = lestring_to_number(zh:read(4)) + cdr.offset = lestring_to_number(zh:read(4)) + cdr.file_name = zh:read(cdr.file_name_length) + cdr.extra_field = zh:read(cdr.extra_field_length) + cdr.file_comment = zh:read(cdr.file_comment_length) + end + return files +end + +--- Uncompress files from a .zip archive. +-- @param zipfile string: pathname of .zip archive to be created. +-- @return boolean or (boolean, string): true on success, +-- false and an error message on failure. +function zip.unzip(zipfile) + zipfile = fs.absolute_name(zipfile) + local zh, err = io.open(zipfile, "rb") + if not zh then + return nil, err + end + + local cd_entries, cd_offset = process_end_of_central_dir(zh) + if not cd_entries then + return nil, cd_offset + end + + local ok, err = zh:seek("set", cd_offset) + if not ok then + return nil, err + end + + local files, err = process_central_dir(zh, cd_entries) + if not files then + return nil, err + end + + for _, cdr in ipairs(files) do + local file = cdr.file_name + if file:sub(#file) == "/" then + local ok, err = fs.make_dir(dir.path(fs.current_dir(), file)) + if not ok then + return nil, err + end + else + local base = dir.dir_name(file) + if base ~= "" then + base = dir.path(fs.current_dir(), base) + if not fs.is_dir(base) then + local ok, err = fs.make_dir(base) + if not ok then + return nil, err + end + end + end + + local ok, err = zh:seek("set", cdr.offset) + if not ok then + return nil, err + end + + local contents, err = read_file_in_zip(zh, cdr) + if not contents then + return nil, err + end + local pathname = dir.path(fs.current_dir(), file) + local wf, err = io.open(pathname, "wb") + if not wf then + zh:close() + return nil, err + end + wf:write(contents) + wf:close() + + if cdr.external_attr > 0 then + fs.set_permissions(pathname, "exec", "all") + else + fs.set_permissions(pathname, "read", "all") + end + fs.set_time(pathname, cdr.last_mod_luatime) + end + end + zh:close() + return true +end + +function zip.gzip(input_filename, output_filename) + assert(type(input_filename) == "string") + assert(output_filename == nil or type(output_filename) == "string") + + if not output_filename then + output_filename = input_filename .. ".gz" + end + + local fn = fun.partial(fun.flip(zlib_compress), "gzip") + return fs.filter_file(fn, input_filename, output_filename) +end + +function zip.gunzip(input_filename, output_filename) + assert(type(input_filename) == "string") + assert(output_filename == nil or type(output_filename) == "string") + + if not output_filename then + output_filename = input_filename:gsub("%.gz$", "") + end + + local fn = fun.partial(fun.flip(zlib_uncompress), "gzip") + return fs.filter_file(fn, input_filename, output_filename) +end + +return zip diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/type/manifest.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/type/manifest.lua new file mode 100644 index 000000000..043366eac --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/type/manifest.lua @@ -0,0 +1,80 @@ +local type_manifest = {} + +local type_check = require("luarocks.type_check") + +local manifest_formats = type_check.declare_schemas({ + ["3.0"] = { + repository = { + _mandatory = true, + -- packages + _any = { + -- versions + _any = { + -- items + _any = { + arch = { _type = "string", _mandatory = true }, + modules = { _any = { _type = "string" } }, + commands = { _any = { _type = "string" } }, + dependencies = { _any = { _type = "string" } }, + -- TODO: to be extended with more metadata. + } + } + } + }, + modules = { + _mandatory = true, + -- modules + _any = { + -- providers + _any = { _type = "string" } + } + }, + commands = { + _mandatory = true, + -- modules + _any = { + -- commands + _any = { _type = "string" } + } + }, + dependencies = { + -- each module + _any = { + -- each version + _any = { + -- each dependency + _any = { + name = { _type = "string" }, + namespace = { _type = "string" }, + constraints = { + _any = { + no_upgrade = { _type = "boolean" }, + op = { _type = "string" }, + version = { + string = { _type = "string" }, + _any = { _type = "number" }, + } + } + } + } + } + } + } + } +}) + +--- Type check a manifest table. +-- Verify the correctness of elements from a +-- manifest table, reporting on unknown fields and type +-- mismatches. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +function type_manifest.check(manifest, globals) + assert(type(manifest) == "table") + local format = manifest_formats["3.0"] + local ok, err = type_check.check_undeclared_globals(globals, format) + if not ok then return nil, err end + return type_check.type_check_table("3.0", manifest, format, "") +end + +return type_manifest diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/type/rockspec.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/type/rockspec.lua new file mode 100644 index 000000000..0b4b5dcfe --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/type/rockspec.lua @@ -0,0 +1,199 @@ +local type_rockspec = {} + +local type_check = require("luarocks.type_check") + +type_rockspec.rockspec_format = "3.0" + +-- Syntax for type-checking tables: +-- +-- A type-checking table describes typing data for a value. +-- Any key starting with an underscore has a special meaning: +-- _type (string) is the Lua type of the value. Default is "table". +-- _mandatory (boolean) indicates if the value is a mandatory key in its container table. Default is false. +-- For "string" types only: +-- _pattern (string) is the string-matching pattern, valid for string types only. Default is ".*". +-- For "table" types only: +-- _any (table) is the type-checking table for unspecified keys, recursively checked. +-- _more (boolean) indicates that the table accepts unspecified keys and does not type-check them. +-- Any other string keys that don't start with an underscore represent known keys and are type-checking tables, recursively checked. + +local rockspec_formats, versions = type_check.declare_schemas({ + ["1.0"] = { + rockspec_format = { _type = "string" }, + package = { _type = "string", _mandatory = true }, + version = { _type = "string", _pattern = "[%w.]+-[%d]+", _mandatory = true }, + description = { + summary = { _type = "string" }, + detailed = { _type = "string" }, + homepage = { _type = "string" }, + license = { _type = "string" }, + maintainer = { _type = "string" }, + }, + dependencies = { + platforms = type_check.MAGIC_PLATFORMS, + _any = { + _type = "string", + _name = "a valid dependency string", + _pattern = "%s*([a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)", + }, + }, + supported_platforms = { + _any = { _type = "string" }, + }, + external_dependencies = { + platforms = type_check.MAGIC_PLATFORMS, + _any = { + program = { _type = "string" }, + header = { _type = "string" }, + library = { _type = "string" }, + } + }, + source = { + _mandatory = true, + platforms = type_check.MAGIC_PLATFORMS, + url = { _type = "string", _mandatory = true }, + md5 = { _type = "string" }, + file = { _type = "string" }, + dir = { _type = "string" }, + tag = { _type = "string" }, + branch = { _type = "string" }, + module = { _type = "string" }, + cvs_tag = { _type = "string" }, + cvs_module = { _type = "string" }, + }, + build = { + platforms = type_check.MAGIC_PLATFORMS, + type = { _type = "string" }, + install = { + lua = { + _more = true + }, + lib = { + _more = true + }, + conf = { + _more = true + }, + bin = { + _more = true + } + }, + copy_directories = { + _any = { _type = "string" }, + }, + _more = true, + _mandatory = true + }, + hooks = { + platforms = type_check.MAGIC_PLATFORMS, + post_install = { _type = "string" }, + }, + }, + + ["1.1"] = { + deploy = { + wrap_bin_scripts = { _type = "boolean" }, + } + }, + + ["3.0"] = { + description = { + labels = { + _any = { _type = "string" } + }, + issues_url = { _type = "string" }, + }, + dependencies = { + _any = { + _pattern = "%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)", + }, + }, + build_dependencies = { + platforms = type_check.MAGIC_PLATFORMS, + _any = { + _type = "string", + _name = "a valid dependency string", + _pattern = "%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)", + }, + }, + test_dependencies = { + platforms = type_check.MAGIC_PLATFORMS, + _any = { + _type = "string", + _name = "a valid dependency string", + _pattern = "%s*([a-zA-Z0-9%.%-%_]*/?[a-zA-Z0-9][a-zA-Z0-9%.%-%_]*)%s*([^/]*)", + }, + }, + build = { + _mandatory = false, + }, + test = { + platforms = type_check.MAGIC_PLATFORMS, + type = { _type = "string" }, + _more = true, + }, + } +}) + +type_rockspec.order = {"rockspec_format", "package", "version", + { "source", { "url", "tag", "branch", "md5" } }, + { "description", {"summary", "detailed", "homepage", "license" } }, + "supported_platforms", "dependencies", "build_dependencies", "external_dependencies", + { "build", {"type", "modules", "copy_directories", "platforms"} }, + "test_dependencies", { "test", {"type"} }, + "hooks"} + +local function check_rockspec_using_version(rockspec, globals, version) + local schema = rockspec_formats[version] + if not schema then + return nil, "unknown rockspec format " .. version + end + local ok, err = type_check.check_undeclared_globals(globals, schema) + if ok then + ok, err = type_check.type_check_table(version, rockspec, schema, "") + end + if ok then + return true + else + return nil, err + end +end + +--- Type check a rockspec table. +-- Verify the correctness of elements from a +-- rockspec table, reporting on unknown fields and type +-- mismatches. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +function type_rockspec.check(rockspec, globals) + assert(type(rockspec) == "table") + + local version = rockspec.rockspec_format or "1.0" + local ok, err = check_rockspec_using_version(rockspec, globals, version) + if ok then + return true + end + + -- Rockspec parsing failed. + -- Let's see if it would pass using a later version. + + local found = false + for _, v in ipairs(versions) do + if not found then + if v == version then + found = true + end + else + local v_ok, v_err = check_rockspec_using_version(rockspec, globals, v) + if v_ok then + return nil, err .. " (using rockspec format " .. version .. " -- " .. + [[adding 'rockspec_format = "]] .. v .. [["' to the rockspec ]] .. + [[will fix this)]] + end + end + end + + return nil, err .. " (using rockspec format " .. version .. ")" +end + +return type_rockspec diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/type_check.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/type_check.lua new file mode 100644 index 000000000..21085ef9a --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/type_check.lua @@ -0,0 +1,213 @@ + +local type_check = {} + +local cfg = require("luarocks.core.cfg") +local fun = require("luarocks.fun") +local util = require("luarocks.util") +local vers = require("luarocks.core.vers") +-------------------------------------------------------------------------------- + +-- A magic constant that is not used anywhere in a schema definition +-- and retains equality when the table is deep-copied. +type_check.MAGIC_PLATFORMS = 0xEBABEFAC + +do + local function fill_in_version(tbl, version) + for _, v in pairs(tbl) do + if type(v) == "table" then + if v._version == nil then + v._version = version + end + fill_in_version(v) + end + end + end + + local function expand_magic_platforms(tbl) + for k,v in pairs(tbl) do + if v == type_check.MAGIC_PLATFORMS then + tbl[k] = { + _any = util.deep_copy(tbl) + } + tbl[k]._any[k] = nil + elseif type(v) == "table" then + expand_magic_platforms(v) + end + end + end + + -- Build a table of schemas. + -- @param versions a table where each key is a version number as a string, + -- and the value is a schema specification. Schema versions are considered + -- incremental: version "2.0" only needs to specify what's new/changed from + -- version "1.0". + function type_check.declare_schemas(inputs) + local schemas = {} + local parent_version + + local versions = fun.reverse_in(fun.sort_in(util.keys(inputs), vers.compare_versions)) + + for _, version in ipairs(versions) do + local schema = inputs[version] + if parent_version ~= nil then + local copy = util.deep_copy(schemas[parent_version]) + util.deep_merge(copy, schema) + schema = copy + end + fill_in_version(schema, version) + expand_magic_platforms(schema) + parent_version = version + schemas[version] = schema + end + + return schemas, versions + end +end + +-------------------------------------------------------------------------------- + +local function check_version(version, typetbl, context) + local typetbl_version = typetbl._version or "1.0" + if vers.compare_versions(typetbl_version, version) then + if context == "" then + return nil, "Invalid rockspec_format version number in rockspec? Please fix rockspec accordingly." + else + return nil, context.." is not supported in rockspec format "..version.." (requires version "..typetbl_version.."), please fix the rockspec_format field accordingly." + end + end + return true +end + +--- Type check an object. +-- The object is compared against an archetypical value +-- matching the expected type -- the actual values don't matter, +-- only their types. Tables are type checked recursively. +-- @param version string: The version of the item. +-- @param item any: The object being checked. +-- @param typetbl any: The type-checking table for the object. +-- @param context string: A string indicating the "context" where the +-- error occurred (the full table path), for error messages. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +-- @see type_check_table +local function type_check_item(version, item, typetbl, context) + assert(type(version) == "string") + + if typetbl._version and typetbl._version ~= "1.0" then + local ok, err = check_version(version, typetbl, context) + if not ok then + return nil, err + end + end + + local item_type = type(item) or "nil" + local expected_type = typetbl._type or "table" + + if expected_type == "number" then + if not tonumber(item) then + return nil, "Type mismatch on field "..context..": expected a number" + end + elseif expected_type == "string" then + if item_type ~= "string" then + return nil, "Type mismatch on field "..context..": expected a string, got "..item_type + end + local pattern = typetbl._pattern + if pattern then + if not item:match("^"..pattern.."$") then + local what = typetbl._name or ("'"..pattern.."'") + return nil, "Type mismatch on field "..context..": invalid value '"..item.."' does not match " .. what + end + end + elseif expected_type == "table" then + if item_type ~= expected_type then + return nil, "Type mismatch on field "..context..": expected a table" + else + return type_check.type_check_table(version, item, typetbl, context) + end + elseif item_type ~= expected_type then + return nil, "Type mismatch on field "..context..": expected "..expected_type + end + return true +end + +local function mkfield(context, field) + if context == "" then + return tostring(field) + elseif type(field) == "string" then + return context.."."..field + else + return context.."["..tostring(field).."]" + end +end + +--- Type check the contents of a table. +-- The table's contents are compared against a reference table, +-- which contains the recognized fields, with archetypical values +-- matching the expected types -- the actual values of items in the +-- reference table don't matter, only their types (ie, for field x +-- in tbl that is correctly typed, type(tbl.x) == type(types.x)). +-- If the reference table contains a field called MORE, then +-- unknown fields in the checked table are accepted. +-- If it contains a field called ANY, then its type will be +-- used to check any unknown fields. If a field is prefixed +-- with MUST_, it is mandatory; its absence from the table is +-- a type error. +-- Tables are type checked recursively. +-- @param version string: The version of tbl. +-- @param tbl table: The table to be type checked. +-- @param typetbl table: The type-checking table, containing +-- values for recognized fields in the checked table. +-- @param context string: A string indicating the "context" where the +-- error occurred (such as the name of the table the item is a part of), +-- to be used by error messages. +-- @return boolean or (nil, string): true if type checking +-- succeeded, or nil and an error message if it failed. +function type_check.type_check_table(version, tbl, typetbl, context) + assert(type(version) == "string") + assert(type(tbl) == "table") + assert(type(typetbl) == "table") + + local ok, err = check_version(version, typetbl, context) + if not ok then + return nil, err + end + + for k, v in pairs(tbl) do + local t = typetbl[k] or typetbl._any + if t then + local ok, err = type_check_item(version, v, t, mkfield(context, k)) + if not ok then return nil, err end + elseif typetbl._more then + -- Accept unknown field + else + if not cfg.accept_unknown_fields then + return nil, "Unknown field "..k + end + end + end + for k, v in pairs(typetbl) do + if k:sub(1,1) ~= "_" and v._mandatory then + if not tbl[k] then + return nil, "Mandatory field "..mkfield(context, k).." is missing." + end + end + end + return true +end + +function type_check.check_undeclared_globals(globals, typetbl) + local undeclared = {} + for glob, _ in pairs(globals) do + if not (typetbl[glob] or typetbl["MUST_"..glob]) then + table.insert(undeclared, glob) + end + end + if #undeclared == 1 then + return nil, "Unknown variable: "..undeclared[1] + elseif #undeclared > 1 then + return nil, "Unknown variables: "..table.concat(undeclared, ", ") + end + return true +end + +return type_check diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/upload/api.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/upload/api.lua new file mode 100644 index 000000000..df57fc802 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/upload/api.lua @@ -0,0 +1,267 @@ + +local api = {} + +local cfg = require("luarocks.core.cfg") +local fs = require("luarocks.fs") +local dir = require("luarocks.dir") +local util = require("luarocks.util") +local persist = require("luarocks.persist") +local multipart = require("luarocks.upload.multipart") + +local Api = {} + +local function upload_config_file() + if not cfg.config_files.user.file then + return nil + end + return (cfg.config_files.user.file:gsub("/[^/]+$", "/upload_config.lua")) +end + +function Api:load_config() + local upload_conf = upload_config_file() + if not upload_conf then return nil end + local config, err = persist.load_into_table(upload_conf) + return config +end + +function Api:save_config() + -- Test configuration before saving it. + local res, err = self:raw_method("status") + if not res then + return nil, err + end + if res.errors then + util.printerr("Server says: " .. tostring(res.errors[1])) + return + end + local upload_conf = upload_config_file() + if not upload_conf then return nil end + local ok, err = fs.make_dir(dir.dir_name(upload_conf)) + if not ok then + return nil, err + end + persist.save_from_table(upload_conf, self.config) + fs.set_permissions(upload_conf, "read", "user") +end + +function Api:check_version() + if not self._server_tool_version then + local tool_version = cfg.upload.tool_version + local res, err = self:request(tostring(self.config.server) .. "/api/tool_version", { + current = tool_version + }) + if not res then + return nil, err + end + if not res.version then + return nil, "failed to fetch tool version" + end + self._server_tool_version = res.version + if res.force_update then + return nil, "Your upload client is too out of date to continue, please upgrade LuaRocks." + end + if res.version ~= tool_version then + util.warning("your LuaRocks is out of date, consider upgrading.") + end + end + return true +end + +function Api:method(...) + local res, err = self:raw_method(...) + if not res then + return nil, err + end + if res.errors then + if res.errors[1] == "Invalid key" then + return nil, res.errors[1] .. " (use the --api-key flag to change)" + end + local msg = table.concat(res.errors, ", ") + return nil, "API Failed: " .. msg + end + return res +end + +function Api:raw_method(path, ...) + self:check_version() + local url = tostring(self.config.server) .. "/api/" .. tostring(cfg.upload.api_version) .. "/" .. tostring(self.config.key) .. "/" .. tostring(path) + return self:request(url, ...) +end + +local function encode_query_string(t, sep) + if sep == nil then + sep = "&" + end + local i = 0 + local buf = { } + for k, v in pairs(t) do + if type(k) == "number" and type(v) == "table" then + k, v = v[1], v[2] + end + buf[i + 1] = multipart.url_escape(k) + buf[i + 2] = "=" + buf[i + 3] = multipart.url_escape(v) + buf[i + 4] = sep + i = i + 4 + end + buf[i] = nil + return table.concat(buf) +end + +local function redact_api_url(url) + url = tostring(url) + return (url:gsub(".*/api/[^/]+/[^/]+", "")) or "" +end + +local ltn12_ok, ltn12 = pcall(require, "ltn12") +if not ltn12_ok then -- If not using LuaSocket and/or LuaSec... + +function Api:request(url, params, post_params) + local vars = cfg.variables + local json_ok, json = util.require_json() + if not json_ok then return nil, "A JSON library is required for this command. "..json end + + if fs.which_tool("downloader") == "wget" then + local curl_ok, err = fs.is_tool_available(vars.CURL, "curl") + if not curl_ok then + return nil, err + end + end + + if not self.config.key then + return nil, "Must have API key before performing any actions." + end + if params and next(params) then + url = url .. ("?" .. encode_query_string(params)) + end + local method = "GET" + local out + local tmpfile = fs.tmpname() + if post_params then + method = "POST" + local curl_cmd = vars.CURL.." -f -k -L --silent --user-agent \""..cfg.user_agent.." via curl\" " + for k,v in pairs(post_params) do + local var = v + if type(v) == "table" then + var = "@"..v.fname + end + curl_cmd = curl_cmd .. "--form \""..k.."="..var.."\" " + end + if cfg.connection_timeout and cfg.connection_timeout > 0 then + curl_cmd = curl_cmd .. "--connect-timeout "..tonumber(cfg.connection_timeout).." " + end + local ok = fs.execute_string(curl_cmd..fs.Q(url).." -o "..fs.Q(tmpfile)) + if not ok then + return nil, "API failure: " .. redact_api_url(url) + end + else + local ok, err = fs.download(url, tmpfile) + if not ok then + return nil, "API failure: " .. tostring(err) .. " - " .. redact_api_url(url) + end + end + + local tmpfd = io.open(tmpfile) + if not tmpfd then + os.remove(tmpfile) + return nil, "API failure reading temporary file - " .. redact_api_url(url) + end + out = tmpfd:read("*a") + tmpfd:close() + os.remove(tmpfile) + + if self.debug then + util.printout("[" .. tostring(method) .. " via curl] " .. redact_api_url(url) .. " ... ") + end + + return json.decode(out) +end + +else -- use LuaSocket and LuaSec + +local warned_luasec = false + +function Api:request(url, params, post_params) + local json_ok, json = util.require_json() + if not json_ok then return nil, "A JSON library is required for this command. "..json end + local server = tostring(self.config.server) + local http_ok, http + local via = "luasocket" + if server:match("^https://") then + http_ok, http = pcall(require, "ssl.https") + if http_ok then + via = "luasec" + else + if not warned_luasec then + util.printerr("LuaSec is not available; using plain HTTP. Install 'luasec' to enable HTTPS.") + warned_luasec = true + end + http_ok, http = pcall(require, "socket.http") + url = url:gsub("^https", "http") + via = "luasocket" + end + else + http_ok, http = pcall(require, "socket.http") + end + if not http_ok then + return nil, "Failed loading socket library!" + end + + if not self.config.key then + return nil, "Must have API key before performing any actions." + end + local body + local headers = {} + if params and next(params) then + url = url .. ("?" .. encode_query_string(params)) + end + if post_params then + local boundary + body, boundary = multipart.encode(post_params) + headers["Content-length"] = #body + headers["Content-type"] = "multipart/form-data; boundary=" .. tostring(boundary) + end + local method = post_params and "POST" or "GET" + if self.debug then + util.printout("[" .. tostring(method) .. " via "..via.."] " .. redact_api_url(url) .. " ... ") + end + local out = {} + local _, status = http.request({ + url = url, + headers = headers, + method = method, + sink = ltn12.sink.table(out), + source = body and ltn12.source.string(body) + }) + if self.debug then + util.printout(tostring(status)) + end + local pok, ret, err = pcall(json.decode, table.concat(out)) + if pok and ret then + return ret + end + return nil, "API returned " .. tostring(status) .. " - " .. redact_api_url(url) +end + +end + +function api.new(args) + local self = {} + setmetatable(self, { __index = Api }) + self.config = self:load_config() or {} + self.config.server = args.server or self.config.server or cfg.upload.server + self.config.version = self.config.version or cfg.upload.version + self.config.key = args.temp_key or args.api_key or self.config.key + self.debug = args.debug + if not self.config.key then + return nil, "You need an API key to upload rocks.\n" .. + "Navigate to "..self.config.server.."/settings to get a key\n" .. + "and then pass it through the --api-key= flag." + end + if args.api_key then + self:save_config() + end + return self +end + +return api diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/upload/multipart.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/upload/multipart.lua new file mode 100644 index 000000000..56ae873e6 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/upload/multipart.lua @@ -0,0 +1,109 @@ + +local multipart = {} + +local File = {} + +local unpack = unpack or table.unpack + +-- socket.url.escape(s) from LuaSocket 3.0rc1 +function multipart.url_escape(s) + return (string.gsub(s, "([^A-Za-z0-9_])", function(c) + return string.format("%%%02x", string.byte(c)) + end)) +end + +function File:mime() + if not self.mimetype then + local mimetypes_ok, mimetypes = pcall(require, "mimetypes") + if mimetypes_ok then + self.mimetype = mimetypes.guess(self.fname) + end + self.mimetype = self.mimetype or "application/octet-stream" + end + return self.mimetype +end + +function File:content() + local fd = io.open(self.fname, "rb") + if not fd then + return nil, "Failed to open file: "..self.fname + end + local data = fd:read("*a") + fd:close() + return data +end + +local function rand_string(len) + local shuffled = {} + for i = 1, len do + local r = math.random(97, 122) + if math.random() >= 0.5 then + r = r - 32 + end + shuffled[i] = r + end + return string.char(unpack(shuffled)) +end + +-- multipart encodes params +-- returns encoded string,boundary +-- params is an a table of tuple tables: +-- params = { +-- {key1, value2}, +-- {key2, value2}, +-- key3: value3 +-- } +function multipart.encode(params) + local tuples = { } + for i = 1, #params do + tuples[i] = params[i] + end + for k,v in pairs(params) do + if type(k) == "string" then + table.insert(tuples, {k, v}) + end + end + local chunks = {} + for _, tuple in ipairs(tuples) do + local k,v = unpack(tuple) + k = multipart.url_escape(k) + local buffer = { 'Content-Disposition: form-data; name="' .. k .. '"' } + local content + if type(v) == "table" and v.__class == File then + buffer[1] = buffer[1] .. ('; filename="' .. v.fname:gsub(".*/", "") .. '"') + table.insert(buffer, "Content-type: " .. v:mime()) + content = v:content() + else + content = v + end + table.insert(buffer, "") + table.insert(buffer, content) + table.insert(chunks, table.concat(buffer, "\r\n")) + end + local boundary + while not boundary do + boundary = "Boundary" .. rand_string(16) + for _, chunk in ipairs(chunks) do + if chunk:find(boundary) then + boundary = nil + break + end + end + end + local inner = "\r\n--" .. boundary .. "\r\n" + return table.concat({ "--", boundary, "\r\n", + table.concat(chunks, inner), + "\r\n", "--", boundary, "--", "\r\n" }), boundary +end + +function multipart.new_file(fname, mime) + local self = {} + setmetatable(self, { __index = File }) + self.__class = File + self.fname = fname + self.mimetype = mime + return self +end + +return multipart + diff --git a/lib/luarocks/internal/luarocks/src/src/luarocks/util.lua b/lib/luarocks/internal/luarocks/src/src/luarocks/util.lua new file mode 100644 index 000000000..e0e57966e --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/src/luarocks/util.lua @@ -0,0 +1,682 @@ + +--- Assorted utilities for managing tables, plus a scheduler for rollback functions. +-- Does not requires modules directly (only as locals +-- inside specific functions) to avoid interdependencies, +-- as this is used in the bootstrapping stage of luarocks.core.cfg. + +local util = {} + +local core = require("luarocks.core.util") + +util.cleanup_path = core.cleanup_path +util.split_string = core.split_string +util.sortedpairs = core.sortedpairs +util.deep_merge = core.deep_merge +util.deep_merge_under = core.deep_merge_under +util.popen_read = core.popen_read +util.show_table = core.show_table +util.printerr = core.printerr +util.warning = core.warning +util.keys = core.keys + +local unpack = unpack or table.unpack +local pack = table.pack or function(...) return { n = select("#", ...), ... } end + +local scheduled_functions = {} + +--- Schedule a function to be executed upon program termination. +-- This is useful for actions such as deleting temporary directories +-- or failure rollbacks. +-- @param f function: Function to be executed. +-- @param ... arguments to be passed to function. +-- @return table: A token representing the scheduled execution, +-- which can be used to remove the item later from the list. +function util.schedule_function(f, ...) + assert(type(f) == "function") + + local item = { fn = f, args = pack(...) } + table.insert(scheduled_functions, item) + return item +end + +--- Unschedule a function. +-- This is useful for cancelling a rollback of a completed operation. +-- @param item table: The token representing the scheduled function that was +-- returned from the schedule_function call. +function util.remove_scheduled_function(item) + for k, v in pairs(scheduled_functions) do + if v == item then + table.remove(scheduled_functions, k) + return + end + end +end + +--- Execute scheduled functions. +-- Some calls create temporary files and/or directories and register +-- corresponding cleanup functions. Calling this function will run +-- these function, erasing temporaries. +-- Functions are executed in the inverse order they were scheduled. +function util.run_scheduled_functions() + local fs = require("luarocks.fs") + if fs.change_dir_to_root then + fs.change_dir_to_root() + end + for i = #scheduled_functions, 1, -1 do + local item = scheduled_functions[i] + item.fn(unpack(item.args, 1, item.args.n)) + end +end + +--- Produce a Lua pattern that matches precisely the given string +-- (this is suitable to be concatenating to other patterns, +-- so it does not include beginning- and end-of-string markers (^$) +-- @param s string: The input string +-- @return string: The equivalent pattern +function util.matchquote(s) + return (s:gsub("[?%-+*%[%].%%()$^]","%%%1")) +end + +local var_format_pattern = "%$%((%a[%a%d_]+)%)" + +-- Check if a set of needed variables are referenced +-- somewhere in a list of definitions, warning the user +-- about any unused ones. Each key in needed_set should +-- appear as a $(XYZ) variable at least once as a +-- substring of some value of var_defs. +-- @param var_defs: a table with string keys and string +-- values, containing variable definitions. +-- @param needed_set: a set where keys are the names of +-- needed variables. +-- @param msg string: the warning message to display. +function util.warn_if_not_used(var_defs, needed_set, msg) + local seen = {} + for _, val in pairs(var_defs) do + for used in val:gmatch(var_format_pattern) do + seen[used] = true + end + end + for var, _ in pairs(needed_set) do + if not seen[var] then + util.warning(msg:format(var)) + end + end +end + +-- Output any entries that might remain in $(XYZ) format, +-- warning the user that substitutions have failed. +-- @param line string: the input string +local function warn_failed_matches(line) + local any_failed = false + if line:match(var_format_pattern) then + for unmatched in line:gmatch(var_format_pattern) do + util.warning("unmatched variable " .. unmatched) + any_failed = true + end + end + return any_failed +end + +--- Perform make-style variable substitutions on string values of a table. +-- For every string value tbl.x which contains a substring of the format +-- "$(XYZ)" will have this substring replaced by vars["XYZ"], if that field +-- exists in vars. Only string values are processed; this function +-- does not scan subtables recursively. +-- @param tbl table: Table to have its string values modified. +-- @param vars table: Table containing string-string key-value pairs +-- representing variables to replace in the strings values of tbl. +function util.variable_substitutions(tbl, vars) + assert(type(tbl) == "table") + assert(type(vars) == "table") + + local updated = {} + for k, v in pairs(tbl) do + if type(v) == "string" then + updated[k] = v:gsub(var_format_pattern, vars) + if warn_failed_matches(updated[k]) then + updated[k] = updated[k]:gsub(var_format_pattern, "") + end + end + end + for k, v in pairs(updated) do + tbl[k] = v + end +end + +function util.lua_versions(sort) + local versions = { "5.1", "5.2", "5.3", "5.4" } + local i = 0 + if sort == "descending" then + i = #versions + 1 + return function() + i = i - 1 + return versions[i] + end + else + return function() + i = i + 1 + return versions[i] + end + end +end + +function util.lua_path_variables() + local cfg = require("luarocks.core.cfg") + local lpath_var = "LUA_PATH" + local lcpath_var = "LUA_CPATH" + + local lv = cfg.lua_version:gsub("%.", "_") + if lv ~= "5_1" then + if os.getenv("LUA_PATH_" .. lv) then + lpath_var = "LUA_PATH_" .. lv + end + if os.getenv("LUA_CPATH_" .. lv) then + lcpath_var = "LUA_CPATH_" .. lv + end + end + return lpath_var, lcpath_var +end + +function util.starts_with(s, prefix) + return s:sub(1,#prefix) == prefix +end + +--- Print a line to standard output +function util.printout(...) + io.stdout:write(table.concat({...},"\t")) + io.stdout:write("\n") +end + +function util.title(msg, porcelain, underline) + if porcelain then return end + util.printout() + util.printout(msg) + util.printout((underline or "-"):rep(#msg)) + util.printout() +end + +function util.this_program(default) + local i = 1 + local last, cur = default, default + while i do + local dbg = debug and debug.getinfo(i,"S") + if not dbg then break end + last = cur + cur = dbg.source + i=i+1 + end + local prog = last:sub(1,1) == "@" and last:sub(2) or last + + -- Check if we found the true path of a script that has a wrapper + local lrdir, binpath = prog:match("^(.*)/lib/luarocks/rocks%-[0-9.]*/[^/]+/[^/]+(/bin/[^/]+)$") + if lrdir then + -- Return the wrapper instead + return lrdir .. binpath + end + + return prog +end + +function util.format_rock_name(name, namespace, version) + return (namespace and namespace.."/" or "")..name..(version and " "..version or "") +end + +function util.deps_mode_option(parser, program) + local cfg = require("luarocks.core.cfg") + + parser:option("--deps-mode", "How to handle dependencies. Four modes are supported:\n".. + "* all - use all trees from the rocks_trees list for finding dependencies\n".. + "* one - use only the current tree (possibly set with --tree)\n".. + "* order - use trees based on order (use the current tree and all ".. + "trees below it on the rocks_trees list)\n".. + "* none - ignore dependencies altogether.\n".. + "The default mode may be set with the deps_mode entry in the configuration file.\n".. + 'The current default is "'..cfg.deps_mode..'".\n'.. + "Type '"..util.this_program(program or "luarocks").."' with no ".. + "arguments to see your list of rocks trees.") + :argname("") + :choices({"all", "one", "order", "none"}) + parser:flag("--nodeps"):hidden(true) +end + +function util.see_help(command, program) + return "See '"..util.this_program(program or "luarocks")..' help'..(command and " "..command or "").."'." +end + +function util.see_also(text) + local see_also = "See also:\n" + if text then + see_also = see_also..text.."\n" + end + return see_also.." '"..util.this_program("luarocks").." help' for general options and configuration." +end + +function util.announce_install(rockspec) + local cfg = require("luarocks.core.cfg") + local path = require("luarocks.path") + + local suffix = "" + if rockspec.description and rockspec.description.license then + suffix = " (license: "..rockspec.description.license..")" + end + + util.printout(rockspec.name.." "..rockspec.version.." is now installed in "..path.root_dir(cfg.root_dir)..suffix) + util.printout() +end + +--- Collect rockspecs located in a subdirectory. +-- @param versions table: A table mapping rock names to newest rockspec versions. +-- @param paths table: A table mapping rock names to newest rockspec paths. +-- @param unnamed_paths table: An array of rockspec paths that don't contain rock +-- name and version in regular format. +-- @param subdir string: path to subdirectory. +local function collect_rockspecs(versions, paths, unnamed_paths, subdir) + local fs = require("luarocks.fs") + local dir = require("luarocks.dir") + local path = require("luarocks.path") + local vers = require("luarocks.core.vers") + + if fs.is_dir(subdir) then + for file in fs.dir(subdir) do + file = dir.path(subdir, file) + + if file:match("rockspec$") and fs.is_file(file) then + local rock, version = path.parse_name(file) + + if rock then + if not versions[rock] or vers.compare_versions(version, versions[rock]) then + versions[rock] = version + paths[rock] = file + end + else + table.insert(unnamed_paths, file) + end + end + end + end +end + +--- Get default rockspec name for commands that take optional rockspec name. +-- @return string or (nil, string): path to the rockspec or nil and error message. +function util.get_default_rockspec() + local versions, paths, unnamed_paths = {}, {}, {} + -- Look for rockspecs in some common locations. + collect_rockspecs(versions, paths, unnamed_paths, ".") + collect_rockspecs(versions, paths, unnamed_paths, "rockspec") + collect_rockspecs(versions, paths, unnamed_paths, "rockspecs") + + if #unnamed_paths > 0 then + -- There are rockspecs not following "name-version.rockspec" format. + -- More than one are ambiguous. + if #unnamed_paths > 1 then + return nil, "Please specify which rockspec file to use." + else + return unnamed_paths[1] + end + else + local fs = require("luarocks.fs") + local dir = require("luarocks.dir") + local basename = dir.base_name(fs.current_dir()) + + if paths[basename] then + return paths[basename] + end + + local rock = next(versions) + + if rock then + -- If there are rockspecs for multiple rocks it's ambiguous. + if next(versions, rock) then + return nil, "Please specify which rockspec file to use." + else + return paths[rock] + end + else + return nil, "Argument missing: please specify a rockspec to use on current directory." + end + end +end + +-- Quote Lua string, analogous to fs.Q. +-- @param s A string, such as "hello" +-- @return string: A quoted string, such as '"hello"' +function util.LQ(s) + return ("%q"):format(s) +end + +-- Split name and namespace of a package name. +-- @param ns_name a name that may be in "namespace/name" format +-- @return string, string? - name and optionally a namespace +function util.split_namespace(ns_name) + local p1, p2 = ns_name:match("^([^/]+)/([^/]+)$") + if p1 then + return p2, p1 + end + return ns_name +end + +--- Argparse action callback for namespaced rock arguments. +function util.namespaced_name_action(args, target, ns_name) + assert(type(args) == "table") + assert(type(target) == "string") + assert(type(ns_name) == "string" or not ns_name) + + if not ns_name then + return + end + + if ns_name:match("%.rockspec$") or ns_name:match("%.rock$") then + args[target] = ns_name + else + local name, namespace = util.split_namespace(ns_name) + args[target] = name:lower() + if namespace then + args.namespace = namespace:lower() + end + end +end + +function util.deep_copy(tbl) + local copy = {} + for k, v in pairs(tbl) do + if type(v) == "table" then + copy[k] = util.deep_copy(v) + else + copy[k] = v + end + end + return copy +end + +-- An ode to the multitude of JSON libraries out there... +function util.require_json() + local list = { "cjson", "dkjson", "json" } + for _, lib in ipairs(list) do + local json_ok, json = pcall(require, lib) + if json_ok then + pcall(json.use_lpeg) -- optional feature in dkjson + return json_ok, json + end + end + local errmsg = "Failed loading " + for i, name in ipairs(list) do + if i == #list then + errmsg = errmsg .."and '"..name.."'. Use 'luarocks search ' to search for a library and 'luarocks install ' to install one." + else + errmsg = errmsg .."'"..name.."', " + end + end + return nil, errmsg +end + +-- A portable version of fs.exists that can be used at early startup, +-- before the platform has been determined and luarocks.fs has been +-- initialized. +function util.exists(file) + local fd, _, code = io.open(file, "r") + if code == 13 then + -- code 13 means "Permission denied" on both Unix and Windows + -- io.open on folders always fails with code 13 on Windows + return true + end + if fd then + fd:close() + return true + end + return false +end + +do + local function Q(pathname) + if pathname:match("^.:") then + return pathname:sub(1, 2) .. '"' .. pathname:sub(3) .. '"' + end + return '"' .. pathname .. '"' + end + + function util.check_lua_version(lua_exe, luaver) + if not util.exists(lua_exe) then + return nil + end + + local cmd = 'io.write(_VERSION:sub(5))' + -- Tarantool falls into the interactive mode if no script is + -- given even when -e option is passed. + if lua_exe:find('tarantool') then + cmd = cmd .. ' os.exit()' + end + local lv, err = util.popen_read(Q(lua_exe) .. (' -e "%s"'):format(cmd)) + if lv == "" then + return nil + end + if luaver and luaver ~= lv then + return nil + end + return lv + end + + function util.get_luajit_version() + local cfg = require("luarocks.core.cfg") + if cfg.cache.luajit_version_checked then + return cfg.cache.luajit_version + end + cfg.cache.luajit_version_checked = true + + local ljv + local dir = require("luarocks.dir") + if cfg.lua_version == "5.1" then + local interp = dir.path(cfg.variables["LUA_BINDIR"], cfg.lua_interpreter) + local cmd = 'io.write(tostring(jit and jit.version:sub(8)))' + -- Exit from the iteractive mode. See the comment above. + if interp:find('tarantool') then + cmd = cmd .. ' os.exit()' + end + ljv = util.popen_read(Q(interp) .. (' -e "%s"'):format(cmd)) + if ljv == "nil" then + ljv = nil + end + end + cfg.cache.luajit_version = ljv + return ljv + end + + local find_lua_bindir + do + local exe_suffix = (package.config:sub(1, 1) == "\\" and ".exe" or "") + + local function insert_lua_variants(names, luaver) + local variants = { + "lua" .. luaver .. exe_suffix, + "lua" .. luaver:gsub("%.", "") .. exe_suffix, + "lua-" .. luaver .. exe_suffix, + "lua-" .. luaver:gsub("%.", "") .. exe_suffix, + } + for _, name in ipairs(variants) do + names[name] = luaver + table.insert(names, name) + end + end + + find_lua_bindir = function(prefix, luaver, verbose) + local names = {} + if luaver then + insert_lua_variants(names, luaver) + else + for v in util.lua_versions("descending") do + insert_lua_variants(names, v) + end + end + if luaver == "5.1" or not luaver then + table.insert(names, "luajit" .. exe_suffix) + table.insert(names, "tarantool") + end + table.insert(names, "lua" .. exe_suffix) + + local tried = {} + local dir_sep = package.config:sub(1, 1) + for _, d in ipairs({ prefix .. dir_sep .. "bin", prefix }) do + for _, name in ipairs(names) do + local lua_exe = d .. dir_sep .. name + local is_wrapper, err = util.lua_is_wrapper(lua_exe) + if is_wrapper == false then + local lv = util.check_lua_version(lua_exe, luaver) + if lv then + return name, d, lv + end + elseif is_wrapper == true or err == nil then + table.insert(tried, lua_exe) + else + table.insert(tried, string.format("%-13s (%s)", lua_exe, err)) + end + end + end + local interp = luaver + and ("Lua " .. luaver .. " interpreter") + or "Lua interpreter" + return nil, interp .. " not found at " .. prefix .. "\n" .. + (verbose and "Tried:\t" .. table.concat(tried, "\n\t") or "") + end + end + + function util.find_lua(prefix, luaver, verbose) + local lua_interpreter, bindir + lua_interpreter, bindir, luaver = find_lua_bindir(prefix, luaver, verbose) + if not lua_interpreter then + return nil, bindir + end + + return { + lua_version = luaver, + lua_interpreter = lua_interpreter, + lua_dir = prefix, + lua_bindir = bindir, + } + end +end + +function util.lua_is_wrapper(interp) + local fd, err = io.open(interp, "r") + if not fd then + return nil, err + end + local data, err = fd:read(1000) + fd:close() + if not data then + return nil, err + end + return not not data:match("LUAROCKS_SYSCONFDIR") +end + +function util.opts_table(type_name, valid_opts) + local opts_mt = {} + + opts_mt.__index = opts_mt + + function opts_mt.type() + return type_name + end + + return function(opts) + for k, v in pairs(opts) do + local tv = type(v) + if not valid_opts[k] then + error("invalid option: "..k) + end + local vo, optional = valid_opts[k]:match("^(.-)(%??)$") + if not (tv == vo or (optional == "?" and tv == nil)) then + error("invalid type option: "..k.." - got "..tv..", expected "..vo) + end + end + for k, v in pairs(valid_opts) do + if (not v:find("?", 1, true)) and opts[k] == nil then + error("missing option: "..k) + end + end + return setmetatable(opts, opts_mt) + end +end + +--- Return a table of modules that are already provided by the VM, which +-- can be specified as dependencies without having to install an actual rock. +-- @param rockspec (optional) a rockspec table, so that rockspec format +-- version compatibility can be checked. If not given, maximum compatibility +-- is assumed. +-- @return a table with rock names as keys and versions and values, +-- specifying modules that are already provided by the VM (including +-- "lua" for the Lua version and, for format 3.0+, "luajit" if detected). +function util.get_rocks_provided(rockspec) + local cfg = require("luarocks.core.cfg") + + if not rockspec and cfg.cache.rocks_provided then + return cfg.cache.rocks_provided + end + + local rocks_provided = {} + + local lv = cfg.lua_version + + rocks_provided["lua"] = lv.."-1" + + if lv == "5.2" then + rocks_provided["bit32"] = lv.."-1" + end + + if lv == "5.3" or lv == "5.4" then + rocks_provided["utf8"] = lv.."-1" + end + + if lv == "5.1" then + local ljv = util.get_luajit_version() + if ljv then + rocks_provided["luabitop"] = ljv.."-1" + if (not rockspec) or rockspec:format_is_at_least("3.0") then + rocks_provided["luajit"] = ljv.."-1" + end + end + end + + local tarantool_ver_raw = rawget(_G, '_TARANTOOL') + if tarantool_ver_raw then + -- Tarantool + local tarantool_version = tarantool_ver_raw:match("([^-]+)-") + rocks_provided["tarantool"] = tarantool_version.."-1" + end + + -- If luarocks is launched from tt, it cannot directly access + -- the _TARANTOOL variable. + local tarantool_ver_env = os.getenv("TT_CLI_TARANTOOL_VERSION") + if tarantool_ver_env ~= nil then + local tarantool_version = tarantool_ver_env:match("([^-]+)-") + if tarantool_version ~= nil then + rocks_provided["tarantool"] = tarantool_version.."-1" + end + end + + if cfg.rocks_provided then + util.deep_merge_under(rocks_provided, cfg.rocks_provided) + end + + if not rockspec then + cfg.cache.rocks_provided = rocks_provided + end + + return rocks_provided +end + +function util.remove_doc_dir(name, version) + local path = require("luarocks.path") + local fs = require("luarocks.fs") + local dir = require("luarocks.dir") + + local install_dir = path.install_dir(name, version) + for _, f in ipairs(fs.list_dir(install_dir)) do + local doc_dirs = { "doc", "docs" } + for _, d in ipairs(doc_dirs) do + if f == d then + fs.delete(dir.path(install_dir, f)) + end + end + end +end + +return util diff --git a/lib/luarocks/internal/luarocks/src/test_regression.sh b/lib/luarocks/internal/luarocks/src/test_regression.sh new file mode 100755 index 000000000..4780893f3 --- /dev/null +++ b/lib/luarocks/internal/luarocks/src/test_regression.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash + +# ## Usage examples: +# +# Test current branch against master: +# test_regression.sh +# Test current branch against another branch: +# test_regression.sh another-branch +# Test current branch against master passing arguments to busted: +# test_regression.sh -- --exclude-tags=flaky +# Test current branch against next passing arguments to busted: +# test_regression.sh next -- --exclude-tags=flaky + +if [ $(git status --untracked-files=no --porcelain | wc -l) != "0" ] +then + echo "==============================" + echo "Local tree is not clean, please commit or stash before running this." + echo "==============================" + exit 1 +fi + +newbranch=$(git rev-parse --abbrev-ref HEAD) +if [ "$newbranch" = "master" ] || [ "$newbranch" = "next" ] +then + echo "==============================" + echo "Please run this from a topic branch, and not from 'next' or 'master'." + echo "==============================" + exit 1 +fi + +basebranch="$1" +if [ "$1" == "" ] || [ "$1" == "--" ] +then + basebranch=master +else + basebranch="$1" + shift +fi +if [ "$1" == "--" ] +then + shift +fi + +git checkout "$newbranch" +rm -rf .spec-new +rm -rf .spec-old +cp -a spec .spec-new +git checkout "$basebranch" + +echo "----------------------------------------" +echo "Tests changed between $basebranch and $newbranch:" +echo "----------------------------------------" +specfiles=($(git diff-tree --no-commit-id --name-only -r "..$newbranch" | grep "^spec/")) +echo "${specfiles[@]}" +echo "----------------------------------------" + +mv spec .spec-old +mv .spec-new spec +./luarocks test -- "${specfiles[@]}" "$@" +if [ $? = 0 ] +then + git checkout . + git checkout $newbranch + echo "==============================" + echo "Regression test does not trigger the issue in base branch" + echo "==============================" + exit 1 +fi +mv spec .spec-new +mv .spec-old spec +git checkout $newbranch +./luarocks test -- "${specfiles[@]}" "$@" +ret=$? +if [ "$ret" != "0" ] +then + echo "==============================" + echo "New branch does not fix issue (returned $ret)" + echo "==============================" + exit 1 +fi + +echo "==============================" +echo "All good! New branch fixes regression!" +echo "==============================" +exit 0 + diff --git a/lib/luarocks/manif/parser.go b/lib/luarocks/manif/parser.go new file mode 100644 index 000000000..7ffc7a386 --- /dev/null +++ b/lib/luarocks/manif/parser.go @@ -0,0 +1,711 @@ +package manif + +import ( + "errors" + "fmt" + "slices" + "sort" + "strconv" + "strings" +) + +// Byte values for the C-style escape sequences upstream persist.lua may emit. +const ( + escBell = 0x07 // \a + escBackspace = 0x08 // \b + escFormFeed = 0x0C // \f + escLineFeed = 0x0A // \n + escCarriage = 0x0D // \r + escTab = 0x09 // \t + escVTab = 0x0B // \v +) + +const ( + // decimalBase is the radix for parsing decimal escape sequences. + decimalBase = 10 + + // maxByteValue is the largest value a single byte can hold; decimal + // escapes above it are out of range. + maxByteValue = 255 + + // hexDigitOffset maps a hex letter (a-f / A-F) to its numeric value + // after subtracting the letter's base: 'a'-'a'+hexDigitOffset == 10. + hexDigitOffset = 10 +) + +// Parse reads a serialized manifest (assignments mode) and returns a tree +// of native Go values: +// +// - tables → map[string]any (also for tables that mix string and +// numeric keys; numeric keys are stringified) +// - pure numeric → []any when keys are exactly 1..N and dense +// arrays +// - Lua strings → string +// - Lua numbers → int64 when integral, else float64 +// - true / false → bool +// +// A bare nil literal in value position is a hard error, not a dropped entry — +// upstream persist.lua never emits one, so Parse never has to represent it. +// +// Parse intentionally supports only the subset emitted by upstream +// persist.lua: table constructors, string literals (short and long form), +// integer and decimal numbers (with optional leading minus), boolean +// literals, identifier or quoted/bracketed keys, and nested tables. +// +// Anything outside that subset — function literals, control-flow keywords +// other than the boolean literals, expressions, the concat operator, etc. +// — is a hard error: the parser never silently drops input. Every such +// error wraps [ErrParse]. +func Parse(b []byte) (any, error) { + p := &parser{src: b} + p.skipWS() + // Top-level form: a sequence of `key = value` assignments. We + // accumulate into a map[string]any. End-of-input when nothing but + // whitespace remains. + out := map[string]any{} + + for p.pos < len(p.src) { + key, err := p.parseTopLevelKey() + if err != nil { + return nil, err + } + + p.skipWS() + + if !p.consume('=') { + return nil, p.errorf("expected '=' after key %q", key) + } + + p.skipWS() + + v, err := p.parseValue() + if err != nil { + return nil, err + } + + out[key] = v + + p.skipWS() + } + + return out, nil +} + +type parser struct { + src []byte + pos int +} + +func (p *parser) errorf(format string, args ...any) error { + line, col := p.lineCol() + + return fmt.Errorf("manif.Parse: line %d col %d: %s: %w", line, col, fmt.Sprintf(format, args...), ErrParse) +} + +func (p *parser) lineCol() (int, int) { + line, col := 1, 1 + + for i := 0; i < p.pos && i < len(p.src); i++ { + if p.src[i] == '\n' { + line++ + col = 1 + } else { + col++ + } + } + + return line, col +} + +func (p *parser) skipWS() { + for p.pos < len(p.src) { + c := p.src[p.pos] + + switch { + case c == ' ' || c == '\t' || c == '\n' || c == '\r': + p.pos++ + case c == '-' && p.pos+1 < len(p.src) && p.src[p.pos+1] == '-': + // Persist never emits comments; but the format does support + // them when humans hand-edit a manifest. Single-line only — + // long comments are not in the persist surface and would + // signal something unusual. + if p.pos+3 < len(p.src) && p.src[p.pos+2] == '[' && (p.src[p.pos+3] == '[' || p.src[p.pos+3] == '=') { + // We don't accept long comments — that's outside the + // persist-emitted subset. + return + } + + p.pos += 2 + for p.pos < len(p.src) && p.src[p.pos] != '\n' { + p.pos++ + } + default: + return + } + } +} + +func (p *parser) peek() byte { + if p.pos >= len(p.src) { + return 0 + } + + return p.src[p.pos] +} + +func (p *parser) consume(c byte) bool { + if p.pos < len(p.src) && p.src[p.pos] == c { + p.pos++ + + return true + } + + return false +} + +func (p *parser) parseTopLevelKey() (string, error) { + if !isIdentStart(p.peek()) { + return "", p.errorf("expected identifier at start of top-level assignment, got %q", string(p.peek())) + } + + return p.parseIdent(), nil +} + +func isIdentStart(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_' +} + +func isIdentPart(c byte) bool { + return isIdentStart(c) || (c >= '0' && c <= '9') +} + +func (p *parser) parseIdent() string { + start := p.pos + + for p.pos < len(p.src) && isIdentPart(p.src[p.pos]) { + p.pos++ + } + + return string(p.src[start:p.pos]) +} + +// parseValue dispatches on the next non-whitespace byte. +func (p *parser) parseValue() (any, error) { + p.skipWS() + + if p.pos >= len(p.src) { + return nil, p.errorf("unexpected end of input — expected value") + } + + c := p.src[p.pos] + + switch { + case c == '{': + return p.parseTable() + case c == '"' || c == '\'': + return p.parseShortString() + case c == '[': + // long bracket string + s, ok, err := p.tryParseLongBracket() + if err != nil { + return nil, err + } + + if ok { + return s, nil + } + + return nil, p.errorf("unexpected '[' (expected long-bracket string)") + case c == '-' || (c >= '0' && c <= '9'): + return p.parseNumber() + case isIdentStart(c): + start := p.pos + + ident := p.parseIdent() + switch ident { + case "true": + return true, nil + case "false": + return false, nil + case "nil": + // persist never emits a bare nil literal; treat it as a parse + // error rather than returning an ambiguous (nil, nil). + return nil, p.errorf("unexpected nil literal in value position") + } + // Anything else — function, control-flow, or an expression + // involving a name — is outside the persist subset. + p.pos = start + + return nil, p.errorf("unsupported identifier %q in value position (only true/false/nil are accepted)", ident) + default: + return nil, p.errorf("unexpected byte %q in value position", string(c)) + } +} + +func (p *parser) parseShortString() (string, error) { + quote := p.src[p.pos] + p.pos++ // consume quote + + var b strings.Builder + + for p.pos < len(p.src) { + c := p.src[p.pos] + if c == quote { + p.pos++ + + return b.String(), nil + } + + if c == '\n' { + return "", p.errorf("unterminated short string (newline before closing quote)") + } + + if c != '\\' { + b.WriteByte(c) + + p.pos++ + + continue + } + // escape + p.pos++ + if p.pos >= len(p.src) { + return "", p.errorf("dangling backslash at end of input") + } + + esc := p.src[p.pos] + switch esc { + case 'a': + b.WriteByte(escBell) + + p.pos++ + case 'b': + b.WriteByte(escBackspace) + + p.pos++ + case 'f': + b.WriteByte(escFormFeed) + + p.pos++ + case 'n': + b.WriteByte(escLineFeed) + + p.pos++ + case 'r': + b.WriteByte(escCarriage) + + p.pos++ + case 't': + b.WriteByte(escTab) + + p.pos++ + case 'v': + b.WriteByte(escVTab) + + p.pos++ + case '\\', '"', '\'': + b.WriteByte(esc) + + p.pos++ + case '\n': + b.WriteByte('\n') + + p.pos++ + case 'x': + // \xHH — Lua 5.2+ syntax. Persist emits decimal, so this + // is only relevant if a human hand-edited the file. + if p.pos+2 >= len(p.src) { + return "", p.errorf(`\x escape requires two hex digits`) + } + + h1, ok1 := hexDigit(p.src[p.pos+1]) + + h2, ok2 := hexDigit(p.src[p.pos+2]) + + if !ok1 || !ok2 { + return "", p.errorf(`\x escape requires two hex digits`) + } + + // h1,h2 are single hex digits (0-15) so h1*16+h2 is 0-255. + b.WriteByte(byte(h1*16 + h2)) + + p.pos += 3 + default: + if esc >= '0' && esc <= '9' { + // Up to three decimal digits. + n := 0 + + digits := 0 + + for digits < 3 && p.pos < len(p.src) && p.src[p.pos] >= '0' && p.src[p.pos] <= '9' { + n = n*decimalBase + int(p.src[p.pos]-'0') + p.pos++ + digits++ + } + + if n > maxByteValue { + return "", p.errorf("decimal escape out of range: %d", n) + } + + b.WriteByte(byte(n)) + + continue + } + + return "", p.errorf("unknown escape sequence \\%s", string(esc)) + } + } + + return "", p.errorf("unterminated short string") +} + +func hexDigit(c byte) (int, bool) { + switch { + case c >= '0' && c <= '9': + return int(c - '0'), true + case c >= 'a' && c <= 'f': + return int(c-'a') + hexDigitOffset, true + case c >= 'A' && c <= 'F': + return int(c-'A') + hexDigitOffset, true + } + + return 0, false +} + +// tryParseLongBracket consumes `[=*[ ... ]=*]` if the current position looks +// like one. Returns (value, true, nil) on success, (_, false, nil) if the +// position does not start a long bracket, and (_, _, err) on malformed +// content. +func (p *parser) tryParseLongBracket() (string, bool, error) { + if p.peek() != '[' { + return "", false, nil + } + + save := p.pos + p.pos++ + + equals := 0 + for p.pos < len(p.src) && p.src[p.pos] == '=' { + equals++ + p.pos++ + } + + if p.pos >= len(p.src) || p.src[p.pos] != '[' { + // Not a long bracket — restore and let the caller deal. + p.pos = save + + return "", false, nil + } + + p.pos++ // consume opening '[' + + // Per Lua spec, an immediate newline after the opening bracket is + // ignored. Persist always writes such a newline. + if p.pos < len(p.src) && p.src[p.pos] == '\n' { + p.pos++ + } else if p.pos+1 < len(p.src) && p.src[p.pos] == '\r' && p.src[p.pos+1] == '\n' { + p.pos += 2 + } + + closeBracket := "]" + strings.Repeat("=", equals) + "]" + + end := strings.Index(string(p.src[p.pos:]), closeBracket) + if end < 0 { + return "", false, p.errorf("unterminated long-bracket string") + } + + val := string(p.src[p.pos : p.pos+end]) + p.pos += end + len(closeBracket) + + return val, true, nil +} + +// parseNumber accepts an optional leading '-' followed by decimal digits +// and an optional fractional part. No exponent support — persist emits +// numbers via tostring, which renders integers and small floats without an +// exponent. If we encounter scientific notation we still parse it via +// strconv to stay tolerant of hand-edits. +func (p *parser) parseNumber() (any, error) { + start := p.pos + + if p.src[p.pos] == '-' { + p.pos++ + } + + for p.pos < len(p.src) && p.src[p.pos] >= '0' && p.src[p.pos] <= '9' { + p.pos++ + } + + isFloat := false + if p.pos < len(p.src) && p.src[p.pos] == '.' { + isFloat = true + + p.pos++ + for p.pos < len(p.src) && p.src[p.pos] >= '0' && p.src[p.pos] <= '9' { + p.pos++ + } + } + + if p.pos < len(p.src) && (p.src[p.pos] == 'e' || p.src[p.pos] == 'E') { + isFloat = true + + p.pos++ + if p.pos < len(p.src) && (p.src[p.pos] == '+' || p.src[p.pos] == '-') { + p.pos++ + } + + for p.pos < len(p.src) && p.src[p.pos] >= '0' && p.src[p.pos] <= '9' { + p.pos++ + } + } + + lit := string(p.src[start:p.pos]) + if lit == "" || lit == "-" { + return nil, p.errorf("malformed number %q", lit) + } + + if !isFloat { + n, err := strconv.ParseInt(lit, 10, 64) + if err == nil { + return n, nil + } + // Falls through to float on overflow. + } + + f, err := strconv.ParseFloat(lit, 64) + if err != nil { + return nil, p.errorf("malformed number %q: %v", lit, err) + } + + return f, nil +} + +// parseTable handles both array-style and record-style entries. +// +// Output shape: +// - If every entry has an integer key 1..N (dense), returns []any. +// - Otherwise returns map[string]any with numeric keys stringified +// (matching the loose shape used by upstream manifest data). +func (p *parser) parseTable() (any, error) { + if !p.consume('{') { + return nil, p.errorf("expected '{' starting table") + } + + var entries []entry + + autoIdx := int64(1) + + for { + p.skipWS() + + if p.consume('}') { + break + } + + var ent entry + // Determine if this is a keyed entry by lookahead. + switch { + case p.peek() == '[': + // bracketed key: [expr] = value + p.pos++ + p.skipWS() + + key, err := p.parseValue() + if err != nil { + return nil, err + } + + p.skipWS() + + if !p.consume(']') { + return nil, p.errorf("expected ']' after bracketed key") + } + + p.skipWS() + + if !p.consume('=') { + return nil, p.errorf("expected '=' after bracketed key") + } + + p.skipWS() + + v, err := p.parseValue() + if err != nil { + return nil, err + } + + ent = entry{key: key, value: v} + case isIdentStart(p.peek()) && p.identKeyLookahead(): + name := p.parseIdent() + p.skipWS() + + if !p.consume('=') { + return nil, p.errorf("expected '=' after ident key %q", name) + } + + p.skipWS() + + v, err := p.parseValue() + if err != nil { + return nil, err + } + + ent = entry{key: name, value: v} + default: + v, err := p.parseValue() + if err != nil { + return nil, err + } + + ent = entry{key: autoIdx, value: v} + autoIdx++ + } + + entries = append(entries, ent) + + p.skipWS() + + if p.consume(',') || p.consume(';') { + continue + } + + p.skipWS() + + if p.consume('}') { + break + } + + return nil, p.errorf("expected ',' or '}' in table") + } + + return entriesToValue(entries), nil +} + +// identKeyLookahead returns true if the position starts with an identifier +// followed (after whitespace) by '='. Used to distinguish `name = value` +// from a value that happens to be a boolean / nil literal. +func (p *parser) identKeyLookahead() bool { + save := p.pos + + for p.pos < len(p.src) && isIdentPart(p.src[p.pos]) { + p.pos++ + } + // Skip whitespace but not newlines beyond — actually allow any WS. + for p.pos < len(p.src) { + c := p.src[p.pos] + if c == ' ' || c == '\t' || c == '\n' || c == '\r' { + p.pos++ + + continue + } + + break + } + + isAssign := p.pos < len(p.src) && p.src[p.pos] == '=' + // Distinguish '=' from '==' just in case (persist never emits '=='). + if isAssign && p.pos+1 < len(p.src) && p.src[p.pos+1] == '=' { + isAssign = false + } + + p.pos = save + + return isAssign +} + +// entriesToValue collapses the entry slice into one of: +// +// - []any — pure dense numeric (keys 1..N, no string keys) +// - map[string]any — pure string-keyed (no numeric keys) +// - *table — mixed numeric + string keys; preserves the +// distinction so the writer can pack numeric +// entries inline and emit string entries +// multi-line, matching persist.lua. +func entriesToValue(entries []entry) any { + hasNum := false + hasStr := false + denseArray := true + maxIdx := int64(0) + + for _, e := range entries { + switch k := e.key.(type) { + case int64: + hasNum = true + + if k != maxIdx+1 { + denseArray = false + } + + maxIdx++ + case string: + hasStr = true + denseArray = false + default: + _ = k + denseArray = false + } + } + + if hasNum && !hasStr && denseArray { + arr := make([]any, len(entries)) + for i, e := range entries { + arr[i] = e.value + } + + return arr + } + + if hasNum && hasStr { + t := newTable() + + for _, e := range entries { + switch k := e.key.(type) { + case string: + if _, ok := t.str[k]; !ok { + t.strKeys = append(t.strKeys, k) + } + + t.str[k] = e.value + case int64: + if _, ok := t.num[k]; !ok { + t.numKeys = append(t.numKeys, k) + } + + t.num[k] = e.value + default: + panic(fmt.Sprintf("manif: unexpected key type %T", e.key)) + } + } + + sort.Strings(t.strKeys) + slices.Sort(t.numKeys) + + return t + } + + m := map[string]any{} + + for _, e := range entries { + switch k := e.key.(type) { + case string: + m[k] = e.value + case int64: + m[strconv.FormatInt(k, 10)] = e.value + case float64: + m[strconv.FormatFloat(k, 'g', -1, 64)] = e.value + default: + panic(fmt.Sprintf("manif: unexpected key type %T", e.key)) + } + } + + return m +} + +type entry struct { + key any + value any +} + +// ErrParse is wrapped by every error Parse returns; test for it with +// errors.Is(err, ErrParse). +var ErrParse = errors.New("parse error") diff --git a/lib/luarocks/manif/store.go b/lib/luarocks/manif/store.go new file mode 100644 index 000000000..9d1c95f20 --- /dev/null +++ b/lib/luarocks/manif/store.go @@ -0,0 +1,605 @@ +package manif + +import ( + "errors" + "fmt" + "os" + "path" + "strings" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// FileStore implements the module-level ManifestStore interface against the +// local filesystem. +// +// Path conventions: +// +// - ReadTree / WriteTree accept a tree-root directory; the manifest file +// lives at `/manifest`. For Tarantool the rocks_dir is +// `/share/tarantool/rocks/`, and `path` here is that +// rocks_dir — i.e. the same directory that `path.rocks_dir(tree)` +// returns upstream. +// +// - ReadRock / WriteRock take the absolute path to the rock_manifest file +// itself (`///rock_manifest`). Computing that +// path is the caller's responsibility — keeps the interface minimal +// and frees FileStore from owning tree-layout knowledge. +// +// FileStore is a value type with no state. +type FileStore struct{} + +// manifestDirMode is the permission applied to directories created while +// writing a manifest: owner rwx, group rx, no world access. +const manifestDirMode = 0o750 + +// Static check that FileStore implements the module-root interface. +var _ rocks.ManifestStore = FileStore{} + +// ReadTreeManifest is the package-level shortcut for FileStore.ReadTree. +// It exists so callers (notably tt pack's LuaGetRocksVersions replacement) +// can obtain a *rocks.Manifest without spelling FileStore out. +// +// This deliberately lives in the manif package rather than at the module +// root: the root rocks package cannot import manif without creating an +// import cycle (manif already imports root for the Manifest type). +func ReadTreeManifest(treePath string) (*rocks.Manifest, error) { + return FileStore{}.ReadTree(treePath) +} + +// ReadTree reads and decodes the tree-level manifest at `/manifest`. +// +// The decoded shape is converted into a *rocks.Manifest. Fields that the +// in-memory type does not model (e.g. each repository entry's `modules` and +// `commands` sub-tables, or the `dependencies` constraint trees) are +// preserved enough to populate the Manifest projection needed by callers. +func (FileStore) ReadTree(treePath string) (*rocks.Manifest, error) { + data, err := os.ReadFile(path.Join(treePath, "manifest")) + if err != nil { + return nil, fmt.Errorf("manif.FileStore.ReadTree: %w", err) + } + + v, err := Parse(data) + if err != nil { + return nil, fmt.Errorf("manif.FileStore.ReadTree: %w", err) + } + + root, ok := v.(map[string]any) + if !ok { + return nil, fmt.Errorf("manif.FileStore.ReadTree: top-level value is %T, want map", v) + } + + return manifestFromTable(root) +} + +// WriteTree encodes m and writes it atomically to `/manifest`. +func (FileStore) WriteTree(treePath string, m *rocks.Manifest) error { + if m == nil { + return errors.New("manif.FileStore.WriteTree: nil manifest") + } + + tbl := manifestToTable(m) + dst := path.Join(treePath, "manifest") + + return writeAtomic(dst, tbl) +} + +// ReadRock reads a rock_manifest file at the given absolute path. +// +// Per persist `save_as_module`, rock_manifest files start with +// `return {\n ... }\n`. Parse is for assignments-mode files, so ReadRock +// strips the `return ` prefix and trailing newlines before delegating to +// the parser via a synthetic `rock_manifest = ...` wrapper. +func (FileStore) ReadRock(filePath string) (*rocks.RockManifest, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("manif.FileStore.ReadRock: %w", err) + } + // rock_manifest is either: + // - module-mode (`return { ... }`) when written by save_as_module, or + // - assignments-mode (`rock_manifest = { ... }`) which is what + // writer.lua actually emits via persist.save_from_table. + // Upstream's `make_rock_manifest` (manif/writer.lua) wraps the data in + // `{ rock_manifest = tree }` and calls save_from_table — so the on-disk + // file is assignments-mode with a single `rock_manifest = { ... }` key. + v, err := Parse(data) + if err != nil { + return nil, fmt.Errorf("manif.FileStore.ReadRock: %w", err) + } + + top, ok := v.(map[string]any) + if !ok { + return nil, fmt.Errorf("manif.FileStore.ReadRock: top-level is %T, want map", v) + } + + inner, ok := top["rock_manifest"] + if !ok { + return nil, errors.New("manif.FileStore.ReadRock: missing rock_manifest key") + } + + innerMap, ok := inner.(map[string]any) + if !ok { + return nil, fmt.Errorf("manif.FileStore.ReadRock: rock_manifest is %T, want map", inner) + } + + return rockManifestFromTable(innerMap) +} + +// WriteRock encodes rm and writes it atomically to filePath. +func (FileStore) WriteRock(filePath string, rm *rocks.RockManifest) error { + if rm == nil { + return errors.New("manif.FileStore.WriteRock: nil rock manifest") + } + + tbl := map[string]any{ + "rock_manifest": rockManifestToTable(rm), + } + + return writeAtomic(filePath, tbl) +} + +// writeAtomic serializes tbl, writes it to filePath.tmp, and renames into +// place. Matches upstream `save_table`'s tmp+rename atomicity. +func writeAtomic(filePath string, tbl any) error { + dir := path.Dir(filePath) + if err := os.MkdirAll(dir, manifestDirMode); err != nil { + return fmt.Errorf("manif: mkdir %q: %w", dir, err) + } + + tmp := filePath + ".tmp" + + f, err := os.Create(tmp) + if err != nil { + return fmt.Errorf("manif: create %q: %w", tmp, err) + } + + if err := Write(f, tbl); err != nil { + _ = f.Close() + _ = os.Remove(tmp) + + return err + } + + if err := f.Close(); err != nil { + _ = os.Remove(tmp) + + return fmt.Errorf("manif: close %q: %w", tmp, err) + } + + if err := os.Rename(tmp, filePath); err != nil { + _ = os.Remove(tmp) + + return fmt.Errorf("manif: rename %q -> %q: %w", tmp, filePath, err) + } + + return nil +} + +// manifestToTable projects a *rocks.Manifest into the table shape persist +// expects. Empty sub-maps render as `{}` (Lua's standard empty-table form), +// matching upstream output for a freshly-initialized tree. +func manifestToTable(m *rocks.Manifest) map[string]any { + out := map[string]any{} + + cmd := map[string]any{} + + for k, v := range m.Commands { + arr := make([]any, len(v)) + for i, s := range v { + arr[i] = s + } + + cmd[k] = arr + } + + out["commands"] = cmd + + mod := map[string]any{} + + for k, v := range m.Modules { + arr := make([]any, len(v)) + for i, s := range v { + arr[i] = s + } + + mod[k] = arr + } + + out["modules"] = mod + + repo := map[string]any{} + + for pkg, versions := range m.Repository { + verMap := map[string]any{} + + for ver, entry := range versions { + ae := map[string]any{ + "arch": entry.Arch, + } + + if len(entry.Modules) > 0 { + modSub := map[string]any{} + for k, v := range entry.Modules { + modSub[k] = v + } + + ae["modules"] = modSub + } + + if len(entry.Commands) > 0 { + cmdSub := map[string]any{} + for k, v := range entry.Commands { + cmdSub[k] = v + } + + ae["commands"] = cmdSub + } + + verMap[ver] = []any{ae} + } + + repo[pkg] = verMap + } + + out["repository"] = repo + + if len(m.Dependencies) > 0 { + deps := map[string]any{} + + for pkg, vers := range m.Dependencies { + verMap := map[string]any{} + + for ver, list := range vers { + arr := make([]any, len(list)) + + for i, d := range list { + depTbl := map[string]any{"name": d.Name} + + if len(d.Constraints) > 0 { + cs := make([]any, len(d.Constraints)) + for j, c := range d.Constraints { + cs[j] = map[string]any{ + "op": c.Op, + "version": c.Version.Raw, + } + } + + depTbl["constraints"] = cs + } + + arr[i] = depTbl + } + + verMap[ver] = arr + } + + deps[pkg] = verMap + } + + out["dependencies"] = deps + } + + return out +} + +// manifestFromTable is the inverse of manifestToTable. It is intentionally +// tolerant of the richer upstream shape (e.g. arch tables with `modules` +// and `commands` sub-fields) by ignoring fields the in-memory type does +// not model — but it does NOT silently invent data when required fields +// are missing. +func manifestFromTable(t map[string]any) (*rocks.Manifest, error) { + m := &rocks.Manifest{ + Repository: map[string]map[string]rocks.RepoEntry{}, + Modules: map[string][]string{}, + Commands: map[string][]string{}, + Dependencies: map[string]map[string][]rocks.Dep{}, + } + + if v, ok := t["modules"]; ok { + err := loadStringListMap(v, m.Modules, "modules") + if err != nil { + return nil, err + } + } + + if v, ok := t["commands"]; ok { + err := loadStringListMap(v, m.Commands, "commands") + if err != nil { + return nil, err + } + } + + if v, ok := t["repository"]; ok { + repoMap, ok := v.(map[string]any) + if !ok { + return nil, fmt.Errorf("manif.ReadTree: repository is %T, want map", v) + } + + for pkg, vAny := range repoMap { + verMap, ok := vAny.(map[string]any) + if !ok { + return nil, fmt.Errorf("manif.ReadTree: repository.%s is %T, want map", pkg, vAny) + } + + inner := map[string]rocks.RepoEntry{} + + for ver, vv := range verMap { + entry, err := extractRepoEntry(vv) + if err != nil { + return nil, fmt.Errorf("manif.ReadTree: repository.%s.%s: %w", pkg, ver, err) + } + + inner[ver] = entry + } + + m.Repository[pkg] = inner + } + } + + if v, ok := t["dependencies"]; ok { + depMap, ok := v.(map[string]any) + if !ok { + return nil, fmt.Errorf("manif.ReadTree: dependencies is %T, want map", v) + } + + for pkg, vAny := range depMap { + verMap, ok := vAny.(map[string]any) + if !ok { + return nil, fmt.Errorf("manif.ReadTree: dependencies.%s is %T, want map", pkg, vAny) + } + + inner := map[string][]rocks.Dep{} + + for ver, vv := range verMap { + deps, err := loadDepList(vv) + if err != nil { + return nil, fmt.Errorf("manif.ReadTree: dependencies.%s.%s: %w", pkg, ver, err) + } + + inner[ver] = deps + } + + m.Dependencies[pkg] = inner + } + } + + return m, nil +} + +func loadStringListMap(v any, dst map[string][]string, name string) error { + tm, ok := v.(map[string]any) + if !ok { + return fmt.Errorf("manif.ReadTree: %s is %T, want map", name, v) + } + + for k, raw := range tm { + arr, ok := raw.([]any) + if !ok { + return fmt.Errorf("manif.ReadTree: %s.%s is %T, want array", name, k, raw) + } + + lst := make([]string, len(arr)) + + for i, e := range arr { + s, ok := e.(string) + if !ok { + return fmt.Errorf("manif.ReadTree: %s.%s[%d] is %T, want string", name, k, i, e) + } + + lst[i] = s + } + + dst[k] = lst + } + + return nil +} + +func extractRepoEntry(v any) (rocks.RepoEntry, error) { + // repository[pkg][ver] is an array of one or more arch entries. + arr, ok := v.([]any) + if !ok { + return rocks.RepoEntry{}, fmt.Errorf("expected array of arch entries, got %T", v) + } + + if len(arr) == 0 { + return rocks.RepoEntry{}, errors.New("empty arch array") + } + + entry, ok := arr[0].(map[string]any) + if !ok { + return rocks.RepoEntry{}, fmt.Errorf("first arch entry is %T, want map", arr[0]) + } + + arch, ok := entry["arch"].(string) + if !ok { + return rocks.RepoEntry{}, errors.New("arch entry missing 'arch' string") + } + + re := rocks.RepoEntry{Arch: arch} + if mods, ok := entry["modules"].(map[string]any); ok && len(mods) > 0 { + re.Modules = map[string]string{} + + for k, v := range mods { + s, ok := v.(string) + if !ok { + return rocks.RepoEntry{}, fmt.Errorf("modules.%s is %T, want string", k, v) + } + + re.Modules[k] = s + } + } + + if cmds, ok := entry["commands"].(map[string]any); ok && len(cmds) > 0 { + re.Commands = map[string]string{} + + for k, v := range cmds { + s, ok := v.(string) + if !ok { + return rocks.RepoEntry{}, fmt.Errorf("commands.%s is %T, want string", k, v) + } + + re.Commands[k] = s + } + } + + return re, nil +} + +func loadDepList(v any) ([]rocks.Dep, error) { + arr, ok := v.([]any) + if !ok { + // Upstream LuaRocks writes a rock's dependency list as a Lua table. + // When the rock has no dependencies the list is the empty table `{}`, + // which decodes to an empty map (Lua can't distinguish an empty array + // from an empty map). Treat that as "no dependencies" so the native + // reader can load manifests written by the gopher-lua backend. + if m, isMap := v.(map[string]any); isMap && len(m) == 0 { + return nil, nil + } + + return nil, fmt.Errorf("expected dep array, got %T", v) + } + + out := make([]rocks.Dep, len(arr)) + + for i, raw := range arr { + m, ok := raw.(map[string]any) + if !ok { + return nil, fmt.Errorf("dep entry %d is %T, want map", i, raw) + } + + name, _ := m["name"].(string) + d := rocks.Dep{Name: name} + + if cs, ok := m["constraints"].([]any); ok { + for _, c := range cs { + cm, ok := c.(map[string]any) + if !ok { + return nil, fmt.Errorf("constraint is %T, want map", c) + } + + op, _ := cm["op"].(string) + ver, _ := cm["version"].(string) + d.Constraints = append(d.Constraints, rocks.VersionConstraint{ + Op: op, + Version: rocks.Version{Raw: ver}, + }) + } + } + + out[i] = d + } + + return out, nil +} + +// rockManifestToTable lays out a RockManifest into the nested +// per-directory shape persist expects. +func rockManifestToTable(rm *rocks.RockManifest) map[string]any { + out := map[string]any{} + if rm.Rockspec != "" { + out["rockspec"] = rm.Rockspec + } + + addDir := func(name string, m map[string]string) { + if len(m) == 0 { + return + } + + out[name] = stringMapToNested(m) + } + addDir("lua", rm.Lua) + addDir("lib", rm.Lib) + addDir("bin", rm.Bin) + addDir("conf", rm.Conf) + addDir("doc", rm.Doc) + + return out +} + +// stringMapToNested folds a map of slash-separated paths into a nested +// tree, mirroring upstream `make_rock_manifest` (manif/writer.lua:256-289). +func stringMapToNested(m map[string]string) map[string]any { + root := map[string]any{} + + for full, md5 := range m { + parts := strings.Split(full, "/") + cur := root + + for i, p := range parts { + if i == len(parts)-1 { + cur[p] = md5 + + break + } + + next, ok := cur[p].(map[string]any) + if !ok { + next = map[string]any{} + cur[p] = next + } + + cur = next + } + } + + return root +} + +func rockManifestFromTable(t map[string]any) (*rocks.RockManifest, error) { + rm := &rocks.RockManifest{ + Lua: map[string]string{}, + Lib: map[string]string{}, + Bin: map[string]string{}, + Conf: map[string]string{}, + Doc: map[string]string{}, + } + + if v, ok := t["rockspec"]; ok { + s, ok := v.(string) + if !ok { + return nil, fmt.Errorf("rock_manifest.rockspec is %T, want string", v) + } + + rm.Rockspec = s + } + + for key, dst := range map[string]map[string]string{ + "lua": rm.Lua, "lib": rm.Lib, "bin": rm.Bin, "conf": rm.Conf, "doc": rm.Doc, + } { + if v, ok := t[key]; ok { + m, ok := v.(map[string]any) + if !ok { + return nil, fmt.Errorf("rock_manifest.%s is %T, want map", key, v) + } + + flattenNested("", m, dst) + } + } + + return rm, nil +} + +func flattenNested(prefix string, m map[string]any, dst map[string]string) { + for k, v := range m { + full := k + if prefix != "" { + full = prefix + "/" + k + } + + switch x := v.(type) { + case string: + dst[full] = x + case map[string]any: + flattenNested(full, x, dst) + default: + // Numeric keys etc. should not appear; the only producer + // (Lua persist) only emits string-keyed trees at this level. + // Encode unexpected leaves as a sentinel stringification so + // we don't lose data — and surface it for inspection rather + // than dropping. + dst[full] = fmt.Sprintf("%v", x) + } + } +} diff --git a/lib/luarocks/manif/writer.go b/lib/luarocks/manif/writer.go new file mode 100644 index 000000000..68208835c --- /dev/null +++ b/lib/luarocks/manif/writer.go @@ -0,0 +1,451 @@ +// Package manif reads and writes LuaRocks tree- and rock-manifest files. +// +// The writer is a byte-for-byte reimplementation of upstream +// `luarocks/src/luarocks/persist.lua`'s `save_from_table_to_string` +// pipeline. Goldens under testdata/persist/out are generated from upstream +// (see gen_goldens.sh) and are the canonical regression evidence. +// +// The parser is a small hand-rolled recursive-descent reader for the +// restricted Lua-table-literal subset that persist.lua emits. It deliberately +// does NOT depend on a Lua VM and fails loud on unsupported syntax. +package manif + +import ( + "errors" + "fmt" + "io" + "maps" + "math" + "slices" + "sort" + "strconv" + "strings" +) + +// Write serializes v to w using upstream luarocks' "assignments mode" +// (`persist.save_from_table_to_string`). The accepted shape of v matches what +// upstream's runtime would have produced from a Lua table: +// +// - map[string]any → table with string keys +// - map[any]any → table with mixed keys (string and int64) +// - []any → 1-indexed numeric array +// - string → Lua string +// - bool → Lua boolean +// - int / int8..int64 / uint / uint8..uint64 → Lua number (integer form) +// - float32 / float64 → Lua number (float form) +// +// The top-level value MUST be a table (map or []any). All top-level keys +// must be strings matching `[a-zA-Z_][a-zA-Z0-9_]*` and not a Lua keyword. +// Anything else mirrors upstream's hard error +// ("cannot store '' as a plain key"). +// +// Write returns ErrInvalidTopLevelKey for that case, and a wrapped error +// for write failures. +func Write(w io.Writer, v any) error { + tbl, err := normalizeTable(v) + if err != nil { + return fmt.Errorf("manif.Write: top-level value: %w", err) + } + + bw := &bufWriter{w: w} + if err := writeTableAsAssignments(bw, tbl); err != nil { + return err + } + + return bw.err +} + +// ErrInvalidTopLevelKey is returned when Write is called with a table whose +// top-level keys cannot be serialized as bare Lua identifiers. +var ErrInvalidTopLevelKey = errors.New("manif: top-level key is not a valid plain Lua identifier") + +// bufWriter is a tiny error-trapping io.Writer wrapper. +type bufWriter struct { + w io.Writer + err error +} + +func (b *bufWriter) write(s string) { + if b.err != nil { + return + } + + _, b.err = io.WriteString(b.w, s) +} + +// table is the internal canonical form used by the writer/parser. +// +// strKeys is sorted by Go string-comparison (matches Lua default_sort for +// string keys). numKeys is sorted ascending. Entries with non-string, +// non-integer keys are an error — upstream persist supports a richer key +// space but every concrete manifest input only uses these two. +type table struct { + strKeys []string + str map[string]any + numKeys []int64 + num map[int64]any +} + +func newTable() *table { + return &table{str: map[string]any{}, num: map[int64]any{}} +} + +// normalizeTable converts the various Go shapes Write accepts into the +// internal canonical form. +func normalizeTable(v any) (*table, error) { + t := newTable() + + switch x := v.(type) { + case *table: + return x, nil + case map[string]any: + maps.Copy(t.str, x) + case map[any]any: + for k, vv := range x { + switch kk := k.(type) { + case string: + t.str[kk] = vv + case int: + t.num[int64(kk)] = vv + case int64: + t.num[kk] = vv + default: + return nil, fmt.Errorf("unsupported key type %T", k) + } + } + case []any: + for i, vv := range x { + t.num[int64(i+1)] = vv + } + default: + return nil, fmt.Errorf("expected table-shaped value, got %T", v) + } + + for k := range t.str { + t.strKeys = append(t.strKeys, k) + } + + sort.Strings(t.strKeys) + + for k := range t.num { + t.numKeys = append(t.numKeys, k) + } + + slices.Sort(t.numKeys) + + return t, nil +} + +// writeTableAsAssignments mirrors persist.lua:154-164. +// At the top level every key MUST be a valid plain Lua identifier. +func writeTableAsAssignments(out *bufWriter, t *table) error { + if len(t.numKeys) != 0 { + return fmt.Errorf("%w: numeric key %d present at top level", ErrInvalidTopLevelKey, t.numKeys[0]) + } + + for _, k := range t.strKeys { + if !isValidPlainKey(k) { + return fmt.Errorf("%w: %q", ErrInvalidTopLevelKey, k) + } + + out.write(k) + out.write(" = ") + writeValue(out, t.str[k], 0) + out.write("\n") + } + + return nil +} + +// writeValue mirrors persist.lua:41-62 (`write_value`). +func writeValue(out *bufWriter, v any, level int) { + switch x := v.(type) { + case string: + writeString(out, x) + case bool: + if x { + out.write("true") + } else { + out.write("false") + } + case nil: + out.write("nil") + case float32: + out.write(formatNumber(float64(x))) + case float64: + out.write(formatNumber(x)) + case int: + out.write(strconv.FormatInt(int64(x), 10)) + case int8: + out.write(strconv.FormatInt(int64(x), 10)) + case int16: + out.write(strconv.FormatInt(int64(x), 10)) + case int32: + out.write(strconv.FormatInt(int64(x), 10)) + case int64: + out.write(strconv.FormatInt(x, 10)) + case uint: + out.write(strconv.FormatUint(uint64(x), 10)) + case uint8: + out.write(strconv.FormatUint(uint64(x), 10)) + case uint16: + out.write(strconv.FormatUint(uint64(x), 10)) + case uint32: + out.write(strconv.FormatUint(uint64(x), 10)) + case uint64: + out.write(strconv.FormatUint(x, 10)) + default: + nt, err := normalizeTable(v) + if err != nil { + if out.err == nil { + out.err = fmt.Errorf("manif.Write: %w", err) + } + + return + } + + writeTable(out, nt, level+1) + } +} + +// writeTable mirrors persist.lua:115-147. +// Key ordering: numeric keys first (ascending), then string keys +// (ascending lexicographic) — matching util.default_sort. +func writeTable(out *bufWriter, t *table, level int) { + out.write("{") + + sep := "\n" + indent := true + + emit := func(k any, v any) { + out.write(sep) + + if indent { + for range level { + out.write(" ") + } + } + + if _, isNum := k.(int64); !isNum { + // String key (numeric keys produce no key assignment). + writeTableKeyAssignment(out, k, level) + } + + writeValue(out, v, level) + + if _, isNum := v.(float32); isNum { + sep = ", " + indent = false + + return + } + + if _, isNum := v.(float64); isNum { + sep = ", " + indent = false + + return + } + + if isGoInt(v) { + sep = ", " + indent = false + + return + } + + sep = ",\n" + indent = true + } + + for _, k := range t.numKeys { + emit(k, t.num[k]) + } + + for _, k := range t.strKeys { + emit(k, t.str[k]) + } + + if sep != "\n" { + out.write("\n") + + for range level - 1 { + out.write(" ") + } + } + + out.write("}") +} + +// writeTableKeyAssignment mirrors persist.lua:96-106. +func writeTableKeyAssignment(out *bufWriter, k any, level int) { + if s, ok := k.(string); ok && isValidPlainKey(s) { + out.write(s) + } else { + out.write("[") + writeValue(out, k, level) + out.write("]") + } + + out.write(" = ") +} + +// writeString mirrors persist.lua:45-58. +// Strings containing \r or \n are written as long-bracket strings; +// everything else goes through %q. +func writeString(out *bufWriter, s string) { + if strings.ContainsAny(s, "\r\n") { + writeLongBracket(out, s) + + return + } + + out.write(luaQ(s)) +} + +// writeLongBracket finds the smallest equals-count k such that "]" + k*"=" + "]" +// does not appear in s + "]". Matches persist.lua:46-55. Note the trailing +// "]" probe is what catches values that legitimately end in "]". +func writeLongBracket(out *bufWriter, s string) { + equals := 0 + probe := s + "]" + + for { + closeBracket := "]" + strings.Repeat("=", equals) + "]" + if !strings.Contains(probe, closeBracket) { + open := "[" + strings.Repeat("=", equals) + "[" + out.write(open) + out.write("\n") + out.write(s) + out.write(closeBracket) + + return + } + + equals++ + } +} + +// luaQ replicates Lua 5.1 / LuaJIT's string.format("%q", s). +// +// Confirmed against /opt/homebrew/bin/tarantool (LuaJIT 2.1): +// +// - '"' → \" +// - '\\' → \\ +// - '\n' → '\' + literal LF +// - bytes 0x00-0x09, 0x0B-0x0C, 0x0E-0x1F, 0x7F → "\DDD" decimal +// - bytes 0x20-0x7E (except '"' and '\\') → literal +// - bytes 0x80-0xFF → literal (LuaJIT does not escape high bytes) +// +// Digit count: minimal needed unless the next byte is an ASCII decimal digit, +// in which case 3 digits are used to prevent re-parsing into a different +// codepoint. +// +// Note: writeString already routes strings containing CR or LF through the +// long-bracket form, so luaQ never sees \n itself in practice — but the rule +// is preserved here for parity with upstream. +// quoteCharsLen accounts for the opening and closing '"' added by luaQ. +const quoteCharsLen = 2 + +func luaQ(s string) string { + var b strings.Builder + + b.Grow(len(s) + quoteCharsLen) + b.WriteByte('"') + + for i := range len(s) { + c := s[i] + + switch { + case c == '"' || c == '\\': + b.WriteByte('\\') + b.WriteByte(c) + case c == '\n': + b.WriteByte('\\') + b.WriteByte('\n') + case c < 0x20 || c == 0x7F: + nextIsDigit := i+1 < len(s) && s[i+1] >= '0' && s[i+1] <= '9' + + b.WriteByte('\\') + + if nextIsDigit { + fmt.Fprintf(&b, "%03d", c) + } else { + b.WriteString(strconv.Itoa(int(c))) + } + default: + b.WriteByte(c) + } + } + + b.WriteByte('"') + + return b.String() +} + +// formatNumber mirrors Lua 5.1 / LuaJIT's tostring on a number. +// +// LuaJIT tostring uses "%.14g" by default. For integer-valued doubles in +// safe-integer range it omits the decimal point (e.g. tostring(3) == "3", +// not "3.0"). +func formatNumber(f float64) string { + if math.IsNaN(f) { + return "nan" + } + + if math.IsInf(f, 1) { + return "inf" + } + + if math.IsInf(f, -1) { + return "-inf" + } + + if f == math.Trunc(f) && math.Abs(f) < 1e16 { + return strconv.FormatInt(int64(f), 10) + } + + return strconv.FormatFloat(f, 'g', 14, 64) +} + +// isValidPlainKey mirrors persist.lua:64-94. +func isValidPlainKey(s string) bool { + if s == "" { + return false + } + + c := s[0] + if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && c != '_' { + return false + } + + for i := 1; i < len(s); i++ { + c := s[i] + if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != '_' { + return false + } + } + + return !luaKeywords[s] +} + +var luaKeywords = map[string]bool{ + "and": true, "break": true, "do": true, "else": true, "elseif": true, + "end": true, "false": true, "for": true, "function": true, "goto": true, + "if": true, "in": true, "local": true, "nil": true, "not": true, + "or": true, "repeat": true, "return": true, "then": true, "true": true, + "until": true, "while": true, +} + +// isGoInt reports whether v is one of the Go integer types that writeValue +// renders without a decimal point. This drives the "inline-pack" separator +// rule alongside float32/float64. +func isGoInt(v any) bool { + switch v.(type) { + case int, int8, int16, int32, int64, + uint, uint8, uint16, uint32, uint64: + return true + } + + return false +} diff --git a/lib/luarocks/remote/remote.go b/lib/luarocks/remote/remote.go new file mode 100644 index 000000000..35c368730 --- /dev/null +++ b/lib/luarocks/remote/remote.go @@ -0,0 +1,391 @@ +// Package remote implements the default rocks.RemoteIndex against an +// HTTP(S) rock server. It is consumed by the Rocks facade's New +// constructor and by deps.Resolve via the rocks.RemoteIndex interface. +// +// The package lives outside the root rocks package because it consumes +// manif (Lua-source manifest parser) which itself imports rocks for the +// shared data types — placing HTTPRemoteIndex at the root would create +// an import cycle. +package remote + +import ( + "context" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "time" + + rocks "github.com/tarantool/tt/lib/luarocks" + "github.com/tarantool/tt/lib/luarocks/deps" + "github.com/tarantool/tt/lib/luarocks/manif" +) + +// HTTPRemoteIndex is the default rocks.RemoteIndex backed by one or more +// rock servers reachable over HTTP/HTTPS. +// +// Manifest filename probe order, per server: +// +// 1. manifest-.json (parsed as JSON) +// 2. manifest- (Lua-source via manif.Parse) +// 3. manifest (Lua-source) +// +// HTTPRemoteIndex caches the parsed manifest per (server, lua-version) +// pair for the lifetime of the struct so the resolver does not re-fetch +// for every Query call. +type HTTPRemoteIndex struct { + // Servers is the ordered list of base URLs (with or without trailing + // slash) to consult. Empty is an error from Query. + Servers []string + + // InsecureServers contains hostnames whose TLS certificates should not + // be verified. Pulled from Config.InsecureServers by the facade. + InsecureServers []string + + // UserAgent overrides the default User-Agent header for outbound GETs. + UserAgent string + + // LuaVersion is the Lua dialect string used to build the manifest + // filename. Defaults to "5.1" if empty (Tarantool case). + LuaVersion string + + // cache memoizes parsed manifests across calls. Keyed by the server + // string as passed to load (the raw, un-normalized entry from Servers). + cache map[string]*remoteManifest +} + +// Compile-time check that HTTPRemoteIndex satisfies rocks.RemoteIndex. +var _ rocks.RemoteIndex = (*HTTPRemoteIndex)(nil) + +const ( + // httpClientTimeout bounds a single manifest GET. + httpClientTimeout = 2 * time.Minute + + // httpStatusErrorThreshold is the lowest HTTP status code treated as an + // error (4xx and 5xx). + httpStatusErrorThreshold = 400 +) + +// Arch priorities ranking arch entries when one rock version is listed +// under multiple arches: richer payloads win (src > all > installed > +// rockspec). +const ( + prioRockspec = iota + prioInstalled + prioAll + prioSrc +) + +// archPriority ranks arch entries by the priorities above. +var archPriority = map[string]int{ + "src": prioSrc, + "all": prioAll, + "installed": prioInstalled, + "rockspec": prioRockspec, +} + +// remoteManifest is the in-memory projection of a parsed rock server +// manifest needed to satisfy Query. The full upstream shape carries more +// data (modules, commands, dependencies) but the resolver only needs the +// (name → versions → arch) triplet to construct URLs. +type remoteManifest struct { + server string + repository map[string]map[string][]archEntry +} + +type archEntry struct { + arch string +} + +// Query implements rocks.RemoteIndex. For each known server, it returns +// every VersionedRock listed under `name` in the manifest, with the URL +// pre-computed using path.make_url-equivalent rules (see makeRockURL). +func (h *HTTPRemoteIndex) Query(ctx context.Context, name string) ([]rocks.VersionedRock, error) { + if len(h.Servers) == 0 { + return nil, errors.New("remote.HTTPRemoteIndex: no servers configured") + } + + out := []rocks.VersionedRock{} + + for _, srv := range h.Servers { + mf, err := h.load(ctx, srv) + if err != nil { + return nil, fmt.Errorf("remote.HTTPRemoteIndex: load %s: %w", srv, err) + } + + versions, ok := mf.repository[name] + if !ok { + continue + } + + for verStr, arches := range versions { + v, err := deps.ParseVersion(verStr) + if err != nil { + return nil, fmt.Errorf("remote.HTTPRemoteIndex: parse version %q for %q: %w", verStr, name, err) + } + + pick := pickArch(arches) + out = append(out, rocks.VersionedRock{ + Name: name, + Version: v, + URL: makeRockURL(srv, name, verStr, pick), + }) + } + } + + return out, nil +} + +// load fetches and parses the manifest for one server, using the cache if +// present. +func (h *HTTPRemoteIndex) load(ctx context.Context, server string) (*remoteManifest, error) { + if h.cache == nil { + h.cache = map[string]*remoteManifest{} + } + + if m, ok := h.cache[server]; ok { + return m, nil + } + + lv := h.LuaVersion + if lv == "" { + lv = "5.1" + } + + base := strings.TrimRight(server, "/") + "/" + + type probe struct { + path string + isJSON bool + } + + probes := []probe{ + {path: base + "manifest-" + lv + ".json", isJSON: true}, + {path: base + "manifest-" + lv, isJSON: false}, + {path: base + "manifest", isJSON: false}, + } + + var lastErr error + + for _, p := range probes { + body, err := h.get(ctx, p.path) + if err != nil { + lastErr = err + + continue + } + + var raw map[string]any + if p.isJSON { + err := json.Unmarshal(body, &raw) + if err != nil { + lastErr = fmt.Errorf("decode JSON manifest at %s: %w", p.path, err) + + continue + } + } else { + v, err := manif.Parse(body) + if err != nil { + lastErr = fmt.Errorf("parse manifest at %s: %w", p.path, err) + + continue + } + + rawMap, ok := v.(map[string]any) + if !ok { + lastErr = fmt.Errorf("manifest at %s: top-level is %T, want map", p.path, v) + + continue + } + + raw = rawMap + } + + m, err := projectManifest(server, raw) + if err != nil { + lastErr = fmt.Errorf("project manifest from %s: %w", p.path, err) + + continue + } + + h.cache[server] = m + + return m, nil + } + + if lastErr == nil { + lastErr = fmt.Errorf("no manifest variant found at %s", server) + } + + return nil, lastErr +} + +// get performs a single HTTP GET with sensible defaults. It returns the +// response body for any status below 400; a status of 400 or above is an +// error. (Redirects are followed by the underlying http.Client, so 3xx +// responses are not normally seen here.) +func (h *HTTPRemoteIndex) get(ctx context.Context, rawURL string) ([]byte, error) { + u, err := url.Parse(rawURL) + if err != nil { + return nil, fmt.Errorf("parse url: %w", err) + } + + tr := &http.Transport{} + + for _, host := range h.InsecureServers { + if strings.EqualFold(host, u.Host) { + tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec // opt-in + + break + } + } + + client := &http.Client{Transport: tr, Timeout: httpClientTimeout} + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) + if err != nil { + return nil, err + } + + ua := h.UserAgent + if ua == "" { + ua = "go-luarocks/0.1" + } + + req.Header.Set("User-Agent", ua) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode >= httpStatusErrorThreshold { + return nil, fmt.Errorf("GET %s: status %d", rawURL, resp.StatusCode) + } + + return io.ReadAll(resp.Body) +} + +// projectManifest extracts the (name → version → arches) shape from the +// raw parsed manifest. We only consume `repository`. +func projectManifest(server string, raw map[string]any) (*remoteManifest, error) { + repoRaw, ok := raw["repository"] + if !ok { + return nil, errors.New("missing `repository` key") + } + + repoMap, ok := repoRaw.(map[string]any) + if !ok { + return nil, fmt.Errorf("repository is %T, want map", repoRaw) + } + + out := &remoteManifest{ + server: server, + repository: map[string]map[string][]archEntry{}, + } + + for name, vAny := range repoMap { + versions, ok := vAny.(map[string]any) + if !ok { + return nil, fmt.Errorf("repository.%s is %T, want map", name, vAny) + } + + inner := map[string][]archEntry{} + + for ver, entry := range versions { + arr, err := toArchArr(entry) + if err != nil { + return nil, fmt.Errorf("repository.%s.%s: %w", name, ver, err) + } + + inner[ver] = arr + } + + out.repository[name] = inner + } + + return out, nil +} + +func toArchArr(v any) ([]archEntry, error) { + switch arr := v.(type) { + case []any: + out := make([]archEntry, 0, len(arr)) + + for i, e := range arr { + em, ok := e.(map[string]any) + if !ok { + return nil, fmt.Errorf("entry %d is %T, want map", i, e) + } + + arch, _ := em["arch"].(string) + out = append(out, archEntry{arch: arch}) + } + + return out, nil + case map[string]any: + // JSON serialization of the upstream manifest can flatten the single- + // element arch array into a bare map. Accept that shape too. + arch, _ := arr["arch"].(string) + + return []archEntry{{arch: arch}}, nil + default: + return nil, fmt.Errorf("expected array or map, got %T", v) + } +} + +// pickArch chooses one arch entry from the list, preferring richer +// payloads (src > all > installed > rockspec). For URL construction the +// choice only matters when one server lists the same rock under multiple +// arches; for typical rock servers each version has one entry. +func pickArch(arches []archEntry) string { + if len(arches) == 0 { + return "" + } + + best := arches[0].arch + + bestP, ok := archPriority[best] + if !ok { + bestP = -1 + } + + for _, a := range arches[1:] { + p, ok := archPriority[a.arch] + if !ok { + p = -1 + } + + if p > bestP { + best = a.arch + bestP = p + } + } + + return best +} + +// makeRockURL constructs the resource URL for one rock entry, matching +// upstream `path.make_url(repo, name, version, arch)`. +// +// arch == "rockspec" → /-.rockspec +// arch == "installed" → ///-.rockspec +// otherwise → /-..rock +func makeRockURL(server, name, version, arch string) string { + base := strings.TrimRight(server, "/") + "/" + + switch arch { + case "rockspec": + return base + name + "-" + version + ".rockspec" + case "installed": + return base + name + "/" + version + "/" + name + "-" + version + ".rockspec" + default: + return base + name + "-" + version + "." + arch + ".rock" + } +} diff --git a/lib/luarocks/rocks.go b/lib/luarocks/rocks.go new file mode 100644 index 000000000..8770934ca --- /dev/null +++ b/lib/luarocks/rocks.go @@ -0,0 +1,20 @@ +// Package rocks is a pure-Go implementation of the LuaRocks subset needed +// to manage rocks inside a Tarantool installation. +// +// The library produces on-disk artifacts (tree layout, manifest files, +// .rock archives) that are byte-equal to what upstream LuaRocks 3.9.2 would +// produce for the same rock at the same version against a Tarantool target. +// A rock installed by lib/luarocks is queryable by upstream +// LuaRocks's `luarocks show` and vice versa. +// +// The facade — `client.New(rocks.Config{...}).Install(ctx, "metrics", opts)` +// — lives in the sub-package `github.com/tarantool/tt/lib/luarocks/client` +// rather than at this root: it calls `deps.Resolve` and composes the `remote` +// index, both of which transitively import this root package for shared data +// types, so a facade here would form an import cycle. +package rocks + +// LuaRocksVersion is the LuaRocks release the embedded Tarantool fork is based on for +// the lua backend (see internal/luarocks). The native backend produces on-disk +// artifacts byte-equal to this LuaRocks version. +const LuaRocksVersion = "3.9.2" diff --git a/lib/luarocks/rockspec/eval.go b/lib/luarocks/rockspec/eval.go new file mode 100644 index 000000000..c365da605 --- /dev/null +++ b/lib/luarocks/rockspec/eval.go @@ -0,0 +1,597 @@ +// Package rockspec implements the sandboxed gopher-lua evaluator for +// .rockspec source files (this is the ONLY package permitted to import +// gopher-lua). The output of Eval is a plain-data *rocks.Rockspec; downstream +// packages (fetch, build, deps, manif) consume that struct without touching +// Lua at all. +// +// Sandbox: +// +// - The evaluator opens base/string/table/math only. io, package, debug, +// channel, coroutine, and the full os library are NOT opened. +// - A custom os table is installed with just three functions: getenv, +// time, date. getenv routes through buildGetenv (configurable per +// RockspecConfig.Env). +// - Base-library doors to the host — require, load, loadfile, dofile, +// loadstring, module — are removed, together with the metatable/raw/ +// environment primitives (setmetatable, getmetatable, rawset, rawget, +// rawequal, rawlen, setfenv, getfenv, newproxy, collectgarbage) and the +// package/io/debug/coroutine globals. (print is intentionally left +// reachable — it is harmless and aids debugging rockspecs.) +// +// Tests in eval_test.go assert that each forbidden global is unreachable +// from rockspec code. +package rockspec + +import ( + "fmt" + "os" + "strings" + "time" + + lua "github.com/yuin/gopher-lua" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// Eval reads `path` as a `.rockspec` file, executes it inside a sandboxed +// gopher-lua VM, and harvests known globals into a *rocks.Rockspec. +// +// The returned spec contains Build.Platforms populated as-declared; the +// caller folds them into Build via MergePlatforms (3.4) when running for a +// specific OS. Eval does NOT call MergePlatforms itself — keeping the two +// stages distinct lets the same parsed spec drive both the host build and +// e.g. `download` flows that want to inspect platform overlays. +// +// Returns ErrUnsupportedRockspecFeature if build.type is outside the +// {builtin, cmake, make, command, none} set. +func Eval(path string, cfg rocks.RockspecConfig) (*rocks.Rockspec, error) { + src, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("rockspec: read %s: %w", path, err) + } + + L := newSandboxedState(cfg) + defer L.Close() + + if err := L.DoString(string(src)); err != nil { + return nil, fmt.Errorf("rockspec: eval %s: %w", path, err) + } + + spec := &rocks.Rockspec{} + if err := harvest(L, spec); err != nil { + return nil, fmt.Errorf("rockspec: harvest %s: %w", path, err) + } + + return spec, nil +} + +// newSandboxedState builds a fresh LState with only the safe libs installed +// and the rockspec-allowed os subset wired in. +func newSandboxedState(cfg rocks.RockspecConfig) *lua.LState { + L := lua.NewState(lua.Options{SkipOpenLibs: true}) + // Whitelist: base, string, table, math. Each entry pushes the loader as + // a function and calls it with the lib name as Lua's stdlib loaders + // expect (mirrors LState.OpenLibs but trimmed). + for _, lib := range []struct { + name string + fn lua.LGFunction + }{ + {"", lua.OpenBase}, + {lua.TabLibName, lua.OpenTable}, + {lua.StringLibName, lua.OpenString}, + {lua.MathLibName, lua.OpenMath}, + } { + L.Push(L.NewFunction(lib.fn)) + L.Push(lua.LString(lib.name)) + L.Call(1, 0) + } + + // Lock down the base-library doors to the host. Setting these to nil + // turns `require(...)` and `loadfile(...)` into "attempt to call a nil + // value" — a loud, recognisable Lua runtime error. + // + // Two groups blocked: + // - Host-reach: load*, require, dofile, io, package, debug, coroutine. + // - Environment manipulation: setfenv/getfenv re-wire the running + // thread's globals; raw* / setmetatable / getmetatable / collectgarbage + // can stage similar shadowing. The reach to the Go host is nil + // (gopher-lua confines them to Lua-side state), but a rockspec doing + // `setfenv(0, t)` could mutate what the harvester later reads via + // L.GetGlobal — defense-in-depth for the eval pass is cheap. + for _, name := range []string{ + "require", "load", "loadfile", "dofile", "loadstring", + "module", "package", "io", "debug", "coroutine", + "newproxy", + "setfenv", "getfenv", + "rawset", "rawget", "rawequal", "rawlen", + "setmetatable", "getmetatable", + "collectgarbage", + } { + L.SetGlobal(name, lua.LNil) + } + + // Install the tightly-scoped os table: only getenv, time, date. + osTab := L.NewTable() + osTab.RawSetString("getenv", L.NewFunction(buildGetenv(cfg.Env))) + osTab.RawSetString("time", L.NewFunction(osTime)) + osTab.RawSetString("date", L.NewFunction(osDate)) + L.SetGlobal("os", osTab) + + return L +} + +// osTime is a pass-through to time.Now().Unix(). gopher-lua's Lua 5.1 +// `os.time()` would accept a date table; we don't — pass no arguments. +func osTime(l *lua.LState) int { + l.Push(lua.LNumber(time.Now().Unix())) + + return 1 +} + +// osDate implements `os.date(format, [time])` for format strings only. The +// upstream Lua 5.1 contract supports a "*t" form that returns a table of +// fields; we intentionally omit it because the rockspec format almost never +// uses it and broader support would expand the sandbox surface for no gain. +// Lua argument positions for os.date(format, [time]). +const ( + osDateFormatArg = 1 + osDateTimeArg = 2 +) + +func osDate(l *lua.LState) int { + format := "%c" + if l.GetTop() >= osDateFormatArg { + format = l.CheckString(osDateFormatArg) + } + + t := time.Now() + if l.GetTop() >= osDateTimeArg { + t = time.Unix(int64(l.CheckNumber(osDateTimeArg)), 0) + } + + utc := false + if strings.HasPrefix(format, "!") { + utc = true + format = format[1:] + } + + if utc { + t = t.UTC() + } + + l.Push(lua.LString(strftime(format, t))) + + return 1 +} + +// strftime is a tiny strftime-equivalent covering the directives rockspecs +// realistically use. Unknown directives are passed through verbatim — this +// is conservative; rockspecs in practice only ever feed os.date a literal +// year/date for a placeholder, and we'd rather emit a recognisable token +// than silently drop characters. +// yearModulo reduces a four-digit year to its two-digit form for %y. +const yearModulo = 100 + +func strftime(format string, t time.Time) string { + var b strings.Builder + + for i := 0; i < len(format); i++ { + if format[i] != '%' || i+1 >= len(format) { + b.WriteByte(format[i]) + + continue + } + + i++ + switch format[i] { + case 'Y': + fmt.Fprintf(&b, "%04d", t.Year()) + case 'm': + fmt.Fprintf(&b, "%02d", int(t.Month())) + case 'd': + fmt.Fprintf(&b, "%02d", t.Day()) + case 'H': + fmt.Fprintf(&b, "%02d", t.Hour()) + case 'M': + fmt.Fprintf(&b, "%02d", t.Minute()) + case 'S': + fmt.Fprintf(&b, "%02d", t.Second()) + case 'y': + fmt.Fprintf(&b, "%02d", t.Year()%yearModulo) + case 'c': + b.WriteString(t.Format("Mon Jan 2 15:04:05 2006")) + case '%': + b.WriteByte('%') + default: + b.WriteByte('%') + b.WriteByte(format[i]) + } + } + + return b.String() +} + +// ---- harvesting ---- + +func harvest(l *lua.LState, spec *rocks.Rockspec) error { + spec.RockspecFormat = optString(l.GetGlobal("rockspec_format")) + spec.Package = optString(l.GetGlobal("package")) + spec.Version = optString(l.GetGlobal("version")) + + if tbl, ok := l.GetGlobal("description").(*lua.LTable); ok { + spec.Description = harvestDescription(tbl) + } + + if tbl, ok := l.GetGlobal("source").(*lua.LTable); ok { + spec.Source = harvestSource(tbl) + } + + if tbl, ok := l.GetGlobal("dependencies").(*lua.LTable); ok { + spec.Dependencies = harvestDeps(tbl) + } + + if tbl, ok := l.GetGlobal("build_dependencies").(*lua.LTable); ok { + spec.BuildDependencies = harvestDeps(tbl) + } + + if tbl, ok := l.GetGlobal("test_dependencies").(*lua.LTable); ok { + spec.TestDependencies = harvestDeps(tbl) + } + + if tbl, ok := l.GetGlobal("external_dependencies").(*lua.LTable); ok { + spec.ExternalDependencies = harvestExternalDeps(tbl) + } + + if tbl, ok := l.GetGlobal("supported_platforms").(*lua.LTable); ok { + spec.SupportedPlatforms = harvestStringArray(tbl) + } + + if tbl, ok := l.GetGlobal("build").(*lua.LTable); ok { + b, err := harvestBuild(tbl) + if err != nil { + return err + } + + spec.Build = b + } + + return nil +} + +func optString(v lua.LValue) string { + if s, ok := v.(lua.LString); ok { + return string(s) + } + + return "" +} + +func harvestDescription(tbl *lua.LTable) rocks.Description { + return rocks.Description{ + Summary: optString(tbl.RawGetString("summary")), + Detailed: optString(tbl.RawGetString("detailed")), + License: optString(tbl.RawGetString("license")), + Homepage: optString(tbl.RawGetString("homepage")), + IssuesURL: optString(tbl.RawGetString("issues_url")), + Maintainer: optString(tbl.RawGetString("maintainer")), + Labels: stringArray(tbl.RawGetString("labels")), + } +} + +func harvestSource(tbl *lua.LTable) rocks.Source { + return rocks.Source{ + URL: optString(tbl.RawGetString("url")), + Tag: optString(tbl.RawGetString("tag")), + Branch: optString(tbl.RawGetString("branch")), + MD5: optString(tbl.RawGetString("md5")), + File: optString(tbl.RawGetString("file")), + Dir: optString(tbl.RawGetString("dir")), + Module: optString(tbl.RawGetString("module")), + } +} + +// harvestDeps walks the array part of a dependencies-table and parses each +// dependency string into Name + raw constraint operators. +// +// Constraint parsing here is intentionally minimal: it splits on commas and +// then on whitespace to extract op+version pairs, leaving Version.Components +// empty. The version-string parser in deps/version.go refines this further. +func harvestDeps(tbl *lua.LTable) []rocks.Dep { + if tbl == nil { + return nil + } + + var deps []rocks.Dep + + tbl.ForEach(func(k, v lua.LValue) { + if _, ok := k.(lua.LNumber); !ok { + return + } + + s, ok := v.(lua.LString) + if !ok { + return + } + + if d, ok := parseDepString(string(s)); ok { + deps = append(deps, d) + } + }) + + return deps +} + +// parseDepString returns the Dep for a string like "foo >= 1.0, < 2.0". +// +// On a malformed input (no leading identifier) it returns ok=false rather +// than fabricating a name — fail loud, but ForEach is best-effort over +// table data so the caller drops the entry. (Validation surfaces missing +// dependencies via Validate later.) +func parseDepString(s string) (rocks.Dep, bool) { + s = strings.TrimSpace(s) + if s == "" { + return rocks.Dep{}, false + } + + parts := strings.Split(s, ",") + first := strings.TrimSpace(parts[0]) + // Identifier is everything up to the first whitespace. + name := first + rest := "" + + if i := strings.IndexAny(first, " \t"); i >= 0 { + name = first[:i] + rest = strings.TrimSpace(first[i:]) + } + + if name == "" { + return rocks.Dep{}, false + } + + dep := rocks.Dep{Name: name} + + if rest != "" { + if c, ok := parseConstraint(rest); ok { + dep.Constraints = append(dep.Constraints, c) + } + } + + for _, more := range parts[1:] { + more = strings.TrimSpace(more) + if more == "" { + continue + } + + if c, ok := parseConstraint(more); ok { + dep.Constraints = append(dep.Constraints, c) + } + } + + return dep, true +} + +// parseConstraint extracts op + raw version from one comma-segment of a +// dependency string. Unknown op → ok=false, caller drops the segment. +func parseConstraint(s string) (rocks.VersionConstraint, bool) { + s = strings.TrimSpace(s) + + ops := []string{">=", "<=", "==", "~=", "~>", ">", "<"} + for _, op := range ops { + if strings.HasPrefix(s, op) { + rest := strings.TrimSpace(s[len(op):]) + + return rocks.VersionConstraint{ + Op: op, + Version: rocks.Version{Raw: rest}, + }, true + } + } + // No operator → treat as implicit ==. + if s != "" { + return rocks.VersionConstraint{ + Op: "==", + Version: rocks.Version{Raw: s}, + }, true + } + + return rocks.VersionConstraint{}, false +} + +func harvestExternalDeps(tbl *lua.LTable) map[string]rocks.ExternalDep { + if tbl == nil { + return nil + } + + out := map[string]rocks.ExternalDep{} + + tbl.ForEach(func(k, v lua.LValue) { + name, ok := k.(lua.LString) + if !ok { + return + } + + sub, ok := v.(*lua.LTable) + if !ok { + return + } + + out[string(name)] = rocks.ExternalDep{ + Header: optString(sub.RawGetString("header")), + Library: optString(sub.RawGetString("library")), + } + }) + + return out +} + +func harvestStringArray(tbl *lua.LTable) []string { + return stringArray(tbl) +} + +func stringArray(v lua.LValue) []string { + tbl, ok := v.(*lua.LTable) + if !ok { + return nil + } + + out := make([]string, 0, tbl.Len()) + for i := 1; i <= tbl.Len(); i++ { + if s, ok := tbl.RawGetInt(i).(lua.LString); ok { + out = append(out, string(s)) + } + } + + return out +} + +func stringMap(v lua.LValue) map[string]string { + tbl, ok := v.(*lua.LTable) + if !ok { + return nil + } + + out := map[string]string{} + + tbl.ForEach(func(k, vv lua.LValue) { + ks, kok := k.(lua.LString) + + vs, vok := vv.(lua.LString) + if !kok || !vok { + return + } + + out[string(ks)] = string(vs) + }) + + if len(out) == 0 { + return nil + } + + return out +} + +func harvestBuild(tbl *lua.LTable) (rocks.Build, error) { + b := rocks.Build{ + Type: optString(tbl.RawGetString("type")), + BuildTarget: optString(tbl.RawGetString("build_target")), + InstallTarget: optString(tbl.RawGetString("install_target")), + BuildCommand: optString(tbl.RawGetString("build_command")), + InstallCommand: optString(tbl.RawGetString("install_command")), + Variables: stringMap(tbl.RawGetString("variables")), + BuildVariables: stringMap(tbl.RawGetString("build_variables")), + InstallVariables: stringMap(tbl.RawGetString("install_variables")), + CopyDirectories: stringArray(tbl.RawGetString("copy_directories")), + } + + if !allowedBuildTypes[b.Type] { + return rocks.Build{}, fmt.Errorf("%w: build.type=%q", + rocks.ErrUnsupportedRockspecFeature, b.Type) + } + + if modsTbl, ok := tbl.RawGetString("modules").(*lua.LTable); ok { + b.Modules = harvestModules(modsTbl) + } + + if installTbl, ok := tbl.RawGetString("install").(*lua.LTable); ok { + b.Install = harvestBuildInstall(installTbl) + } + + if platsTbl, ok := tbl.RawGetString("platforms").(*lua.LTable); ok { + plats, err := harvestPlatforms(platsTbl) + if err != nil { + return rocks.Build{}, err + } + + if len(plats) > 0 { + b.Platforms = plats + } + } + + return b, nil +} + +// harvestPlatforms recurses into a build.platforms table, returning one +// rocks.Build per named platform overlay. +func harvestPlatforms(platsTbl *lua.LTable) (map[string]rocks.Build, error) { + plats := map[string]rocks.Build{} + + var harvestErr error + + platsTbl.ForEach(func(k, v lua.LValue) { + if harvestErr != nil { + return + } + + name, ok := k.(lua.LString) + if !ok { + return + } + + sub, ok := v.(*lua.LTable) + if !ok { + return + } + + pb, err := harvestBuild(sub) + if err != nil { + harvestErr = err + + return + } + + plats[string(name)] = pb + }) + + if harvestErr != nil { + return nil, harvestErr + } + + return plats, nil +} + +func harvestModules(tbl *lua.LTable) map[string]rocks.Module { + out := map[string]rocks.Module{} + + tbl.ForEach(func(k, v lua.LValue) { + name, ok := k.(lua.LString) + if !ok { + return + } + + switch vv := v.(type) { + case lua.LString: + out[string(name)] = rocks.Module{Path: string(vv)} + case *lua.LTable: + m := rocks.Module{} + // String shorthand for sources: `{"src/foo.c"}` (array form). + if arr := stringArray(vv); len(arr) > 0 && vv.RawGetString("sources") == lua.LNil { + m.Sources = arr + } + + if s := stringArray(vv.RawGetString("sources")); len(s) > 0 { + m.Sources = s + } + + m.Incdirs = stringArray(vv.RawGetString("incdirs")) + m.Libdirs = stringArray(vv.RawGetString("libdirs")) + m.Libraries = stringArray(vv.RawGetString("libraries")) + m.Defines = stringArray(vv.RawGetString("defines")) + out[string(name)] = m + } + }) + + if len(out) == 0 { + return nil + } + + return out +} + +func harvestBuildInstall(tbl *lua.LTable) rocks.BuildInstall { + return rocks.BuildInstall{ + Lua: stringMap(tbl.RawGetString("lua")), + Lib: stringMap(tbl.RawGetString("lib")), + Bin: stringMap(tbl.RawGetString("bin")), + Conf: stringMap(tbl.RawGetString("conf")), + } +} diff --git a/lib/luarocks/rockspec/getenv.go b/lib/luarocks/rockspec/getenv.go new file mode 100644 index 000000000..bca1b6474 --- /dev/null +++ b/lib/luarocks/rockspec/getenv.go @@ -0,0 +1,46 @@ +package rockspec + +import ( + "os" + + lua "github.com/yuin/gopher-lua" +) + +// buildGetenv returns the sandboxed os.getenv implementation governed by +// RockspecConfig.Env: +// +// - env == nil → pass through to the host process via os.Getenv; +// - env != nil (non-empty) → return env[name] if present, else nil; +// - env != nil (empty) → always nil (env is exhaustively allow-listed). +// +// Returning lua nil for absent keys matches upstream Lua 5.1 os.getenv +// semantics: a `or` fallback in the rockspec ("$X or default") still works. +func buildGetenv(env map[string]string) lua.LGFunction { + if env == nil { + return func(L *lua.LState) int { + name := L.CheckString(1) + if v, ok := os.LookupEnv(name); ok { + L.Push(lua.LString(v)) + + return 1 + } + + L.Push(lua.LNil) + + return 1 + } + } + // Captured by reference — caller controls the map; do not mutate. + return func(L *lua.LState) int { + name := L.CheckString(1) + if v, ok := env[name]; ok { + L.Push(lua.LString(v)) + + return 1 + } + + L.Push(lua.LNil) + + return 1 + } +} diff --git a/lib/luarocks/rockspec/platforms.go b/lib/luarocks/rockspec/platforms.go new file mode 100644 index 000000000..d69a25847 --- /dev/null +++ b/lib/luarocks/rockspec/platforms.go @@ -0,0 +1,131 @@ +package rockspec + +import ( + "maps" + "runtime" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// RuntimePlatforms returns the platform-name slice to feed to MergePlatforms +// for the current runtime.GOOS, ordered least-specific to most-specific. The +// Tarantool-relevant set is {unix, linux, macosx, macos, bsd}. macosx and +// macos are both emitted on darwin because rockspecs in the wild use either +// spelling. +func RuntimePlatforms() []string { + switch runtime.GOOS { + case "linux": + return []string{"unix", "linux"} + case "darwin": + return []string{"unix", "macosx", "macos"} + case "freebsd", "openbsd", "netbsd", "dragonfly": + return []string{"unix", "bsd"} + default: + return []string{"unix"} + } +} + +// MergePlatforms folds spec.Build.Platforms[name] overlays into spec.Build +// for each name in plats, in order. Scalars overwrite; maps merge recursively. +// After all overlays are applied, spec.Build.Platforms is cleared (matching +// upstream rockspecs.lua: `tbl.platforms = nil`). +// +// Platform names not present in spec.Build.Platforms are ignored — undeclared +// platforms in the rockspec are valid; they are inert for our target (the +// fail-loud rule applies to unknown build types, not unknown platforms). +func MergePlatforms(spec *rocks.Rockspec, plats []string) { + if spec == nil || len(spec.Build.Platforms) == 0 { + if spec != nil { + spec.Build.Platforms = nil + } + + return + } + + for _, name := range plats { + overlay, ok := spec.Build.Platforms[name] + if !ok { + continue + } + + mergeBuild(&spec.Build, &overlay) + } + + spec.Build.Platforms = nil +} + +// mergeBuild applies src's fields onto dst. +// +// Mirrors util.deep_merge: for tables, merge in place; for scalars (non-zero +// in src), overwrite dst. We adopt the convention that the zero value of a +// scalar means "not set" by the overlay, so it does not clobber the base. +// This is conservative — rockspecs in practice declare a field only when +// they want to override it. +func mergeBuild(dst, src *rocks.Build) { + if src.Type != "" { + dst.Type = src.Type + } + + if src.BuildTarget != "" { + dst.BuildTarget = src.BuildTarget + } + + if src.InstallTarget != "" { + dst.InstallTarget = src.InstallTarget + } + + if src.BuildCommand != "" { + dst.BuildCommand = src.BuildCommand + } + + if src.InstallCommand != "" { + dst.InstallCommand = src.InstallCommand + } + + if len(src.CopyDirectories) > 0 { + dst.CopyDirectories = append(dst.CopyDirectories, src.CopyDirectories...) + } + + dst.Modules = mergeModules(dst.Modules, src.Modules) + dst.Variables = mergeStringMap(dst.Variables, src.Variables) + dst.BuildVariables = mergeStringMap(dst.BuildVariables, src.BuildVariables) + dst.InstallVariables = mergeStringMap(dst.InstallVariables, src.InstallVariables) + dst.Install = mergeInstall(dst.Install, src.Install) +} + +func mergeModules(dst, src map[string]rocks.Module) map[string]rocks.Module { + if len(src) == 0 { + return dst + } + + if dst == nil { + dst = make(map[string]rocks.Module, len(src)) + } + + maps.Copy(dst, src) + + return dst +} + +func mergeStringMap(dst, src map[string]string) map[string]string { + if len(src) == 0 { + return dst + } + + if dst == nil { + dst = make(map[string]string, len(src)) + } + + maps.Copy(dst, src) + + return dst +} + +func mergeInstall(dst, src rocks.BuildInstall) rocks.BuildInstall { + dst.Lua = mergeStringMap(dst.Lua, src.Lua) + dst.Lib = mergeStringMap(dst.Lib, src.Lib) + dst.Bin = mergeStringMap(dst.Bin, src.Bin) + dst.Conf = mergeStringMap(dst.Conf, src.Conf) + + return dst +} diff --git a/lib/luarocks/rockspec/validate.go b/lib/luarocks/rockspec/validate.go new file mode 100644 index 000000000..2921df543 --- /dev/null +++ b/lib/luarocks/rockspec/validate.go @@ -0,0 +1,48 @@ +package rockspec + +import ( + "errors" + "fmt" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// allowedBuildTypes mirrors the upstream backend table for the subset we +// implement. Empty string defaults to "builtin" per upstream rockspecs.lua. +var allowedBuildTypes = map[string]bool{ + "": true, // implies builtin + "builtin": true, + "cmake": true, + "make": true, + "command": true, + "none": true, +} + +// Validate performs presence and feature-allowlist checks on a harvested +// Rockspec. It deliberately does NOT re-do every type assertion the upstream +// `type_check.lua` schema enforces — the harvest in Eval already routes each +// field through a typed path. Fail loud on unknown build types. +func Validate(spec *rocks.Rockspec) error { + if spec == nil { + return errors.New("rockspec: nil spec") + } + + if spec.Package == "" { + return errors.New("rockspec: missing required field 'package'") + } + + if spec.Version == "" { + return errors.New("rockspec: missing required field 'version'") + } + + if spec.Source.URL == "" { + return errors.New("rockspec: missing required field 'source.url'") + } + + if !allowedBuildTypes[spec.Build.Type] { + return fmt.Errorf("%w: build.type=%q (supported: builtin, cmake, make, command, none)", + rocks.ErrUnsupportedRockspecFeature, spec.Build.Type) + } + + return nil +} diff --git a/lib/luarocks/tree/conflicts.go b/lib/luarocks/tree/conflicts.go new file mode 100644 index 000000000..33501694d --- /dev/null +++ b/lib/luarocks/tree/conflicts.go @@ -0,0 +1,65 @@ +package tree + +import ( + "fmt" + "os" + "strings" +) + +// resolveCollision implements upstream luarocks's filename-munging conflict +// resolution: when a deploy-target path is already occupied by a file from +// some prior install (typically a different version of the same rock), the +// new file is written under a munged name composed of the original +// basename and the new install's - string with `.` and `-` +// replaced by `_`. The previously-deployed file is left in place. +// +// Example: deploying metrics 1.5.0-1 to a tree that already holds +// metrics 2.0.0-1's `/share/tarantool/metrics/init.lua` produces +// `/share/tarantool/metrics/init.lua~metrics_1_5_0_1`. The active +// version's bytes stay at the plain path; the inactive sibling is the +// renamed copy. +// +// This is the v1 conservative form: we ALWAYS write inactive (i.e. the +// new install is treated as inactive whenever the target exists). The +// caller — eventually the install/upgrade policy — is responsible for +// promoting a version by swapping bytes between the plain and munged +// paths. (No symlinks; matches upstream Tarantool layout.) +// +// manifest.modules[] is a +// 1-indexed array where [1] is the active version's repo string. We do +// not write that here — the manifest update is the install pipeline's +// concern, on top of the rock_manifest we return from Deploy. +func resolveCollision(target, pkg, ver string) (string, error) { + if _, err := os.Stat(target); err != nil { + if os.IsNotExist(err) { + return target, nil + } + + return "", fmt.Errorf("stat %q: %w", target, err) + } + + suffix := mungedVersion(pkg, ver) + munged := target + "~" + suffix + + return munged, nil +} + +// mungedVersion returns the conflict-suffix form of "-" with +// every `.` and `-` replaced by `_`. This is the canonical munged +// identifier embedded in conflict filenames and used as keys in some +// manifest sub-tables. +func mungedVersion(pkg, ver string) string { + combined := pkg + "-" + ver + combined = strings.ReplaceAll(combined, ".", "_") + combined = strings.ReplaceAll(combined, "-", "_") + + return combined +} + +// MungedPath returns the conflict-suffixed sibling of base for the given +// (pkg, ver). Exported because installer logic — selecting the active +// version among munged siblings — needs the same string formation that +// Deploy used. +func MungedPath(base, pkg, ver string) string { + return base + "~" + mungedVersion(pkg, ver) +} diff --git a/lib/luarocks/tree/deploy.go b/lib/luarocks/tree/deploy.go new file mode 100644 index 000000000..8bb9ee7a7 --- /dev/null +++ b/lib/luarocks/tree/deploy.go @@ -0,0 +1,491 @@ +package tree + +import ( + "crypto/md5" + "encoding/hex" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + rocks "github.com/tarantool/tt/lib/luarocks" +) + +// Module kinds, matching the LuaRocks install layout: pure-Lua modules go +// under the "lua" tree, native libraries under the "lib" tree. +const ( + kindLua = "lua" + kindLib = "lib" +) + +// Deploy copies the artifacts of a built rock into the tree, producing +// the per-rock RockManifest (path → md5 hex) that the caller then writes +// to /rock_manifest via t.Store.WriteRock. +// +// Two source directories are consulted: +// - srcDir holds the original rockspec source tree (the output of +// fetch.Fetch). Deploy reads .lua modules, prebuilt .so/.dylib +// modules, install.{lua,lib,bin,conf} files, and copy_directories +// from srcDir at the rockspec-relative paths. +// - buildDir holds compiled artifacts produced by the build subsystem. +// Deploy reads .c-source-derived .so files and table-form module +// artifacts from buildDir at the canonical slashed path +// `.so`. +// +// For callers without a separate build phase (pure-.lua rocks, or unit +// tests where srcDir already contains every artifact), pass srcDir as +// both arguments — the lookup is layered and both forms work. +// +// What gets copied where: +// +// - build.modules: dotted module name → slashed path; .lua → DeployLuaDir, +// .so → DeployLibDir. +// - build.install.lua / .lib: the KEY is a dotted module name (upstream +// is_module_path=true) → module_to_path(key) subdir; a .lua source is +// renamed to .lua. See installDest. +// - build.install.conf: the KEY is a literal path joined under ConfDir. +// - build.install.bin: the KEY is a literal path. When BinWrap is set the +// real script lands in the per-rock /bin and BinDir gets an +// executable launcher (0o755); when nil the script is copied verbatim +// (0o755). +// - build.copy_directories: each entry is a directory under srcDir copied +// recursively into the per-rock install dir (only a "doc" dir is recorded +// in the rock_manifest; see copyTree). +// +// Collisions with previously-deployed versions are handled via munged +// filenames; see conflicts.go. +func (t *Tree) Deploy(spec *rocks.Rockspec, srcDir, buildDir string) (*rocks.RockManifest, error) { + if spec == nil { + return nil, errors.New("tree.Deploy: nil spec") + } + + if spec.Package == "" || spec.Version == "" { + return nil, errors.New("tree.Deploy: spec missing Package/Version") + } + + if buildDir == "" { + buildDir = srcDir + } + + rm := &rocks.RockManifest{ + Lua: map[string]string{}, + Lib: map[string]string{}, + Bin: map[string]string{}, + Conf: map[string]string{}, + Doc: map[string]string{}, + } + + // 1) build.modules + for modName, mod := range spec.Build.Modules { + srcPath, dstPath, kind, err := resolveModule(srcDir, buildDir, modName, mod, t.Paths) + if err != nil { + return nil, err + } + + dstPath, err = resolveCollision(dstPath, spec.Package, spec.Version) + if err != nil { + return nil, err + } + + sum, err := copyFile(srcPath, dstPath, filePerm) + if err != nil { + return nil, fmt.Errorf("tree.Deploy: module %q: %w", modName, err) + } + + rel, err := filepath.Rel(deployDirForKind(t.Paths, kind), dstPath) + if err != nil { + return nil, err + } + + rel = filepath.ToSlash(rel) + + switch kind { + case kindLua: + rm.Lua[rel] = sum + case kindLib: + rm.Lib[rel] = sum + } + } + + // 2) build.install.* + // + // Upstream's install_files (luarocks/build.lua) interprets the string KEYS + // of each section differently depending on the section (prepare_install_dirs + // sets is_module_path per section): + // + // - lua, lib → is_module_path=true: the key is a DOTTED MODULE NAME. The + // destination subdir is module_to_path(key) (dots→slashes, minus the last + // segment); a .lua source is renamed to .lua, anything else + // keeps the source basename. + // - bin, conf → is_module_path=false: the key is a literal slash path; the + // destination is dir_name(key)/base_name(key), which filepath.Join + // reproduces directly. + type instGroup struct { + entries map[string]string + dir string + mode os.FileMode + dst map[string]string + isModulePath bool + } + + groups := []instGroup{ + {spec.Build.Install.Lua, t.DeployLuaDir(), 0o644, rm.Lua, true}, + {spec.Build.Install.Lib, t.DeployLibDir(), 0o644, rm.Lib, true}, + {spec.Build.Install.Conf, t.ConfDir(), 0o644, rm.Conf, false}, + } + for _, g := range groups { + for key, srcRel := range g.entries { + src := filepath.Join(srcDir, srcRel) + dst := installDest(g.dir, key, srcRel, g.isModulePath) + + dst, err := resolveCollision(dst, spec.Package, spec.Version) + if err != nil { + return nil, err + } + + sum, err := copyFile(src, dst, g.mode) + if err != nil { + return nil, fmt.Errorf("tree.Deploy: install %q: %w", key, err) + } + + rel, err := filepath.Rel(g.dir, dst) + if err != nil { + return nil, err + } + + g.dst[filepath.ToSlash(rel)] = sum + } + } + + // 2b) build.install.bin — is_module_path=false, like conf, but upstream + // additionally deploys the public /bin entry as a launcher + // (repos.deploy_files with should_wrap_bin_scripts → fs.wrap_script). When + // t.BinWrap is set we reproduce that two-step layout: the real script lands + // in the per-rock /bin and /bin gets the wrapper. When + // unset we copy verbatim (legacy behavior). + installDir := t.InstallDir(spec.Package, spec.Version) + + for key, srcRel := range spec.Build.Install.Bin { + src := filepath.Join(srcDir, srcRel) + rel := filepath.Clean(key) // is_module_path=false: dir_name/base_name == the key path + + if t.BinWrap == nil { + dst, err := resolveCollision(filepath.Join(t.BinDir(), rel), spec.Package, spec.Version) + if err != nil { + return nil, err + } + + sum, err := copyFile(src, dst, execPerm) + if err != nil { + return nil, fmt.Errorf("tree.Deploy: install bin %q: %w", key, err) + } + + r, err := filepath.Rel(t.BinDir(), dst) + if err != nil { + return nil, err + } + + rm.Bin[filepath.ToSlash(r)] = sum + + continue + } + + // Wrapped: real script → per-rock /bin/. + realScript := filepath.Join(installDir, "bin", rel) + + sum, err := copyFile(src, realScript, execPerm) + if err != nil { + return nil, fmt.Errorf("tree.Deploy: install bin %q: %w", key, err) + } + + rm.Bin[filepath.ToSlash(filepath.Join("bin", rel))] = sum + + // Public launcher → /bin/, execing the interpreter on the + // real script (matches fs/unix.lua wrap_script byte-for-byte). + wrapperDst, err := resolveCollision(filepath.Join(t.BinDir(), rel), spec.Package, spec.Version) + if err != nil { + return nil, err + } + + if err := os.MkdirAll(filepath.Dir(wrapperDst), dirPerm); err != nil { + return nil, fmt.Errorf("tree.Deploy: mkdir bin %q: %w", key, err) + } + + body := t.binWrapperScript(realScript, spec.Package, spec.Version) + if err := os.WriteFile(wrapperDst, []byte(body), execPerm); err != nil { + return nil, fmt.Errorf("tree.Deploy: write bin wrapper %q: %w", key, err) + } + } + + // 3) build.copy_directories — recursive copy into the per-rock install dir. + + for _, dir := range spec.Build.CopyDirectories { + src := filepath.Join(srcDir, dir) + + dst := filepath.Join(installDir, dir) + + err := copyTree(src, dst, rm, dir) + if err != nil { + return nil, fmt.Errorf("tree.Deploy: copy_directories[%q]: %w", dir, err) + } + } + + return rm, nil +} + +// resolveModule maps a single build.modules entry to (srcPath, dstPath, kind). +// kind is "lua" or "lib"; the caller writes into rm.Lua / rm.Lib accordingly. +// +// Source-directory layering: +// - srcDir for .lua module paths and prebuilt .so/.dylib paths from the +// rockspec (these live in the original source tree). +// - buildDir for compiled artifacts the build subsystem produced +// (C-source modules and table-form modules emit `.so` into +// buildDir). +// +// Dotted module names become slashed paths; the last segment becomes the +// filename. So "foo.bar.baz" with a .lua source becomes "foo/bar/baz.lua" +// under DeployLuaDir, and a .so module of the same name becomes +// "foo/bar/baz.so" under DeployLibDir. +func resolveModule(srcDir, buildDir, modName string, mod rocks.Module, p Paths) (src, dst, kind string, err error) { + slashName := strings.ReplaceAll(modName, ".", string(filepath.Separator)) + + switch { + case mod.Path != "": + ext := strings.ToLower(filepath.Ext(mod.Path)) + switch ext { + case ".lua": + src = filepath.Join(srcDir, mod.Path) + dst = filepath.Join(p.DeployLuaDir(), slashName+".lua") + kind = kindLua + case ".so", ".dylib": + // Tarantool/upstream luarocks deploy compiled modules with the + // canonical platform suffix `.so`. Sources with `.dylib` on + // macOS are renamed at deploy time to `.so` to match upstream's + // install_files behavior. Prebuilt artifacts live in srcDir. + src = filepath.Join(srcDir, mod.Path) + dst = filepath.Join(p.DeployLibDir(), slashName+".so") + kind = kindLib + case ".c", ".cpp", ".cxx", ".cc": + // A C source listed as Path implies the build step compiled it + // to buildDir/lib/.so. The "lib/" prefix matches + // build/builtin's compileModule output layout. + src = filepath.Join(buildDir, kindLib, slashName+".so") + dst = filepath.Join(p.DeployLibDir(), slashName+".so") + kind = kindLib + default: + err = fmt.Errorf("module %q: unsupported source extension %q", modName, ext) + + return src, dst, kind, err + } + case len(mod.Sources) > 0: + // Table-form: the build subsystem produced the .so under + // buildDir/lib/.so (matching compileModule output). + src = filepath.Join(buildDir, kindLib, slashName+".so") + dst = filepath.Join(p.DeployLibDir(), slashName+".so") + kind = kindLib + default: + err = fmt.Errorf("module %q: neither Path nor Sources set", modName) + } + + return src, dst, kind, err +} + +// installDest computes the on-disk destination for one build.install.
+// entry, mirroring upstream's install_to. For module-path sections (lua, lib) +// the key is a dotted module name: the subdir is module_to_path(key) and a .lua +// source is renamed to .lua. For literal sections (bin, conf) the +// key is a slash path joined directly under dir. +func installDest(dir, key, srcRel string, isModulePath bool) string { + if !isModulePath { + return filepath.Join(dir, key) + } + + sub := moduleToPathDir(key) + + filename := filepath.Base(srcRel) + if strings.HasSuffix(strings.ToLower(filename), ".lua") { + filename = moduleLastSegment(key) + ".lua" + } + + return filepath.Join(dir, filepath.FromSlash(sub), filename) +} + +// moduleToPathDir mirrors upstream luarocks path.module_to_path: it drops the +// last dot-delimited segment of a module name and converts the remaining dots +// to forward slashes, yielding the destination SUBDIRECTORY (slash-delimited, +// possibly empty). E.g. "withinstall.helper" → "withinstall/", "a.b.c" → +// "a/b/", "foo" → "". +func moduleToPathDir(mod string) string { + if i := strings.LastIndex(mod, "."); i >= 0 { + mod = mod[:i+1] // keep through the final dot, drop the last segment + } else { + mod = "" // no dot: the whole string is the last segment, removed + } + + return strings.ReplaceAll(mod, ".", "/") +} + +// moduleLastSegment returns the final dot-delimited segment of a module name +// (upstream's `modname:match("([^.]+)$")`). E.g. "withinstall.helper" → +// "helper", "foo" → "foo". +func moduleLastSegment(mod string) string { + if i := strings.LastIndex(mod, "."); i >= 0 { + return mod[i+1:] + } + + return mod +} + +// binWrapperScript reproduces fs/unix.lua wrap_script byte-for-byte for the +// single-tree deps case: a /bin/sh launcher that sets package.path/cpath to the +// tree's deploy dirs, attempts to load luarocks.loader (guarded — a native tree +// has none, the pcall absorbs it), registers the rock context, then execs the +// interpreter on the real script. Path entries mirror path.package_paths: +// "/?.lua;/?/init.lua" and "/?.so". +func (t *Tree) binWrapperScript(realScript, name, version string) string { + lua := filepath.ToSlash(t.DeployLuaDir()) + lib := filepath.ToSlash(t.DeployLibDir()) + lpath := lua + "/?.lua;" + lua + "/?/init.lua" + lcpath := lib + "/?.so" + + luainit := "package.path=" + luaQuote(lpath+";") + "..package.path" + + ";package.cpath=" + luaQuote(lcpath+";") + "..package.cpath" + + ";local k,l,_=pcall(require," + luaQuote("luarocks.loader") + ") _=k and l.add_context(" + + luaQuote(name) + "," + luaQuote(version) + ")" + + return "#!/bin/sh\n\nLUAROCKS_SYSCONFDIR=" + shellQuote(t.BinWrap.Sysconfdir) + + " exec " + shellQuote(t.BinWrap.Interpreter) + + " -e " + shellQuote(luainit) + + " " + shellQuote(realScript) + ` "$@"` + "\n" +} + +// luaQuote mirrors util.LQ — Lua's string.format("%q", s). For the wrapper's +// path/identifier inputs (no embedded quotes, backslashes, or newlines) this is +// a plain double-quoted string; the escapes below keep it faithful regardless. +func luaQuote(s string) string { + var b strings.Builder + + b.WriteByte('"') + + for i := range len(s) { + switch c := s[i]; c { + case '"': + b.WriteString(`\"`) + case '\\': + b.WriteString(`\\`) + case '\n': + b.WriteString(`\n`) + case '\r': + b.WriteString(`\r`) + case 0: + b.WriteString(`\0`) + default: + b.WriteByte(c) + } + } + + b.WriteByte('"') + + return b.String() +} + +// shellQuote mirrors fs/unix.lua unix.Q: wrap in single quotes, escaping any +// embedded single quote as '\”. +func shellQuote(s string) string { + return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" +} + +func deployDirForKind(p Paths, kind string) string { + switch kind { + case kindLua: + return p.DeployLuaDir() + case kindLib: + return p.DeployLibDir() + } + + return p.Tree +} + +// copyFile writes src → dst with mode, mkdir-p the parent, and returns +// the md5 hex of the bytes written. +func copyFile(src, dst string, mode os.FileMode) (string, error) { + if err := os.MkdirAll(filepath.Dir(dst), dirPerm); err != nil { + return "", fmt.Errorf("mkdir %q: %w", filepath.Dir(dst), err) + } + + in, err := os.Open(src) + if err != nil { + return "", fmt.Errorf("open %q: %w", src, err) + } + + defer func() { _ = in.Close() }() + + out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, mode) + if err != nil { + return "", fmt.Errorf("create %q: %w", dst, err) + } + + h := md5.New() + + w := io.MultiWriter(out, h) + if _, err := io.Copy(w, in); err != nil { + _ = out.Close() + + return "", fmt.Errorf("copy %q->%q: %w", src, dst, err) + } + + if err := out.Close(); err != nil { + return "", fmt.Errorf("close %q: %w", dst, err) + } + + return hex.EncodeToString(h.Sum(nil)), nil +} + +// copyTree recursively copies src → dst, computing an md5 for every file. +// Only files under a "doc" top dir are recorded — into rm.Doc, keyed by the +// rock-root-relative path so the manifest key matches the on-disk layout. +// Files from any other copy_directories entry are written to disk but NOT +// recorded in the rock_manifest: they don't belong to the lua/lib/bin/conf +// buckets, and keeping the manifest closed over those known buckets is +// simpler than inventing a category for arbitrary copied content. +func copyTree(src, dst string, rm *rocks.RockManifest, topName string) error { + st, err := os.Stat(src) + if err != nil { + return fmt.Errorf("stat %q: %w", src, err) + } + + if !st.IsDir() { + return fmt.Errorf("%q is not a directory", src) + } + + return filepath.Walk(src, func(p string, info os.FileInfo, werr error) error { + if werr != nil { + return werr + } + + rel, err := filepath.Rel(src, p) + if err != nil { + return err + } + + target := filepath.Join(dst, rel) + if info.IsDir() { + return os.MkdirAll(target, dirPerm) + } + + sum, err := copyFile(p, target, info.Mode().Perm()) + if err != nil { + return err + } + + if topName == "doc" { + key := filepath.ToSlash(rel) + rm.Doc[key] = sum + } + + return nil + }) +} diff --git a/lib/luarocks/tree/paths.go b/lib/luarocks/tree/paths.go new file mode 100644 index 000000000..f0c8809a9 --- /dev/null +++ b/lib/luarocks/tree/paths.go @@ -0,0 +1,68 @@ +// Package tree implements the on-disk Tarantool-rocks layout: the path +// scheme under /share/tarantool, /lib/tarantool, /bin, +// and the Deploy operation that copies a built rock's artifacts into it. +// +// The layout is fixed for Tarantool: +// +// /share/tarantool/rocks/// — per-rock install dir +// /share/tarantool/ — deploy_lua_dir +// /lib/tarantool/ — deploy_lib_dir +// /bin/ — bin scripts +// /etc/ — conf files +// +// This package uses forward-slash Unix paths exclusively via +// path/filepath; no Windows-specific handling. +package tree + +import "path/filepath" + +// Paths computes the conventional subdirectories under a tree root. +// +// All methods are pure functions of Tree; they do not touch disk. The +// caller is responsible for ensuring directories exist (Tree.Open does +// the minimal mkdir). +type Paths struct { + // Tree is the tree root (e.g. ".rocks" in a project, or a system + // tarantool prefix). Always an absolute or project-relative path. + Tree string +} + +// RocksDir returns /share/tarantool/rocks — the rocks_dir upstream +// luarocks refers to. +func (p Paths) RocksDir() string { + return filepath.Join(p.Tree, "share", "tarantool", "rocks") +} + +// InstallDir returns /share/tarantool/rocks// — the +// per-rock install directory holding rockspec, rock_manifest, build/, doc/. +func (p Paths) InstallDir(name, ver string) string { + return filepath.Join(p.RocksDir(), name, ver) +} + +// DeployLuaDir returns /share/tarantool — where dotted .lua modules +// are flattened to slashed paths during Deploy. +func (p Paths) DeployLuaDir() string { + return filepath.Join(p.Tree, "share", "tarantool") +} + +// DeployLibDir returns /lib/tarantool — where compiled .so modules +// are flattened to slashed paths during Deploy. +func (p Paths) DeployLibDir() string { + return filepath.Join(p.Tree, "lib", "tarantool") +} + +// BinDir returns /bin — destination for build.install.bin entries. +func (p Paths) BinDir() string { + return filepath.Join(p.Tree, "bin") +} + +// ConfDir returns /etc — destination for build.install.conf entries. +func (p Paths) ConfDir() string { + return filepath.Join(p.Tree, "etc") +} + +// DocDir returns /share/tarantool/rocks///doc — the +// per-rock documentation directory. +func (p Paths) DocDir(name, ver string) string { + return filepath.Join(p.InstallDir(name, ver), "doc") +} diff --git a/lib/luarocks/tree/tree.go b/lib/luarocks/tree/tree.go new file mode 100644 index 000000000..3cabe8394 --- /dev/null +++ b/lib/luarocks/tree/tree.go @@ -0,0 +1,78 @@ +package tree + +import ( + "errors" + "fmt" + "os" + + rocks "github.com/tarantool/tt/lib/luarocks" + "github.com/tarantool/tt/lib/luarocks/manif" +) + +// File and directory permission bits used across the tree package when +// materializing deployed rocks. +const ( + // dirPerm is applied to directories created in the rock tree. + dirPerm os.FileMode = 0o750 + // filePerm is the default mode for regular deployed files (non-executable). + filePerm os.FileMode = 0o644 + // execPerm is the mode for executable deployed files: install.bin scripts + // and the generated command wrappers. + execPerm os.FileMode = 0o755 +) + +// Tree is the in-memory handle on a Tarantool rocks tree on disk. It +// composes the path scheme (Paths) and a ManifestStore. Operations +// that mutate the tree — Deploy chiefly — go through this type. +type Tree struct { + Paths + + // Store reads/writes the tree-level and per-rock manifest files. The + // default is manif.FileStore. Tests inject fakes. + Store rocks.ManifestStore + + // BinWrap, when non-nil, makes Deploy reproduce upstream LuaRocks' + // command-script handling (repos.deploy_files → fs.wrap_script): the real + // script is installed under the per-rock dir and the public /bin entry + // becomes a generated /bin/sh launcher. When nil, build.install.bin entries + // are copied verbatim (the pre-parity behavior; kept for callers that + // construct a Tree without a Tarantool target). + BinWrap *BinWrap +} + +// BinWrap carries the two environment-dependent inputs upstream bakes into a +// command wrapper (fs/unix.lua wrap_script): the absolute interpreter path +// (cfg.variables.LUA_BINDIR + cfg.lua_interpreter) and cfg.sysconfdir. +type BinWrap struct { + // Interpreter is the absolute path to the Lua interpreter the wrapper + // execs, e.g. "/opt/tarantool/bin/tarantool". + Interpreter string + // Sysconfdir is the value exported as LUAROCKS_SYSCONFDIR, matching + // upstream's cfg.sysconfdir (default "/etc/luarocks"). + Sysconfdir string +} + +// Open returns a Tree handle backed by cfg.Tree on disk. The minimal set +// of subdirectories — RocksDir, DeployLuaDir, DeployLibDir, BinDir — are +// created if missing (the tree-init step is idempotent). +// +// cfg.Tarantool fields are not consulted: tree management is independent +// of the Tarantool target (which only matters at build time). +func Open(cfg rocks.Config) (*Tree, error) { + if cfg.Tree == "" { + return nil, errors.New("tree.Open: Config.Tree is empty") + } + + t := &Tree{ + Paths: Paths{Tree: cfg.Tree}, + Store: manif.FileStore{}, + } + for _, d := range []string{t.RocksDir(), t.DeployLuaDir(), t.DeployLibDir(), t.BinDir()} { + err := os.MkdirAll(d, dirPerm) + if err != nil { + return nil, fmt.Errorf("tree.Open: mkdir %q: %w", d, err) + } + } + + return t, nil +} diff --git a/lib/luarocks/tree/which.go b/lib/luarocks/tree/which.go new file mode 100644 index 000000000..f990e980b --- /dev/null +++ b/lib/luarocks/tree/which.go @@ -0,0 +1,38 @@ +package tree + +import ( + "os" + "path/filepath" + "strings" +) + +// Which resolves a dotted Lua module name to the on-disk file that would +// satisfy `require("")` against this tree. +// +// Search order matches upstream `package.path` then `package.cpath`: +// +// 1. DeployLuaDir/.lua +// 2. DeployLuaDir//init.lua +// 3. DeployLibDir/.so +// +// Returns ("", false) when none of the above exist on disk. +// +// Conflict-munged siblings (`.../foo.lua~name_version`) are NOT considered +// — only the active (plain) path. That mirrors how Tarantool's `require` +// works against the rocks tree. +func (t *Tree) Which(module string) (string, bool) { + slashed := strings.ReplaceAll(module, ".", string(filepath.Separator)) + + candidates := []string{ + filepath.Join(t.DeployLuaDir(), slashed+".lua"), + filepath.Join(t.DeployLuaDir(), slashed, "init.lua"), + filepath.Join(t.DeployLibDir(), slashed+".so"), + } + for _, c := range candidates { + if st, err := os.Stat(c); err == nil && !st.IsDir() { + return c, true + } + } + + return "", false +} diff --git a/lib/luarocks/types.go b/lib/luarocks/types.go new file mode 100644 index 000000000..eb6ead09c --- /dev/null +++ b/lib/luarocks/types.go @@ -0,0 +1,235 @@ +package rocks + +// Field-only data types. Methods belong in the subsystem that owns each +// type's behavior (manif, rockspec, deps, …). Nothing here imports +// gopher-lua; the rockspec evaluator harvests a *Rockspec by populating +// these plain Go fields. + +// Rockspec is the parsed and validated contents of a `.rockspec` file. +// `name-version-revision.rockspec` filenames serialize Package, Version +// joined by `-`. +type Rockspec struct { + // Package is the rock name. + Package string + // Version is the version-revision string (e.g. "1.0-1"). + Version string + // RockspecFormat is the declared rockspec_format (e.g. "3.0"), if any. + RockspecFormat string + // Description mirrors the rockspec description table. + Description Description + // SupportedPlatforms restricts the rock to the listed platforms, if set. + SupportedPlatforms []string + // Dependencies are the runtime dependencies. + Dependencies []Dep + // BuildDependencies are needed only to build the rock. + BuildDependencies []Dep + // ExternalDependencies are non-Lua libraries/headers, keyed by symbolic name. + ExternalDependencies map[string]ExternalDep + // TestDependencies are needed only to run the rock's tests. + TestDependencies []Dep + // Source describes where the rock's source is fetched from. + Source Source + // Build describes how the rock is built and installed. + Build Build +} + +// Description mirrors the rockspec `description = {...}` table. +type Description struct { + Summary string + Detailed string + License string + Homepage string + IssuesURL string + Maintainer string + Labels []string +} + +// Source mirrors the rockspec `source = {...}` table. +type Source struct { + // URL drives Fetcher selection (http/git/file). + URL string + // Tag is the git tag to check out (git-only). + Tag string + // Branch is the git branch to check out (git-only). + Branch string + // MD5 is the expected archive checksum, verified for http downloads. + MD5 string + // File overrides the downloaded archive's basename (else derived from URL). + File string + // Dir overrides the directory the archive is expected to unpack into. + Dir string + // Module is the legacy SCM module name (cvs/svn); rarely used. + Module string +} + +// Build mirrors the rockspec `build = {...}` table. +// +// Type ∈ {"builtin","cmake","make","command","none"}. Anything else is +// ErrUnsupportedRockspecFeature. +// +// Platforms contains per-platform Build overrides that the platforms-merge +// step (rockspec/platforms.go) folds into the top-level fields least- +// specific-to-most-specific. After Eval, Platforms is empty. +type Build struct { + Type string + Modules map[string]Module + Install BuildInstall + + CopyDirectories []string + + // Variables — cmake -D pass-through (cmake backend only). + Variables map[string]string + + // make backend. + BuildTarget string + BuildVariables map[string]string + InstallTarget string + InstallVariables map[string]string + + // command backend. + BuildCommand string + InstallCommand string + + Platforms map[string]Build +} + +// Module is a single entry in the rockspec `build.modules` table. +// +// String-form module `["foo.bar"] = "src/foo/bar.lua"`: Path != "". +// Table-form module `["foo.bar"] = {sources = {...}, ...}`: Sources non-empty. +type Module struct { + Path string + Sources []string + Incdirs []string + Libdirs []string + Libraries []string + Defines []string +} + +// BuildInstall mirrors the rockspec `build.install = {...}` table. +// Each map is destination-name → source-path-relative-to-rockspec-dir. +type BuildInstall struct { + Lua map[string]string + Lib map[string]string + Bin map[string]string + Conf map[string]string +} + +// Dep is one entry in the rockspec `dependencies` (or build_dependencies) +// list. Name is the dependency rock name; Constraints is the AND'd set of +// version constraints from "foo >= 1, < 2". +type Dep struct { + Name string + Constraints []VersionConstraint +} + +// ExternalDep is one entry in the rockspec `external_dependencies` table: +// Header is e.g. "tarantool/module.h"; Library is e.g. "tarantool". +type ExternalDep struct { + Header string + Library string +} + +// Version is a parsed rock version. +// +// Components holds the numeric dot-separated parts ("1.2.3" → [1, 2, 3]). +// Revision is the trailing "-N" (defaults to 0 if absent). +// IsSCM is true for "scm-N"; IsDev is true for "dev-N". Both sort ABOVE +// numeric releases per upstream `core/vers.compare_versions`. +// Raw is the original string for round-trip serialization (e.g. into +// rock_manifest filenames). +type Version struct { + Raw string + Components []int + Revision int + IsSCM bool + IsDev bool +} + +// VersionConstraint is one operator+version pair from a constraint +// expression. Op ∈ {"==", "~=", ">", "<", ">=", "<=", "~>"}. +type VersionConstraint struct { + Op string + Version Version +} + +// Manifest is the top-level tree manifest at `/manifest`. The +// upstream format is a Lua-source serialization of a table. +type Manifest struct { + // Repository indexes installed rocks: name → version → per-arch entry. + Repository map[string]map[string]RepoEntry + // Modules maps a module name to the "name/version" rocks that provide it. + Modules map[string][]string + // Commands maps a command-binary name to the "name/version" rocks that + // provide it. + Commands map[string][]string + // Dependencies records each rock's resolved deps: name → version → deps. + Dependencies map[string]map[string][]Dep +} + +// RepoEntry is one entry under `repository..`. Arch is +// typically "installed" for a Tarantool tree. +// +// Modules and Commands carry the per-arch index that upstream luarocks +// emits inside each repository entry: module name → on-disk slashed path +// for Modules, command-binary name → relative install path for Commands. +// Populated by the facade after `tree.Deploy` returns the *RockManifest* +// for the installed rock. Empty for a freshly-installed entry only if the +// rock genuinely has no modules / commands. +type RepoEntry struct { + Arch string + Modules map[string]string + Commands map[string]string +} + +// VersionedRock is one row from a RemoteIndex.Query result: a (name, +// version) tuple and the URL of the resource (.rock or .rockspec) on the +// originating server. Spec, if non-nil, is the preloaded rockspec for +// that version — the resolver uses it instead of re-fetching when present. +type VersionedRock struct { + Name string + Version Version + URL string + Spec *Rockspec +} + +// InstallStep is one entry in the topo-ordered install list produced by +// deps.Resolve. Steps appear deepest-dep-first: a step's prerequisites +// all precede it. The facade's Install method walks this list in order. +type InstallStep struct { + Name string + Version Version + URL string + Rockspec *Rockspec +} + +// InstalledRock is one row of the result returned by Rocks.List. Version +// is the raw display string matching what `luarocks list` would print. +type InstalledRock struct { + Name string + Version string +} + +// ShowInfo is the projection returned by Rocks.Show — the human-readable +// summary upstream's `luarocks show` prints. +type ShowInfo struct { + Package string + Version string + Summary string + License string + Homepage string + Modules []string + Dependencies []Dep +} + +// RockManifest is the per-rock manifest at +// `/share/tarantool/rocks///rock_manifest`. +// Each map is path → md5 hex. +type RockManifest struct { + Rockspec string + Lua map[string]string + Lib map[string]string + Bin map[string]string + Conf map[string]string + Doc map[string]string +} diff --git a/test/integration/aeon/server/go.mod b/test/integration/aeon/server/go.mod index 6489d108a..e6ff4f7c5 100644 --- a/test/integration/aeon/server/go.mod +++ b/test/integration/aeon/server/go.mod @@ -8,8 +8,8 @@ require ( ) require ( - golang.org/x/net v0.52.0 // indirect - golang.org/x/sys v0.42.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.36.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260511170946-3700d4141b60 // indirect google.golang.org/protobuf v1.36.11 // indirect diff --git a/test/integration/aeon/server/go.sum b/test/integration/aeon/server/go.sum index b4268fd6a..333e3ba3e 100644 --- a/test/integration/aeon/server/go.sum +++ b/test/integration/aeon/server/go.sum @@ -22,10 +22,10 @@ go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2W go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=

LuaRocks