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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/lpm-core/lpm.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"http": { "path": "../http" },
"semver": { "path": "../semver" },
"lpm-test": { "path": "../lpm-test" },
"git": { "path": "../git" }
"git": { "path": "../git" },
"rocked": { "path": "../rocked" }
}
}
1 change: 1 addition & 0 deletions packages/lpm-core/src/config.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ end

---@class lpm.Config.BaseDependency
---@field name string? # The actual package name in the registry, when aliasing
---@field rockspec string? # Path to the rockspec file, relative to the dependency directory

---@class lpm.Config.GitDependency: lpm.Config.BaseDependency
---@field git string
Expand Down
7 changes: 2 additions & 5 deletions packages/lpm-core/src/package/build.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@ local function buildPackage(package, destinationPath)
fs.mkdir(target)
end

local buildScriptPath = package:getBuildScriptPath()
if fs.exists(buildScriptPath) then
fs.copy(package:getSrcDir(), destinationPath)

local ok, err = package:runFile(buildScriptPath, nil, { LPM_OUTPUT_DIR = destinationPath })
if package:hasBuildScript() then
local ok, err = package:runBuildScript(destinationPath)
if not ok then
error("Build script failed for package '" .. package:getName() .. "': " .. err)
end
Expand Down
170 changes: 169 additions & 1 deletion packages/lpm-core/src/package/init.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
local Config = require("lpm-core.config")
local Lockfile = require("lpm-core.lockfile")
local rocked = require("rocked")
local sea = require("sea")

local global = require("lpm-core.global")

Expand All @@ -13,6 +15,7 @@ local process = require("process")
---@field dir string
---@field cachedConfig lpm.Config?
---@field cachedConfigMtime number?
---@field buildfn (fun(pkg: lpm.Package, outputDir: string): boolean, string?)?
local Package = {}
Package.__index = Package

Expand Down Expand Up @@ -42,9 +45,33 @@ function Package:getConfigPath() return configPathAtDir(self.dir) end

function Package:getLockfilePath() return path.join(self.dir, "lpm-lock.json") end

---@param pkg lpm.Package
---@param outputDir string
local function defaultBuildFn(pkg, outputDir)
fs.copy(pkg:getSrcDir(), outputDir)

local buildScriptPath = pkg:getBuildScriptPath()
if not fs.exists(buildScriptPath) then
return nil, "No build script found: " .. buildScriptPath
end

return pkg:runFile(buildScriptPath, nil, { LPM_OUTPUT_DIR = outputDir })
end

function Package:hasBuildScript()
return self.buildfn ~= nil or fs.exists(self:getBuildScriptPath())
end

---@param outputDir string
---@return boolean? ok
---@return string? err
function Package:runBuildScript(outputDir)
return (self.buildfn or defaultBuildFn)(self, outputDir)
end

---@param dir string?
---@return lpm.Package?, string?
function Package.open(dir)
function Package.openLPM(dir)
dir = dir or env.cwd()

local configPath = configPathAtDir(dir)
Expand All @@ -55,6 +82,147 @@ function Package.open(dir)
return setmetatable({ dir = dir }, Package), nil
end

---@param dir string?
---@param rockspecPath string? # Path to the rockspec file; if nil, scanned from dir
---@return lpm.Package?, string?
function Package.openRockspec(dir, rockspecPath)
dir = dir or env.cwd()

if not rockspecPath then
if fs.isdir(dir) then
for _, entry in ipairs(fs.scan(dir, "**.rockspec")) do
rockspecPath = path.join(dir, entry)
break
end
end
elseif not path.isAbsolute(rockspecPath) then
rockspecPath = path.join(dir, rockspecPath)
end

if not rockspecPath then
return nil, "No rockspec found in directory: " .. dir
end

local content = fs.read(rockspecPath)
if not content then
return nil, "Could not read rockspec: " .. rockspecPath
end

local ok, spec = rocked.parse(content)
if not ok then
return nil, "Failed to parse rockspec: " .. (spec or rockspecPath)
end ---@cast spec rocked.raw.Output

local pkg = setmetatable({ dir = dir }, Package)

-- Collect pure-Lua module_name -> src_path from build.modules and build.install.lua
local modules = {}
local nativeModules = {} -- modname -> src_path (.c)
if spec.build then
for modname, src in pairs(spec.build.modules or {}) do
if type(src) == "string" then
if src:match("%.lua$") then
modules[modname] = src
elseif src:match("%.c$") then
nativeModules[modname] = src
end
end
end
for modname, src in pairs((spec.build.install or {}).lua or {}) do
modules[modname] = src
end
end

local entryModule = spec.package and spec.package:lower()

pkg.buildfn = function(_, outputDir)
local modulesDir = path.dirname(outputDir)

for modname, src in pairs(modules) do
local srcAbs = path.join(dir, src)
local destRel = modname:gsub("%.", path.separator) .. ".lua"
-- Mangle if this would collide with the generated init.lua
if path.join(modulesDir, destRel) == path.join(outputDir, "init.lua") then
destRel = modname:gsub("%.", path.separator):gsub("init$", "__init") .. ".lua"
end
local destAbs = path.join(modulesDir, destRel)
local destDir = path.dirname(destAbs)
if not fs.isdir(destDir) then
fs.mkdir(destDir)
end
fs.copy(srcAbs, destAbs)
modules[modname] = { destRel = destRel, destAbs = destAbs }
end

for modname, src in pairs(nativeModules) do
local srcAbs = path.join(dir, src)
local ext = process.platform == "darwin" and "dylib" or "so"
local destRel = modname:gsub("%.", path.separator) .. "." .. ext
local destAbs = path.join(modulesDir, destRel)
local destDir = path.dirname(destAbs)
if not fs.isdir(destDir) then
fs.mkdir(destDir)
end

local ok, err = process.exec("gcc", {
"-shared", "-fPIC",
"-I" .. path.join(sea.getLuajitPath(), "include"),
srcAbs,
"-o", destAbs,
})
if not ok then
return nil, "Failed to compile native module '" .. modname .. "': " .. (err or "")
end
end

local lines = {
"local _dir = debug.getinfo(1,'S').source:sub(2):match('^(.*/)') or './'",
}
for modname, info in pairs(modules) do
table.insert(lines, string.format(
"package.preload[%q] = package.preload[%q] or function() return dofile(_dir .. %q) end",
modname, modname, "../" .. info.destRel
))
end
if entryModule then
local info = modules[entryModule] or modules[entryModule .. ".init"]
if info then
table.insert(lines, string.format("return dofile(_dir .. %q)", "../" .. info.destRel))
elseif nativeModules[entryModule] then
local ext = process.platform == "darwin" and "dylib" or "so"
table.insert(lines, string.format(
"return package.loadlib(_dir .. %q, %q)()",
"../" .. entryModule:gsub("%.", path.separator) .. "." .. ext,
"luaopen_" .. entryModule:gsub("%.", "_")
))
end
end

fs.write(path.join(outputDir, "init.lua"), table.concat(lines, "\n") .. "\n")

return true
end

pkg.readConfig = function()
return Config.new({ name = spec.package, version = spec.version })
end

return pkg, nil
end

---@param dir string?
---@param rockspec string? # Path to rockspec, forwarded to openRockspec if no lpm.json
---@return lpm.Package?, string?
function Package.open(dir, rockspec)
dir = dir or env.cwd()

if fs.exists(configPathAtDir(dir)) then
return Package.openLPM(dir)
end

return Package.openRockspec(dir, rockspec)
end

---@return lpm.Config
function Package:readConfig()
local configPath = self:getConfigPath()
Expand Down
6 changes: 3 additions & 3 deletions packages/lpm-core/src/package/install.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ local function dependencyToPackage(alias, depInfo, relativeTo)
---@type lpm.Lockfile.GitDependency
local lockEntry = { git = depInfo.git, commit = resolvedCommit, branch = depInfo.branch, name = depInfo.name }

local pkg = Package.open(repoDir)
local pkg = Package.open(repoDir, depInfo.rockspec)
if pkg and pkg:getName() == packageName then return pkg, lockEntry end

for _, config in ipairs(fs.scan(repoDir, "**" .. path.separator .. "lpm.json")) do
Expand All @@ -35,7 +35,7 @@ local function dependencyToPackage(alias, depInfo, relativeTo)

error("No lpm.json with name '" .. packageName .. "' found in git repository")
elseif depInfo.path then
local localPackage, err = Package.open(path.resolve(relativeTo, path.normalize(depInfo.path)))
local localPackage, err = Package.open(path.resolve(relativeTo, path.normalize(depInfo.path)), depInfo.rockspec)
if not localPackage then
error("Failed to load local dependency package for: " .. alias .. "\nError: " .. err)
end
Expand All @@ -54,7 +54,7 @@ local function dependencyToPackage(alias, depInfo, relativeTo)
---@type lpm.Lockfile.GitDependency
local lockEntry = { git = portfile.git, commit = commit, branch = portfile.branch, name = depInfo.name }

local pkg = Package.open(repoDir)
local pkg = Package.open(repoDir, depInfo.rockspec)
if pkg and pkg:getName() == packageName then return pkg, lockEntry end

for _, config in ipairs(fs.scan(repoDir, "**" .. path.separator .. "lpm.json")) do
Expand Down
8 changes: 8 additions & 0 deletions packages/lpm/lpm.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
"package": {
"type": "string",
"description": "Actual package name at the path, when aliasing (key is the require name)"
},
"rockspec": {
"type": "string",
"description": "Path to the rockspec file relative to the dependency directory"
}
},
"additionalProperties": false
Expand All @@ -40,6 +44,10 @@
"package": {
"type": "string",
"description": "Actual package name in the repository, when aliasing (key is the require name)"
},
"rockspec": {
"type": "string",
"description": "Path to the rockspec file relative to the dependency directory"
}
},
"additionalProperties": false
Expand Down
2 changes: 1 addition & 1 deletion packages/lpm/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ if os.getenv("BOOTSTRAP") then

local pathPackages = {
"ansi", "clap", "fs", "http", "env", "path", "json", "git",
"process", "sea", "semver", "util", "lpm-core", "lpm-test"
"process", "sea", "semver", "util", "lpm-core", "lpm-test", "rocked"
}

for _, pkg in ipairs(pathPackages) do
Expand Down
67 changes: 67 additions & 0 deletions packages/lpm/tests/git.lua
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,70 @@ test.it("installDependencies respects a pinned commit in lpm.json", function()
local fixturePath = path.join(dir, "target", FIXTURE_NAME, "init.lua")
test.truthy(fs.exists(fixturePath))
end)

test.it("rockspec git dep: middleclass can be required after install", function()
local dir = path.join(tmpBase, "middleclass-consumer")
fs.mkdir(dir)
fs.mkdir(path.join(dir, "src"))
fs.write(path.join(dir, "src", "init.lua"), [[
local class = require("middleclass")
local Animal = class("Animal")
function Animal:initialize(name) self.name = name end
local a = Animal("cat")
assert(a.name == "cat", "expected name 'cat', got " .. tostring(a.name))
]])
fs.write(path.join(dir, "lpm.json"), json.encode({
name = "middleclass-consumer",
version = "0.1.0",
dependencies = {
middleclass = {
git = "https://github.com/kikito/middleclass",
branch = "master",
rockspec = "rockspecs/middleclass-4.1.1-0.rockspec"
}
}
}))

local pkg = lpm.Package.open(dir)
pkg:installDependencies()
pkg:build()

test.truthy(fs.exists(path.join(dir, "target", "middleclass.lua")))

local ok, err = pkg:runFile()
if not ok then print(err) end
test.truthy(ok)
end)

test.it("rockspec git dep: luafilesystem native C module works", function()
-- TODO: Re-enable on MacOS when nightly exports LuaJIT symbols.
if jit.os == "Windows" or jit.os == "OSX" then return end

local dir = path.join(tmpBase, "lfs-consumer")
fs.mkdir(dir)
fs.mkdir(path.join(dir, "src"))
fs.write(path.join(dir, "src", "init.lua"), [[
local lfs = require("lfs")
local attr = lfs.attributes(".")
assert(attr ~= nil, "lfs.attributes returned nil")
assert(attr.mode == "directory", "expected directory, got " .. tostring(attr.mode))
]])
fs.write(path.join(dir, "lpm.json"), json.encode({
name = "lfs-consumer",
version = "0.1.0",
dependencies = {
luafilesystem = {
git = "https://github.com/lunarmodules/luafilesystem",
branch = "master"
}
}
}))

local pkg = lpm.Package.open(dir)
pkg:installDependencies()
pkg:build()

local ok, err = pkg:runFile()
if not ok then print(err) end
test.truthy(ok)
end)
Loading