mirror of
https://github.com/pesde-pkg/tooling.git
synced 2024-12-12 07:10:36 +00:00
init: initial binlib logic and demo lune package
This commit is contained in:
commit
b1892a8098
28 changed files with 1525 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
lune_packages/
|
||||
luau_packages/
|
||||
core/pesde.lock
|
3
.luaurc
Normal file
3
.luaurc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"languageMode": "strict"
|
||||
}
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"luau-lsp.require.mode": "relativeToFile",
|
||||
"luau-lsp.require.directoryAliases": {
|
||||
"@lune/": "~/.lune/.typedefs/0.8.9/"
|
||||
},
|
||||
"stylua.targetReleaseVersion": "latest"
|
||||
}
|
0
core/README.md
Normal file
0
core/README.md
Normal file
19
core/pesde.toml
Normal file
19
core/pesde.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
name = "compeydev/binlib"
|
||||
version = "0.1.0"
|
||||
includes = ["src/", "pesde.toml", "LICENSE", "README.md"]
|
||||
|
||||
[target]
|
||||
environment = "lune"
|
||||
lib = "src/init.luau"
|
||||
|
||||
[dependencies]
|
||||
pathfs = { name = "jiwonz/pathfs", version = "^0.1.0" }
|
||||
dirs = { name = "jiwonz/dirs", version = "^0.1.1" }
|
||||
semver = { name = "0x5eal/semver", version = "^0.1.0", target = "luau" }
|
||||
|
||||
[peer_dependencies]
|
||||
result = { name = "lukadev_0/result", version = "^1.2.0" }
|
||||
option = { name = "lukadev_0/option", version = "^1.2.0" }
|
||||
|
||||
[indices]
|
||||
default = "https://github.com/daimond113/pesde-index"
|
97
core/src/compression.luau
Normal file
97
core/src/compression.luau
Normal file
|
@ -0,0 +1,97 @@
|
|||
local serde = require("@lune/serde")
|
||||
local process = require("@lune/process")
|
||||
|
||||
local dirs = require("../lune_packages/dirs")
|
||||
local pathfs = require("../lune_packages/pathfs")
|
||||
|
||||
local revTable = require("./utils/rev_table")
|
||||
local CommandBuilder = require("./utils/exec")
|
||||
local types = require("./utils/result_option_conv")
|
||||
local Option = types.Option
|
||||
type Option<T> = types.Option<T>
|
||||
local Result = types.Result
|
||||
type Result<T, E> = types.Result<T, E>
|
||||
|
||||
export type CompressionFormat = "TarGz" | "TarXz" | "Zip"
|
||||
|
||||
local function detectFormat(fileName: string): Option<CompressionFormat>
|
||||
local fileNameParts = string.split(string.lower(fileName), ".")
|
||||
revTable(fileNameParts)
|
||||
|
||||
if fileNameParts[1] == "zip" then
|
||||
return Option.Some("Zip" :: CompressionFormat) :: Option<unknown>
|
||||
end
|
||||
|
||||
if fileNameParts[2] == "tar" then
|
||||
if fileNameParts[1] == "gz" then
|
||||
return Option.Some("TarGz" :: CompressionFormat) :: Option<unknown>
|
||||
end
|
||||
|
||||
if fileNameParts[1] == "xz" then
|
||||
return Option.Some("TarXz" :: CompressionFormat) :: Option<unknown>
|
||||
end
|
||||
end
|
||||
|
||||
return Option.None :: Option<CompressionFormat>
|
||||
end
|
||||
|
||||
-- TODO: Use a type function to make all CompressionFormat lowercase
|
||||
local decompress: { [CompressionFormat]: (compressed: buffer) -> Result<pathfs.AsPath, string> } = {
|
||||
Zip = function(compressed: buffer)
|
||||
-- FIXME: remove any usage
|
||||
return (Option.from(dirs.cacheDir()):map(function(cacheDir)
|
||||
local progCacheDir = cacheDir:join("pesde-bin")
|
||||
if not pathfs.isDir(progCacheDir) then
|
||||
pathfs.writeDir(progCacheDir)
|
||||
end
|
||||
|
||||
return progCacheDir :: pathfs.AsPath
|
||||
end) :: Option<any>):match({
|
||||
Some = function(dir)
|
||||
-- Generate a unique file name and write the contents to the temporary file
|
||||
local tmpFile = dir:join(`{serde.hash("blake3", compressed)}.zip`)
|
||||
local tmpFilePath = tmpFile:toString()
|
||||
pathfs.writeFile(tmpFile, compressed)
|
||||
|
||||
-- Create the directory to decompress into
|
||||
local decompressedDir = pathfs.Path.from(tmpFile):withExtension("")
|
||||
pathfs.writeDir(decompressedDir)
|
||||
|
||||
-- Run unzip to decompress the file
|
||||
local child = CommandBuilder
|
||||
.new("unzip")
|
||||
:withArgs({ tmpFilePath, "-d", decompressedDir:toString() })
|
||||
-- FIXME: remove unknown usage
|
||||
:withStdioStrategy({
|
||||
stdout = Option.Some("pipe" :: CommandBuilder.StdioStrategy) :: Option<unknown>,
|
||||
stderr = Option.Some(if process.env.PESDE_LOG == "debug" then "forward" else "pipe" :: CommandBuilder.StdioStrategy) :: Option<unknown>,
|
||||
} :: CommandBuilder.IoStrategyMapping)
|
||||
:intoChildProcess()
|
||||
|
||||
child:start()
|
||||
local status = child:waitForChild()
|
||||
|
||||
-- Cleanup temporary file and handle errors
|
||||
pathfs.removeFile(tmpFile)
|
||||
if not status.ok then
|
||||
return Result.Err(
|
||||
`DecompressError::CommandFailed(exitCode={status.code})`
|
||||
) :: Result<pathfs.AsPath, string>
|
||||
end
|
||||
|
||||
return Result.Ok(decompressedDir) :: Result<pathfs.AsPath, string>
|
||||
end,
|
||||
|
||||
None = function()
|
||||
return Result.Err("DecompressError::NoCacheDir") :: Result<pathfs.AsPath, string>
|
||||
end,
|
||||
})
|
||||
end,
|
||||
|
||||
-- TODO: Other formats
|
||||
}
|
||||
|
||||
-- local path = "pesde-0.5.0-rc.7-windows-x86_64.zip"
|
||||
-- print(decompress[detectFormat(path):unwrap()](buffer.fromstring(pathfs.readFile(path))))
|
||||
|
||||
return { decompress = decompress, detectFormat = detectFormat }
|
91
core/src/github.luau
Normal file
91
core/src/github.luau
Normal file
|
@ -0,0 +1,91 @@
|
|||
local net = require("@lune/net")
|
||||
|
||||
local copy = require("./utils/copy")
|
||||
local types = require("./utils/result_option_conv")
|
||||
local Option = types.Option
|
||||
local Result = types.Result
|
||||
type Option<T> = types.Option<T>
|
||||
type Result<T, E> = types.Result<T, E>
|
||||
|
||||
local Github = {}
|
||||
export type Github = typeof(setmetatable(Github :: GithubFields, { __index = Github }))
|
||||
type GithubFields = {
|
||||
req: net.FetchParams,
|
||||
retries: number,
|
||||
}
|
||||
|
||||
export type Config = {
|
||||
authToken: Option<string>,
|
||||
retries: Option<number>,
|
||||
}
|
||||
export type GithubOperation = "FetchReleases" | "GetMetadata" | "GetActionArtifacts"
|
||||
|
||||
local API_BASE_URL = "https://api.github.com"
|
||||
local DEFAULT_MAX_RETRIES = 5
|
||||
local DEFAULT_CONFIG: Config = {
|
||||
authToken = Option.None :: Option<string>,
|
||||
retries = Option.Some(DEFAULT_MAX_RETRIES) :: Option<number>,
|
||||
}
|
||||
|
||||
function Github.new(repo: string, config: Option<Config>)
|
||||
local configOrDefault = config:unwrapOr(DEFAULT_CONFIG)
|
||||
return setmetatable(
|
||||
{
|
||||
req = {
|
||||
url = API_BASE_URL .. "/repos/" .. repo,
|
||||
headers = {
|
||||
["Authorization"] = configOrDefault.authToken:mapOr("", function(token)
|
||||
return `Bearer {token}`
|
||||
end),
|
||||
},
|
||||
} :: net.FetchParams,
|
||||
config = config,
|
||||
retries = configOrDefault.retries:unwrapOr(DEFAULT_MAX_RETRIES),
|
||||
} :: GithubFields,
|
||||
{
|
||||
__index = Github,
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
-- FIXME: Remove unknown usage here
|
||||
function Github.queueTransactions(self: Github, operations: { GithubOperation }): { Result<unknown, string> }
|
||||
local queue: { (retries: number) -> Result<unknown, string> } = table.create(#operations)
|
||||
|
||||
for _, operation: GithubOperation in operations do
|
||||
local req: net.FetchParams = copy(self.req)
|
||||
|
||||
if operation == "FetchReleases" then
|
||||
req.url ..= "/releases"
|
||||
req.method = "GET"
|
||||
end
|
||||
|
||||
-- TODO: Other methods
|
||||
|
||||
table.insert(queue, function(retries: number)
|
||||
local lastCode: number
|
||||
for _ = 1, retries do
|
||||
local resp = net.request(req)
|
||||
lastCode = resp.statusCode
|
||||
|
||||
if not resp.ok then
|
||||
continue
|
||||
end
|
||||
|
||||
return Result.Ok(net.jsonDecode(resp.body))
|
||||
end
|
||||
|
||||
return Result.Err(`Github::RespErr(statusCode={lastCode})`)
|
||||
end)
|
||||
end
|
||||
|
||||
local results = {}
|
||||
for _, req in queue do
|
||||
local ok, respRes: Result<unknown, string> = pcall(req, self.retries)
|
||||
table.insert(results, if ok then respRes else Result.Err("Github::IoError"))
|
||||
end
|
||||
|
||||
return results
|
||||
end
|
||||
|
||||
return Github
|
176
core/src/init.luau
Normal file
176
core/src/init.luau
Normal file
|
@ -0,0 +1,176 @@
|
|||
--> ENTRYPOINT; The main logic is as follows:
|
||||
--> * First, we fetch the artifacts
|
||||
--> * We try going through the list and using pattern matching to figure out
|
||||
--> which is the artifact to download
|
||||
--> * If we get no matches, we try downloading all the artifacts
|
||||
--> * The artifacts are all extracted and we try detecting the platform from
|
||||
--> binary data
|
||||
--> * We then install the artifact with the matching platform descriptor
|
||||
|
||||
local net = require("@lune/net")
|
||||
local process = require("@lune/process")
|
||||
|
||||
local pathfs = require("../lune_packages/pathfs")
|
||||
local dirs = require("../lune_packages/dirs")
|
||||
local semver = require("../luau_packages/semver")
|
||||
|
||||
local types = require("./utils/result_option_conv")
|
||||
local Option = types.Option
|
||||
type Option<T> = types.Option<T>
|
||||
|
||||
local Github = require("./github")
|
||||
local PlatformDescriptor = require("./platform/descriptor")
|
||||
local compression = require("./compression")
|
||||
local eq = require("./utils/eq")
|
||||
|
||||
export type ToolId = {
|
||||
alias: Option<string>,
|
||||
repo: string,
|
||||
version: Option<semver.SemverImpl>,
|
||||
}
|
||||
|
||||
export type GithubReleases = {
|
||||
{
|
||||
tag_name: string,
|
||||
prerelease: boolean,
|
||||
draft: boolean,
|
||||
assets: {
|
||||
{
|
||||
name: string,
|
||||
browser_download_url: string,
|
||||
size: number,
|
||||
content_type: string,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
local function downloadAndDecompress(asset: {
|
||||
name: string,
|
||||
browser_download_url: string,
|
||||
size: number,
|
||||
content_type: string,
|
||||
}): pathfs.Path
|
||||
local contentsResp = net.request(asset.browser_download_url)
|
||||
if not contentsResp.ok then
|
||||
return error(`Failed to download asset {asset.name}: HTTP Code {contentsResp.statusCode}`)
|
||||
end
|
||||
|
||||
local decompressedPath = compression.decompress
|
||||
[compression.detectFormat(asset.name):unwrap()](buffer.fromstring(contentsResp.body))
|
||||
:unwrap() :: pathfs.Path
|
||||
|
||||
return decompressedPath
|
||||
end
|
||||
|
||||
local function runTool(path: pathfs.Path)
|
||||
-- TODO: Fixup our wrapper and use that instead in the future
|
||||
process.spawn(path:toString(), process.args, {
|
||||
stdio = "forward",
|
||||
shell = true,
|
||||
})
|
||||
end
|
||||
|
||||
local TOOL_INSTALL_DIR = (dirs.homeDir() or error("Couldn't get home dir :(")):join(".pesde"):join("bin")
|
||||
|
||||
function installTool(tool: ToolId)
|
||||
local toolAlias = tool.alias:unwrapOr(string.split(tool.repo, "/")[2])
|
||||
local toolInstallPath = TOOL_INSTALL_DIR:join(toolAlias)
|
||||
|
||||
-- TODO: This is a bit too much overhead on startup and takes about 2s
|
||||
-- We can improve this by following what rokit does, where we symlink
|
||||
-- the tool's path to this script, and check the file that we are being
|
||||
-- invoked as, in order to figure out what tool to execute
|
||||
|
||||
-- We can create "linker" scripts for each tool at ~/.pesde/bins with
|
||||
-- contents like so:
|
||||
--[[
|
||||
#!/bin/env -S lune run
|
||||
|
||||
local pathInfo = debug.info(1, "s")
|
||||
local path = string.sub(pathInfo, 10, #pathInfo - 2))
|
||||
|
||||
-- Now we can use `path` to figure out the real tool to execute
|
||||
-- ...
|
||||
]]
|
||||
if pathfs.isFile(toolInstallPath) then
|
||||
runTool(toolInstallPath)
|
||||
return
|
||||
end
|
||||
|
||||
local client = Github.new(
|
||||
tool.repo,
|
||||
Option.Some({
|
||||
-- TODO: Maybe use the `gh auth token` command to get the token
|
||||
authToken = Option.from(process.env.GITHUB_TOKEN) :: Option<string>,
|
||||
retries = Option.None :: Option<number>,
|
||||
}) :: Option<Github.Config>
|
||||
)
|
||||
|
||||
local releases = client:queueTransactions({ "FetchReleases" })[1]:unwrap() :: GithubReleases
|
||||
local assets = tool.version:match({
|
||||
Some = function(version: semver.SemverImpl)
|
||||
for _, release in releases do
|
||||
if semver.parse(release.tag_name):unwrap() :: semver.SemverImpl == version then
|
||||
return release.assets
|
||||
end
|
||||
end
|
||||
|
||||
return error(`No release found for version {version}`)
|
||||
end,
|
||||
|
||||
None = function()
|
||||
return releases[0].assets
|
||||
end,
|
||||
})
|
||||
|
||||
-- TODO: Use index type fn in solver v2
|
||||
local matchingAsset: {
|
||||
name: string,
|
||||
browser_download_url: string,
|
||||
size: number,
|
||||
content_type: string,
|
||||
}
|
||||
|
||||
local currentDesc = PlatformDescriptor.currentSystem()
|
||||
local aliasPath = pathfs.Path.from(toolAlias):withExtension(if currentDesc.os == "windows" then "exe" else "")
|
||||
|
||||
for _, asset in assets do
|
||||
local desc = PlatformDescriptor.fromString(asset.name):unwrap()
|
||||
if eq(currentDesc, desc) then
|
||||
matchingAsset = asset
|
||||
end
|
||||
end
|
||||
|
||||
local binaryPath: pathfs.Path
|
||||
if matchingAsset == nil then
|
||||
warn("No matching asset found, downloading all assets")
|
||||
for _, asset in assets do
|
||||
local decompressedPath = downloadAndDecompress(asset)
|
||||
|
||||
for _, file in pathfs.readDir(decompressedPath) do
|
||||
local filePath = decompressedPath:join(file)
|
||||
local nativeDesc = PlatformDescriptor.fromExecutable(filePath:toString()):unwrap()
|
||||
|
||||
if eq(currentDesc, nativeDesc) then
|
||||
binaryPath = filePath
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
local decompressedPath = downloadAndDecompress(matchingAsset)
|
||||
binaryPath = decompressedPath:join(aliasPath)
|
||||
if not pathfs.isFile(binaryPath) then
|
||||
error(`No matching binary found in {decompressedPath}`)
|
||||
end
|
||||
end
|
||||
|
||||
pathfs.move(binaryPath, toolInstallPath)
|
||||
runTool(toolInstallPath)
|
||||
end
|
||||
|
||||
return {
|
||||
installTool = installTool,
|
||||
runTool = runTool
|
||||
}
|
38
core/src/platform/arch.luau
Normal file
38
core/src/platform/arch.luau
Normal file
|
@ -0,0 +1,38 @@
|
|||
--> Mostly a recreation of rokit's detection logic in Luau
|
||||
--> See https://github.com/rojo-rbx/rokit/blob/a6b84c/lib/descriptor/arch.rs
|
||||
|
||||
local process = require("@lune/process")
|
||||
local detection = require("./detection")
|
||||
|
||||
local types = require("../utils/result_option_conv")
|
||||
local Option = types.Option
|
||||
type Option<T> = types.Option<T>
|
||||
|
||||
export type Arch = process.Arch | "arm" | "x86"
|
||||
|
||||
local ARCH_SUBSTRINGS: { [Arch]: { string } } = {
|
||||
aarch64 = { "aarch64", "arm64", "armv9" },
|
||||
x86_64 = { "x86-64", "x86_64", "amd64", "win64", "win-x64" },
|
||||
arm = { "arm32", "armv7" },
|
||||
x86 = { "i686", "i386", "win32", "win-x86" },
|
||||
}
|
||||
|
||||
local ARCH_FULL_WORDS: { [Arch]: { string } } = {
|
||||
aarch64 = {},
|
||||
x86_64 = { "x64", "win" },
|
||||
arm = { "arm" },
|
||||
x86 = { "x86" },
|
||||
}
|
||||
|
||||
return {
|
||||
detect = function(str: string): Option<Arch>
|
||||
return detection.detect(str, ARCH_SUBSTRINGS, ARCH_FULL_WORDS)
|
||||
end,
|
||||
|
||||
detectFromExecutable = function(binaryContents: buffer)
|
||||
return Option.from(detection.detectFromExecutable(binaryContents))
|
||||
:map(function(inner: detection.ExecutableDetectionResult)
|
||||
return inner.arch
|
||||
end)
|
||||
end,
|
||||
}
|
59
core/src/platform/descriptor.luau
Normal file
59
core/src/platform/descriptor.luau
Normal file
|
@ -0,0 +1,59 @@
|
|||
local process = require("@lune/process")
|
||||
local fs = require("@lune/fs")
|
||||
|
||||
local os = require("./os")
|
||||
local arch = require("./arch")
|
||||
local toolchain = require("./toolchain")
|
||||
local result = require("./result")
|
||||
local detectFromExecutable = require("./detection/executable")
|
||||
|
||||
local types = require("../utils/result_option_conv")
|
||||
local Option = types.Option
|
||||
local Result = types.Result
|
||||
type Option<T> = types.Option<T>
|
||||
type Result<T, E> = types.Result<T, E>
|
||||
|
||||
local PlatformDescriptor = {}
|
||||
type PlatformDescriptor = {
|
||||
os: process.OS,
|
||||
arch: Option<arch.Arch>,
|
||||
toolchain: Option<toolchain.Toolchain>,
|
||||
}
|
||||
|
||||
function PlatformDescriptor.currentSystem(): PlatformDescriptor
|
||||
return {
|
||||
os = process.os,
|
||||
arch = Option.Some(process.arch) :: Option<arch.Arch>,
|
||||
toolchain = Option.None :: Option<toolchain.Toolchain>,
|
||||
}
|
||||
end
|
||||
|
||||
function PlatformDescriptor.fromString(str: string)
|
||||
local detectedOs = os.detect(str)
|
||||
if detectedOs:isNone() then
|
||||
return Result.Err("NoPatternDetected" :: result.PlatformError)
|
||||
end
|
||||
|
||||
return Result.Ok({
|
||||
os = detectedOs:unwrap() :: process.OS,
|
||||
arch = arch.detect(str),
|
||||
toolchain = toolchain.detect(str),
|
||||
} :: PlatformDescriptor)
|
||||
end
|
||||
|
||||
function PlatformDescriptor.fromExecutable(path: string): result.PlatformResult<PlatformDescriptor>
|
||||
local binaryContents = fs.readFile(path)
|
||||
local detected =
|
||||
Option.from(detectFromExecutable(buffer.fromstring(binaryContents))) :: Option<detectFromExecutable.ExecutableDetectionResult>
|
||||
local platformDesc = detected:map(function(inner: detectFromExecutable.ExecutableDetectionResult)
|
||||
local innerClone: PlatformDescriptor = table.clone(inner) :: any
|
||||
innerClone.toolchain = Option.None :: Option<toolchain.Toolchain>
|
||||
return innerClone
|
||||
end) :: Option<PlatformDescriptor>
|
||||
|
||||
return platformDesc:okOr(
|
||||
"NoExecutableDetected" :: result.PlatformError
|
||||
) :: result.PlatformResult<PlatformDescriptor>
|
||||
end
|
||||
|
||||
return PlatformDescriptor
|
92
core/src/platform/detection/executable.luau
Normal file
92
core/src/platform/detection/executable.luau
Normal file
|
@ -0,0 +1,92 @@
|
|||
local process = require("@lune/process")
|
||||
|
||||
type Arch = process.Arch | "arm" | "x86"
|
||||
export type ExecutableDetectionResult = {
|
||||
os: process.OS?,
|
||||
arch: Arch?,
|
||||
}
|
||||
|
||||
return function(binaryContents: buffer): ExecutableDetectionResult?
|
||||
-- Windows PE
|
||||
do
|
||||
local DOS_HEADER = "MZ"
|
||||
local PE_HEADER_OFFSET = 0x3c
|
||||
local PE_MAGIC_SIGNATURE = "PE\x00\x00"
|
||||
local PE_MACHINE_TYPES: { [number]: Arch } = {
|
||||
[0x8664] = "x86_64",
|
||||
[0x01c0] = "arm",
|
||||
[0xaa64] = "aarch64",
|
||||
[0x014c] = "x86",
|
||||
}
|
||||
|
||||
if buffer.readstring(binaryContents, 0, 2) == DOS_HEADER then
|
||||
-- File was a DOS executable, jump to PE header to get the magic offset
|
||||
local signatureOffset = buffer.readu8(binaryContents, PE_HEADER_OFFSET)
|
||||
|
||||
-- Check if the value at the magic offset was the PE magic signature
|
||||
if buffer.readstring(binaryContents, signatureOffset, 4) == PE_MAGIC_SIGNATURE then
|
||||
-- After the first 4 magic bytes, the next 2 bytes are architecture information
|
||||
local machineType = buffer.readu16(binaryContents, signatureOffset + 4)
|
||||
return {
|
||||
os = "windows",
|
||||
arch = PE_MACHINE_TYPES[machineType],
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Linux ELF
|
||||
do
|
||||
local ELF_MAGIC_START = 0x7f
|
||||
local ELF_MAGIC_SIGNATURE = "ELF"
|
||||
local ELF_MACHINE_TYPES: { [number]: Arch } = {
|
||||
[0x3e] = "x86_64",
|
||||
[0x28] = "arm",
|
||||
[0xb7] = "aarch64",
|
||||
[0x03] = "x86",
|
||||
}
|
||||
|
||||
-- ELF files have a magic signature of [0x7f, 'E', 'L', 'F']
|
||||
if
|
||||
buffer.readu8(binaryContents, 0) == ELF_MAGIC_START
|
||||
and buffer.readstring(binaryContents, 1, 3) == ELF_MAGIC_SIGNATURE
|
||||
then
|
||||
-- Machine type is located after 16 bytes of ident and 2 bytes of type
|
||||
local machineType = buffer.readu16(binaryContents, 18)
|
||||
return {
|
||||
os = "linux",
|
||||
arch = ELF_MACHINE_TYPES[machineType],
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
-- macOS Mach-O
|
||||
do
|
||||
local MACHO_MAGIC_32 = 0xFEEDFACE
|
||||
local MACHO_MAGIC_64 = 0xFEEDFACF
|
||||
local MACHO_CPU_TYPES = {
|
||||
x86 = 0x7,
|
||||
arm = 0xc,
|
||||
}
|
||||
|
||||
-- First 4 bytes are the magic depending on 32 or 64 bit
|
||||
-- Next 2 bytes are the CPU type
|
||||
local headerStart = buffer.readu32(binaryContents, 0)
|
||||
local cpuType = buffer.readu16(binaryContents, 4)
|
||||
|
||||
local is64bit = headerStart == MACHO_MAGIC_64
|
||||
local is32bit = headerStart == MACHO_MAGIC_32
|
||||
|
||||
return {
|
||||
os = "macos",
|
||||
--stylua: ignore
|
||||
arch = (
|
||||
if is64bit and cpuType == MACHO_CPU_TYPES.x86 then "x86_64"
|
||||
elseif is64bit and cpuType == MACHO_CPU_TYPES.arm then "aarch64"
|
||||
elseif is32bit and cpuType == MACHO_CPU_TYPES.x86 then "x86"
|
||||
elseif is32bit and cpuType == MACHO_CPU_TYPES.arm then "arm"
|
||||
else nil
|
||||
),
|
||||
}
|
||||
end
|
||||
end
|
7
core/src/platform/detection/init.luau
Normal file
7
core/src/platform/detection/init.luau
Normal file
|
@ -0,0 +1,7 @@
|
|||
local executable = require("./executable")
|
||||
export type ExecutableDetectionResult = executable.ExecutableDetectionResult
|
||||
|
||||
return {
|
||||
detect = require("./pattern"),
|
||||
detectFromExecutable = executable,
|
||||
}
|
34
core/src/platform/detection/pattern.luau
Normal file
34
core/src/platform/detection/pattern.luau
Normal file
|
@ -0,0 +1,34 @@
|
|||
local String = require("../../utils/string")
|
||||
|
||||
local types = require("../../utils/result_option_conv")
|
||||
local Option = types.Option
|
||||
type Option<T> = types.Option<T>
|
||||
|
||||
local function charWordSep(char: string)
|
||||
return char == " " or char == "-" or char == "_"
|
||||
end
|
||||
|
||||
return function<T>(str: string, substrings: { [T]: { string } }, fullWords: { [T]: { string } }): Option<T>
|
||||
local lowercased = string.lower(str)
|
||||
|
||||
-- Look for substring matches
|
||||
for item: T, keywords in substrings do
|
||||
for _, keyword in keywords do
|
||||
if string.find(lowercased, keyword) then
|
||||
return Option.Some(item) :: Option<T>
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If no substring matches found, look for a full word as a component
|
||||
local components = String.splitAtChar(lowercased, charWordSep)
|
||||
for _, component in components do
|
||||
for item, keywords in fullWords do
|
||||
if table.find(keywords, component) then
|
||||
return Option.Some(item) :: Option<T>
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Option.None :: Option<T>
|
||||
end
|
34
core/src/platform/os.luau
Normal file
34
core/src/platform/os.luau
Normal file
|
@ -0,0 +1,34 @@
|
|||
--> Mostly a recreation of rokit's detection logic in Luau
|
||||
--> See https://github.com/rojo-rbx/rokit/blob/a6b84c/lib/descriptor/os.rs
|
||||
|
||||
local process = require("@lune/process")
|
||||
|
||||
local detection = require("./detection")
|
||||
|
||||
local Option = require("../../lune_packages/option")
|
||||
type Option<T> = Option.Option<T>
|
||||
|
||||
local OS_SUBSTRINGS: { [process.OS]: { string } } = {
|
||||
windows = { "windows" },
|
||||
macos = { "macos", "darwin", "apple" },
|
||||
linux = { "linux", "ubuntu", "debian", "fedora" },
|
||||
}
|
||||
|
||||
local OS_FULL_WORDS: { [process.OS]: { string } } = {
|
||||
windows = { "win", "win32", "win64" },
|
||||
macos = { "mac", "osx" },
|
||||
linux = {},
|
||||
}
|
||||
|
||||
return {
|
||||
detect = function(str: string): Option<process.OS>
|
||||
return detection.detect(str, OS_SUBSTRINGS, OS_FULL_WORDS)
|
||||
end,
|
||||
|
||||
detectFromExecutable = function(binaryContents: buffer): Option<process.OS>
|
||||
return Option.from(detection.detectFromExecutable(binaryContents))
|
||||
:map(function(inner: detection.ExecutableDetectionResult)
|
||||
return inner.os
|
||||
end)
|
||||
end,
|
||||
}
|
7
core/src/platform/result.luau
Normal file
7
core/src/platform/result.luau
Normal file
|
@ -0,0 +1,7 @@
|
|||
local types = require("../utils/result_option_conv")
|
||||
type Result<T, E> = types.Result<T, E>
|
||||
|
||||
export type PlatformError = "NoPatternDetected" | "NoExecutableDetected" | "UnknownExecutableField"
|
||||
export type PlatformResult<T> = Result<T, PlatformError>
|
||||
|
||||
return "<PlatformResult>"
|
19
core/src/platform/toolchain.luau
Normal file
19
core/src/platform/toolchain.luau
Normal file
|
@ -0,0 +1,19 @@
|
|||
local types = require("../utils/result_option_conv")
|
||||
local Option = types.Option
|
||||
type Option<T> = types.Option<T>
|
||||
|
||||
local TOOLCHAINS: { Toolchain } = { "msvc", "gnu", "musl" }
|
||||
export type Toolchain = "msvc" | "gnu" | "musl"
|
||||
|
||||
return {
|
||||
detect = function(str: string): Option<Toolchain>
|
||||
for _, toolchain: Toolchain in TOOLCHAINS do
|
||||
if string.find(str, toolchain) then
|
||||
-- FIXME: remove any usage
|
||||
return Option.Some(toolchain :: any) :: Option<Toolchain>
|
||||
end
|
||||
end
|
||||
|
||||
return Option.None :: Option<Toolchain>
|
||||
end,
|
||||
}
|
15
core/src/utils/copy.luau
Normal file
15
core/src/utils/copy.luau
Normal file
|
@ -0,0 +1,15 @@
|
|||
local function copy<T>(tab: T): T
|
||||
if typeof(tab) == "table" then
|
||||
local copied = {}
|
||||
|
||||
for k, v in tab do
|
||||
copied[k] = copy(v)
|
||||
end
|
||||
|
||||
return copied :: any
|
||||
end
|
||||
|
||||
return tab
|
||||
end
|
||||
|
||||
return copy
|
29
core/src/utils/eq.luau
Normal file
29
core/src/utils/eq.luau
Normal file
|
@ -0,0 +1,29 @@
|
|||
-- !! FIXME: THIS IS BROKEN !!
|
||||
local function eq(this: any, that: any): boolean
|
||||
if type(this) ~= type(that) then
|
||||
return false
|
||||
end
|
||||
|
||||
if type(this) == "table" then
|
||||
local visited = {}
|
||||
|
||||
for key, value in pairs(this) do
|
||||
if not eq(value, that[key]) then
|
||||
return false
|
||||
end
|
||||
visited[key] = true
|
||||
end
|
||||
|
||||
for key, _ in pairs(that) do
|
||||
if not visited[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
return this == that
|
||||
end
|
||||
|
||||
return eq
|
193
core/src/utils/exec.luau
Normal file
193
core/src/utils/exec.luau
Normal file
|
@ -0,0 +1,193 @@
|
|||
--> Builder pattern class to spawn, manage and kill child processes
|
||||
|
||||
local process = require("@lune/process")
|
||||
local task = require("@lune/task")
|
||||
|
||||
local types = require("../utils/result_option_conv")
|
||||
local Option = types.Option
|
||||
type Option<T> = types.Option<T>
|
||||
|
||||
local CommandBuilder = {}
|
||||
type CommandBuilderFields = {
|
||||
program: string,
|
||||
args: { string },
|
||||
retries: Option<number>,
|
||||
ignoreErrors: Option<boolean>,
|
||||
stdioStrategy: Option<IoStrategyMapping>,
|
||||
}
|
||||
export type CommandBuilder = typeof(setmetatable({} :: CommandBuilderFields, { __index = CommandBuilder }))
|
||||
export type StdioStrategy = "pipe" | "forward" | "none"
|
||||
export type IoStrategyMapping = {
|
||||
stdout: Option<StdioStrategy>,
|
||||
stderr: Option<StdioStrategy>,
|
||||
}
|
||||
export type ChildProcess = {
|
||||
_thread: thread,
|
||||
_pid: string,
|
||||
_status: ChildStatus,
|
||||
start: (self: ChildProcess) -> (),
|
||||
waitForChild: (self: ChildProcess) -> ChildStatus,
|
||||
kill: (self: ChildProcess) -> (),
|
||||
}
|
||||
export type ChildStatus = { ok: boolean, code: number, io: {
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
} }
|
||||
|
||||
-- FIXME: remove unknown usage
|
||||
local DEFAULT_STDIO_STRATEGY: IoStrategyMapping = {
|
||||
stdout = Option.Some("pipe" :: StdioStrategy) :: Option<unknown>,
|
||||
stderr = Option.Some("pipe" :: StdioStrategy) :: Option<unknown>,
|
||||
}
|
||||
local DEFAULT_RETRIES = 0
|
||||
local DEFAULT_IGNORE_ERRORS = false
|
||||
|
||||
function CommandBuilder.new(program: string)
|
||||
return setmetatable(
|
||||
{
|
||||
program = program,
|
||||
args = {},
|
||||
retries = Option.None,
|
||||
ignoreErrors = Option.None,
|
||||
stdioStrategy = Option.None :: Option<IoStrategyMapping>,
|
||||
} :: CommandBuilderFields,
|
||||
{
|
||||
__index = CommandBuilder,
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
function CommandBuilder.withArg(self: CommandBuilder, arg: string): CommandBuilder
|
||||
table.insert(self.args, arg)
|
||||
return self
|
||||
end
|
||||
|
||||
function CommandBuilder.withArgs(self: CommandBuilder, args: { string }): CommandBuilder
|
||||
for _, arg in args do
|
||||
self:withArg(arg)
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function CommandBuilder.withMaxRetries(self: CommandBuilder, retries: number): CommandBuilder
|
||||
self.retries = Option.Some(retries) :: Option<number>
|
||||
return self
|
||||
end
|
||||
|
||||
function CommandBuilder.withIgnoreErrors(self: CommandBuilder, yes: boolean): CommandBuilder
|
||||
self.ignoreErrors = Option.Some(yes) :: Option<boolean>
|
||||
return self
|
||||
end
|
||||
|
||||
function CommandBuilder.withStdioStrategy(
|
||||
self: CommandBuilder,
|
||||
strategy: StdioStrategy | IoStrategyMapping
|
||||
): CommandBuilder
|
||||
-- FIXME: remove unknown usage
|
||||
self.stdioStrategy = Option.Some(if typeof(strategy) == "string"
|
||||
then {
|
||||
stdout = Option.Some(strategy) :: Option<unknown>,
|
||||
stderr = Option.Some(strategy) :: Option<unknown>,
|
||||
}
|
||||
else strategy) :: Option<IoStrategyMapping>
|
||||
return self
|
||||
end
|
||||
|
||||
local function intoSpawnOptionsStdioKind(strategy: StdioStrategy): process.SpawnOptionsStdioKind
|
||||
if strategy == "pipe" then
|
||||
return "default"
|
||||
end
|
||||
|
||||
if strategy == "forward" then
|
||||
return "forward"
|
||||
end
|
||||
|
||||
if strategy == "none" then
|
||||
return "none"
|
||||
end
|
||||
|
||||
error(`Non-strategy provided: {strategy}`)
|
||||
end
|
||||
|
||||
function CommandBuilder.intoChildProcess(self: CommandBuilder): ChildProcess
|
||||
local child = {
|
||||
_thread = coroutine.create(function(this: ChildProcess)
|
||||
local retries = self.retries:unwrapOr(DEFAULT_RETRIES)
|
||||
local ignoreErrors = self.ignoreErrors:unwrapOr(DEFAULT_IGNORE_ERRORS)
|
||||
local argsList = table.concat(self.args, " ")
|
||||
|
||||
for _ = 0, retries do
|
||||
local spawned = process.spawn(
|
||||
if process.os == "windows"
|
||||
then `(Start-Process {self.program} -Passthru -NoNewWindow -ArgumentList \"{argsList}\").Id`
|
||||
else `{self.program} {argsList} & echo $!`,
|
||||
{},
|
||||
{
|
||||
stdio = self
|
||||
.stdioStrategy
|
||||
-- FIXME: remove unknown usage
|
||||
:orOpt(
|
||||
Option.Some(DEFAULT_STDIO_STRATEGY) :: Option<unknown>
|
||||
)
|
||||
:map(function(mappings: IoStrategyMapping)
|
||||
local translatedMappings: process.SpawnOptionsStdio = {}
|
||||
for field, value in mappings do
|
||||
translatedMappings[field] =
|
||||
intoSpawnOptionsStdioKind((value :: Option<StdioStrategy>):unwrap())
|
||||
end
|
||||
|
||||
return translatedMappings
|
||||
end)
|
||||
:unwrap(),
|
||||
shell = true,
|
||||
}
|
||||
)
|
||||
|
||||
if spawned.ok then
|
||||
local lines = spawned.stdout:split("\n")
|
||||
|
||||
-- TODO: Abstract upvalues here into a channels primitive
|
||||
this._pid = assert(table.remove(lines, 1), "Failed to get PID")
|
||||
this._status = {
|
||||
code = spawned.code,
|
||||
ok = spawned.code == 0 and not ignoreErrors,
|
||||
io = {
|
||||
stdout = table.concat(lines, "\n"),
|
||||
stderr = spawned.stderr,
|
||||
},
|
||||
}
|
||||
break
|
||||
end
|
||||
end
|
||||
end),
|
||||
|
||||
start = function(self: ChildProcess)
|
||||
coroutine.resume(self._thread, self)
|
||||
end,
|
||||
|
||||
waitForChild = function(self: ChildProcess): ChildStatus
|
||||
while coroutine.status(self._thread) ~= "dead" or self._status == nil do
|
||||
task.wait(0.1)
|
||||
end
|
||||
|
||||
return self._status
|
||||
end,
|
||||
|
||||
kill = function(self: ChildProcess)
|
||||
coroutine.close(self._thread)
|
||||
local killResult = process.spawn(
|
||||
if process.os == "windows" then `Stop-Process -Id {self._pid} -Force` else `kill {self._pid}`,
|
||||
{
|
||||
shell = true,
|
||||
}
|
||||
)
|
||||
|
||||
assert(killResult.ok, `Failed to kill process with PID {self._pid}`)
|
||||
end,
|
||||
} :: ChildProcess
|
||||
|
||||
return child
|
||||
end
|
||||
|
||||
return CommandBuilder
|
28
core/src/utils/result_option_conv.luau
Normal file
28
core/src/utils/result_option_conv.luau
Normal file
|
@ -0,0 +1,28 @@
|
|||
--> Non-exhaustive set of extensions for Option<->Result conversion
|
||||
|
||||
local OptionImpl = require("../../lune_packages/option")
|
||||
local ResultImpl = require("../../lune_packages/result")
|
||||
|
||||
local Option = {}
|
||||
local Result = {}
|
||||
|
||||
export type Option<T> = OptionImpl.Option<T> & typeof(Option)
|
||||
export type Result<T, E> = ResultImpl.Result<T, E>
|
||||
|
||||
function Option.okOr<T, E>(self: Option<T>, err: E): Result<T, E>
|
||||
return self:mapOrElse(function()
|
||||
return ResultImpl.Err(err)
|
||||
end, function(val)
|
||||
return ResultImpl.Ok(val)
|
||||
end)
|
||||
end
|
||||
|
||||
return {
|
||||
Option = setmetatable(OptionImpl, {
|
||||
__index = Option,
|
||||
}),
|
||||
|
||||
Result = setmetatable(ResultImpl, {
|
||||
__index = Result,
|
||||
}),
|
||||
}
|
6
core/src/utils/rev_table.luau
Normal file
6
core/src/utils/rev_table.luau
Normal file
|
@ -0,0 +1,6 @@
|
|||
return function(tab)
|
||||
local len = #tab
|
||||
for pos = 1, len // 2 do
|
||||
tab[pos], tab[len - pos + 1] = tab[len - pos + 1], tab[pos]
|
||||
end
|
||||
end
|
296
core/src/utils/semver.luau
Normal file
296
core/src/utils/semver.luau
Normal file
|
@ -0,0 +1,296 @@
|
|||
local types = require("../utils/result_option_conv")
|
||||
local Option = types.Option
|
||||
type Option<T> = types.Option<T>
|
||||
local Result = types.Result
|
||||
|
||||
local function stringStartsWith(str: string, sub: string)
|
||||
return string.sub(str, 1, #sub) == sub
|
||||
end
|
||||
|
||||
local function hasLeadingZeros(num: string)
|
||||
return string.match(num, "^(0+)") ~= nil
|
||||
end
|
||||
|
||||
local function doLeadingZeroesCheck(type: "major" | "minor" | "patch", component: string)
|
||||
-- We only do this check if each component has more than 1 digit as an optimization
|
||||
if #component > 1 and hasLeadingZeros(component) then
|
||||
return Result.Err({
|
||||
msg = Option.Some(`Found disallowed leading zeros in {string.upper(type)} component`),
|
||||
kind = {
|
||||
id = "LeadingZerosPresent",
|
||||
component = type,
|
||||
} :: ParseError,
|
||||
} :: SemverError)
|
||||
end
|
||||
|
||||
return Result.Ok(nil)
|
||||
end
|
||||
|
||||
type InvalidTypes = "char" | "symbol"
|
||||
local function getInvalidTypeName(invalidComponent: string): InvalidTypes
|
||||
local typePatterns: { [InvalidTypes]: string } = {
|
||||
char = "%a",
|
||||
symbol = "%p",
|
||||
}
|
||||
|
||||
local presentType: InvalidTypes
|
||||
|
||||
for type: InvalidTypes, pattern in typePatterns do
|
||||
if string.match(invalidComponent, pattern) then
|
||||
presentType = type
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return presentType
|
||||
end
|
||||
|
||||
local Semver = {}
|
||||
|
||||
export type SemverImpl = typeof(setmetatable({} :: Version, Semver))
|
||||
|
||||
export type PreleaseType = "alpha" | "beta" | "rc"
|
||||
|
||||
export type PrereleaseVersion = {
|
||||
type: PreleaseType,
|
||||
ordinal: Option<number>,
|
||||
}
|
||||
|
||||
export type Version = {
|
||||
major: number,
|
||||
minor: number,
|
||||
patch: number,
|
||||
buildMetadata: Option<string>,
|
||||
prerelease: Option<PrereleaseVersion>,
|
||||
}
|
||||
|
||||
export type SemverResult<T, E = SemverError> = types.Result<T, E>
|
||||
export type SemverError = {
|
||||
msg: Option<string>,
|
||||
kind: SemverErrorKind,
|
||||
}
|
||||
export type SemverErrorKind = ParseError | {}
|
||||
export type ParseError =
|
||||
{ id: "MandatoryComponentMissing", components: { string } }
|
||||
| {
|
||||
id: "InvalidComponentType",
|
||||
component: "major" | "minor" | "patch",
|
||||
expected: "number",
|
||||
got: "char" | "symbol" | "unknown",
|
||||
}
|
||||
| { id: "LeadingZerosPresent", component: "major" | "minor" | "patch" }
|
||||
| { id: "InvalidIdentifierCharacter", component: "prerelease" | "metadata" }
|
||||
| { id: "InvalidPrereleaseType", component: "prerelease", type: string }
|
||||
| { id: "InvalidPrereleaseOrdinalType", expected: "number", got: "char" | "symbol" }
|
||||
|
||||
local PRERELEASE_LEX_ORDER: { [PreleaseType]: number } = {
|
||||
alpha = 1,
|
||||
beta = 2,
|
||||
rc = 3,
|
||||
}
|
||||
|
||||
function Semver.parse(ver: string): SemverResult<SemverImpl>
|
||||
local components = string.split(ver, ".")
|
||||
if #components < 3 then
|
||||
return Result.Err({
|
||||
msg = Option.Some(`Expected MAJOR.MINOR.PATCH format, missing {#components} / 3 components`),
|
||||
kind = {
|
||||
id = "MandatoryComponentMissing",
|
||||
components = components,
|
||||
} :: ParseError,
|
||||
} :: SemverError)
|
||||
end
|
||||
|
||||
local patchStr, ext = string.match(components[3], "(%d)([-+]?.*)")
|
||||
if patchStr == nil then
|
||||
return Result.Err({
|
||||
msg = Option.Some(`Expected patch to be only a number`),
|
||||
kind = {
|
||||
id = "InvalidComponentType",
|
||||
expected = "number",
|
||||
got = "unknown",
|
||||
component = "patch",
|
||||
} :: ParseError,
|
||||
} :: SemverError)
|
||||
end
|
||||
|
||||
local major, minor, patch = tonumber(components[1]), tonumber(components[2]), tonumber(patchStr)
|
||||
if major == nil or minor == nil or patch == nil then
|
||||
local invalidComponentType: "major" | "minor" | "patch", invalidComponent: string
|
||||
|
||||
if major == nil then
|
||||
invalidComponentType, invalidComponent = "major", components[1]
|
||||
elseif minor == nil then
|
||||
invalidComponentType, invalidComponent = "minor", components[2]
|
||||
elseif patch == nil then
|
||||
invalidComponentType, invalidComponent = "patch", patchStr :: string
|
||||
end
|
||||
|
||||
local presentType: InvalidTypes = getInvalidTypeName(invalidComponent)
|
||||
return Result.Err({
|
||||
msg = Option.Some(
|
||||
`Expected {string.upper(invalidComponentType)} to be only a number, but got {presentType}`
|
||||
),
|
||||
kind = {
|
||||
id = "InvalidComponentType",
|
||||
expected = "number",
|
||||
got = presentType,
|
||||
component = invalidComponentType,
|
||||
} :: ParseError,
|
||||
} :: SemverError)
|
||||
end
|
||||
|
||||
-- All components were valid numbers, but we check to see if they had any leading zeros
|
||||
-- Leading zeros are invalid in semver
|
||||
local majorLeadingZeros = doLeadingZeroesCheck("major", components[1])
|
||||
if majorLeadingZeros:isErr() then
|
||||
return majorLeadingZeros
|
||||
end
|
||||
|
||||
local minorLeadingZeros = doLeadingZeroesCheck("minor", components[2])
|
||||
if minorLeadingZeros:isErr() then
|
||||
return minorLeadingZeros
|
||||
end
|
||||
|
||||
local patchLeadingZeros = doLeadingZeroesCheck("patch", patchStr)
|
||||
if patchLeadingZeros:isErr() then
|
||||
return patchLeadingZeros
|
||||
end
|
||||
|
||||
local parsed: Version = {
|
||||
major = major :: number,
|
||||
minor = minor :: number,
|
||||
patch = patch :: number,
|
||||
buildMetadata = Option.None :: Option<string>,
|
||||
prerelease = Option.None :: Option<PrereleaseVersion>,
|
||||
}
|
||||
|
||||
if ext ~= nil then
|
||||
if stringStartsWith(ext, "-") then
|
||||
-- Prerelease information
|
||||
local prereleaseType = string.sub(ext, 2)
|
||||
if prereleaseType ~= "alpha" and prereleaseType ~= "beta" and prereleaseType ~= "rc" then
|
||||
return Result.Err({
|
||||
msg = Option.Some(`Expected prerelease type to be alpha | beta | rc, but got {prereleaseType}`),
|
||||
kind = {
|
||||
id = "InvalidPrereleaseType",
|
||||
component = "prerelease",
|
||||
type = prereleaseType,
|
||||
} :: ParseError,
|
||||
} :: SemverError)
|
||||
end
|
||||
|
||||
local function badPrereleaseType(prerelease: string): SemverResult<SemverImpl>
|
||||
local invalidType: InvalidTypes = getInvalidTypeName(prerelease)
|
||||
return Result.Err({
|
||||
msg = Option.Some(`Expected PRERELEASE to only be a number, but got {invalidType}`),
|
||||
kind = {
|
||||
id = "InvalidPrereleaseOrdinalType",
|
||||
expected = "number",
|
||||
got = invalidType,
|
||||
} :: ParseError,
|
||||
} :: SemverError)
|
||||
end
|
||||
|
||||
local prereleaseOrdinal = Option.None :: Option<number>
|
||||
|
||||
-- TODO: Cleanup this recovery logic of recomputing a bit more, maybe remove it entirely
|
||||
if components[4] ~= nil then
|
||||
local ordinalNum = tonumber(components[4])
|
||||
if ordinalNum == nil then
|
||||
-- If it wasn't a valid number for the ordinal, that must mean one of two things:
|
||||
-- a) The PRERELEASE component was bad
|
||||
-- b) The component has build metadata after prerelease info
|
||||
-- Here, we handle both those cases
|
||||
|
||||
local potentialOrdinalNumber, potentialBuildMetadata = string.match(components[4], "(%d)+(.*)")
|
||||
if potentialOrdinalNumber == nil then
|
||||
return badPrereleaseType(components[4])
|
||||
end
|
||||
|
||||
if potentialBuildMetadata ~= nil then
|
||||
parsed.buildMetadata = Option.Some(potentialBuildMetadata :: string) :: Option<string>
|
||||
end
|
||||
|
||||
if potentialOrdinalNumber ~= nil then
|
||||
ordinalNum = tonumber(potentialOrdinalNumber)
|
||||
if ordinalNum == nil then
|
||||
return badPrereleaseType(components[4])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
prereleaseOrdinal = Option.Some(ordinalNum :: number) :: Option<number>
|
||||
end
|
||||
|
||||
parsed.prerelease = Option.Some({
|
||||
type = prereleaseType,
|
||||
ordinal = prereleaseOrdinal,
|
||||
} :: PrereleaseVersion) :: Option<PrereleaseVersion>
|
||||
end
|
||||
|
||||
if stringStartsWith(ext, "+") then
|
||||
-- Build metadata information; we remove the leading `+` symbol
|
||||
parsed.buildMetadata = Option.Some(string.sub(ext, 2)) :: Option<string>
|
||||
end
|
||||
end
|
||||
|
||||
return Result.Ok(setmetatable(parsed :: Version, Semver))
|
||||
end
|
||||
|
||||
local function prereleaseEq(leftPrerelease: PrereleaseVersion?, rightPrerelease: PrereleaseVersion?): boolean
|
||||
if leftPrerelease == nil or rightPrerelease == nil then
|
||||
return true
|
||||
end
|
||||
|
||||
return leftPrerelease.type == rightPrerelease.type and leftPrerelease.ordinal == rightPrerelease.ordinal
|
||||
end
|
||||
|
||||
--- Returns whether a prerelease is lesser than the other
|
||||
local function prereleaseLt(leftPrerelease: PrereleaseVersion?, rightPrerelase: PrereleaseVersion?): boolean
|
||||
if not prereleaseEq(leftPrerelease, rightPrerelase) then
|
||||
if leftPrerelease ~= nil and rightPrerelase ~= nil then
|
||||
if leftPrerelease.type == rightPrerelase.type then
|
||||
return leftPrerelease.ordinal:unwrapOr(0) < rightPrerelase.ordinal:unwrapOr(0)
|
||||
end
|
||||
|
||||
return PRERELEASE_LEX_ORDER[leftPrerelease.type] < PRERELEASE_LEX_ORDER[rightPrerelase.type]
|
||||
end
|
||||
|
||||
return not (leftPrerelease == nil and rightPrerelase ~= nil)
|
||||
and (leftPrerelease ~= nil and rightPrerelase == nil)
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function prereleaseLe(leftPrerelease: PrereleaseVersion?, rightPrerelase: PrereleaseVersion?): boolean
|
||||
return prereleaseLt(leftPrerelease, rightPrerelase) or prereleaseEq(leftPrerelease, rightPrerelase)
|
||||
end
|
||||
|
||||
function Semver.__eq(left: SemverImpl, right: SemverImpl): boolean
|
||||
return left.major == right.major
|
||||
and left.minor == right.minor
|
||||
and left.patch == right.patch
|
||||
and prereleaseEq(left.prerelease:unwrapOrNil(), right.prerelease:unwrapOrNil())
|
||||
end
|
||||
|
||||
function Semver.__lt(left: SemverImpl, right: SemverImpl): boolean
|
||||
return if left.major ~= right.major
|
||||
then left.major < right.major
|
||||
elseif left.minor ~= right.minor then left.minor < right.minor
|
||||
elseif left.patch ~= right.patch then left.patch < right.patch
|
||||
else prereleaseLt(left.prerelease:unwrapOrNil(), right.prerelease:unwrapOrNil())
|
||||
end
|
||||
|
||||
function Semver.__le(left: SemverImpl, right: SemverImpl): boolean
|
||||
return if left.major ~= right.major
|
||||
then left.major <= right.major
|
||||
elseif left.minor ~= right.minor then left.minor <= right.minor
|
||||
elseif left.patch ~= right.patch then left.patch <= right.patch
|
||||
else prereleaseLe(left.prerelease:unwrapOrNil(), right.prerelease:unwrapOrNil())
|
||||
end
|
||||
|
||||
return Semver
|
||||
|
||||
-- TODO: Testing!
|
51
core/src/utils/string.luau
Normal file
51
core/src/utils/string.luau
Normal file
|
@ -0,0 +1,51 @@
|
|||
local String = {}
|
||||
|
||||
function String.splitAt(str: string, pos: number): { string }?
|
||||
local left, right = string.sub(str, 1, pos - 1), string.sub(str, pos + 1)
|
||||
|
||||
if left == nil or right == nil then
|
||||
return nil
|
||||
end
|
||||
|
||||
return { left, right }
|
||||
end
|
||||
|
||||
function String.splitAtChar(str: string, char: string | number | (c: string) -> boolean): { string }
|
||||
local strChars = string.split(str, "")
|
||||
local splitsPositions: { number } = {}
|
||||
|
||||
for pos, strChar in strChars do
|
||||
local innerChar = if typeof(char) == "number"
|
||||
then string.char(char)
|
||||
elseif typeof(char) == "function" then char(strChar)
|
||||
elseif typeof(char) == "string" and #char == 1 then char
|
||||
else error(`Expected char, got {typeof(char)}`)
|
||||
|
||||
if innerChar == true or strChar == innerChar then
|
||||
table.insert(splitsPositions, pos)
|
||||
end
|
||||
end
|
||||
|
||||
local splitted: { string } = {}
|
||||
for idx, splitPos in splitsPositions do
|
||||
local previous = splitted[#splitted]
|
||||
local relativeSplit = if previous
|
||||
then String.splitAt(previous, splitPos - splitsPositions[idx - 1])
|
||||
else String.splitAt(str, splitPos)
|
||||
|
||||
for _, split in assert(relativeSplit) do
|
||||
table.insert(splitted, split)
|
||||
end
|
||||
end
|
||||
|
||||
local result = {}
|
||||
for idx, split in splitted do
|
||||
if idx % 2 ~= 0 or idx == #splitted or idx == #splitted - 1 then
|
||||
table.insert(result, split)
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return String
|
10
lune/init.luau
Normal file
10
lune/init.luau
Normal file
|
@ -0,0 +1,10 @@
|
|||
local core = require("./lune_packages/core")
|
||||
local semver = require("./luau_packages/semver")
|
||||
local Option = require("./lune_packages/option")
|
||||
type Option<T> = Option.Option<T>
|
||||
|
||||
core.installTool({
|
||||
alias = Option.Some("lune"),
|
||||
repo = "lune-org/lune",
|
||||
version = Option.Some(semver.parse("0.8.9"):unwrap()) :: Option<semver.SemverImpl>,
|
||||
} :: core.ToolId)
|
170
lune/pesde.lock
Normal file
170
lune/pesde.lock
Normal file
|
@ -0,0 +1,170 @@
|
|||
name = "compeydev/lune"
|
||||
version = "0.1.0"
|
||||
target = "lune"
|
||||
|
||||
[graph."0x5eal/semver"."0.1.0 luau"]
|
||||
direct = ["semver", { name = "0x5eal/semver", version = "^0.1.0", target = "luau" }]
|
||||
ty = "standard"
|
||||
|
||||
[graph."0x5eal/semver"."0.1.0 luau".target]
|
||||
environment = "luau"
|
||||
lib = "lib/init.luau"
|
||||
|
||||
[graph."0x5eal/semver"."0.1.0 luau".dependencies]
|
||||
"lukadev_0/option" = ["1.2.0 luau", "option"]
|
||||
"lukadev_0/result" = ["1.2.0 luau", "result"]
|
||||
|
||||
[graph."0x5eal/semver"."0.1.0 luau".pkg_ref]
|
||||
ref_ty = "pesde"
|
||||
name = "0x5eal/semver"
|
||||
version = "0.1.0"
|
||||
index_url = "https://github.com/daimond113/pesde-index"
|
||||
|
||||
[graph."0x5eal/semver"."0.1.0 luau".pkg_ref.dependencies]
|
||||
frktest = [{ name = "itsfrank/frktest", version = "^0.0.2", index = "https://github.com/daimond113/pesde-index", target = "lune" }, "dev"]
|
||||
option = [{ name = "lukadev_0/option", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "peer"]
|
||||
result = [{ name = "lukadev_0/result", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "peer"]
|
||||
|
||||
[graph."0x5eal/semver"."0.1.0 luau".pkg_ref.target]
|
||||
environment = "luau"
|
||||
lib = "lib/init.luau"
|
||||
|
||||
[graph."compeydev/binlib"."0.1.0 lune"]
|
||||
direct = ["core", { workspace = "compeydev/binlib", version = "^" }]
|
||||
ty = "standard"
|
||||
|
||||
[graph."compeydev/binlib"."0.1.0 lune".target]
|
||||
environment = "lune"
|
||||
lib = "src/init.luau"
|
||||
|
||||
[graph."compeydev/binlib"."0.1.0 lune".dependencies]
|
||||
"0x5eal/semver" = ["0.1.0 luau", "semver"]
|
||||
"jiwonz/dirs" = ["0.1.2 lune", "dirs"]
|
||||
"jiwonz/pathfs" = ["0.1.0 lune", "pathfs"]
|
||||
"lukadev_0/option" = ["1.2.0 lune", "option"]
|
||||
"lukadev_0/result" = ["1.2.0 lune", "result"]
|
||||
|
||||
[graph."compeydev/binlib"."0.1.0 lune".pkg_ref]
|
||||
ref_ty = "workspace"
|
||||
path = "core"
|
||||
|
||||
[graph."compeydev/binlib"."0.1.0 lune".pkg_ref.dependencies]
|
||||
dirs = [{ name = "jiwonz/dirs", version = "^0.1.1", index = "https://github.com/daimond113/pesde-index" }, "standard"]
|
||||
option = [{ name = "lukadev_0/option", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "peer"]
|
||||
pathfs = [{ name = "jiwonz/pathfs", version = "^0.1.0", index = "https://github.com/daimond113/pesde-index" }, "standard"]
|
||||
result = [{ name = "lukadev_0/result", version = "^1.2.0", index = "https://github.com/daimond113/pesde-index" }, "peer"]
|
||||
semver = [{ name = "0x5eal/semver", version = "^0.1.0", index = "https://github.com/daimond113/pesde-index", target = "luau" }, "standard"]
|
||||
|
||||
[graph."compeydev/binlib"."0.1.0 lune".pkg_ref.target]
|
||||
environment = "lune"
|
||||
lib = "src/init.luau"
|
||||
|
||||
[graph."jiwonz/dirs"."0.1.2 lune"]
|
||||
ty = "standard"
|
||||
|
||||
[graph."jiwonz/dirs"."0.1.2 lune".target]
|
||||
environment = "lune"
|
||||
lib = "src/init.luau"
|
||||
|
||||
[graph."jiwonz/dirs"."0.1.2 lune".dependencies]
|
||||
"jiwonz/pathfs" = ["0.1.0 lune", "pathfs"]
|
||||
|
||||
[graph."jiwonz/dirs"."0.1.2 lune".pkg_ref]
|
||||
ref_ty = "pesde"
|
||||
name = "jiwonz/dirs"
|
||||
version = "0.1.2"
|
||||
index_url = "https://github.com/daimond113/pesde-index"
|
||||
|
||||
[graph."jiwonz/dirs"."0.1.2 lune".pkg_ref.dependencies]
|
||||
pathfs = [{ name = "jiwonz/pathfs", version = "^0.1.0", index = "https://github.com/daimond113/pesde-index" }, "standard"]
|
||||
|
||||
[graph."jiwonz/dirs"."0.1.2 lune".pkg_ref.target]
|
||||
environment = "lune"
|
||||
lib = "src/init.luau"
|
||||
|
||||
[graph."jiwonz/pathfs"."0.1.0 lune"]
|
||||
ty = "standard"
|
||||
|
||||
[graph."jiwonz/pathfs"."0.1.0 lune".target]
|
||||
environment = "lune"
|
||||
lib = "init.luau"
|
||||
|
||||
[graph."jiwonz/pathfs"."0.1.0 lune".pkg_ref]
|
||||
ref_ty = "pesde"
|
||||
name = "jiwonz/pathfs"
|
||||
version = "0.1.0"
|
||||
index_url = "https://github.com/daimond113/pesde-index"
|
||||
|
||||
[graph."jiwonz/pathfs"."0.1.0 lune".pkg_ref.target]
|
||||
environment = "lune"
|
||||
lib = "init.luau"
|
||||
|
||||
[graph."lukadev_0/option"."1.2.0 lune"]
|
||||
direct = ["option", { name = "lukadev_0/option", version = "^1.2.0" }]
|
||||
ty = "standard"
|
||||
|
||||
[graph."lukadev_0/option"."1.2.0 lune".target]
|
||||
environment = "lune"
|
||||
lib = "lib/init.luau"
|
||||
|
||||
[graph."lukadev_0/option"."1.2.0 lune".pkg_ref]
|
||||
ref_ty = "pesde"
|
||||
name = "lukadev_0/option"
|
||||
version = "1.2.0"
|
||||
index_url = "https://github.com/daimond113/pesde-index"
|
||||
|
||||
[graph."lukadev_0/option"."1.2.0 lune".pkg_ref.target]
|
||||
environment = "lune"
|
||||
lib = "lib/init.luau"
|
||||
|
||||
[graph."lukadev_0/option"."1.2.0 luau"]
|
||||
ty = "peer"
|
||||
|
||||
[graph."lukadev_0/option"."1.2.0 luau".target]
|
||||
environment = "luau"
|
||||
lib = "lib/init.luau"
|
||||
|
||||
[graph."lukadev_0/option"."1.2.0 luau".pkg_ref]
|
||||
ref_ty = "pesde"
|
||||
name = "lukadev_0/option"
|
||||
version = "1.2.0"
|
||||
index_url = "https://github.com/daimond113/pesde-index"
|
||||
|
||||
[graph."lukadev_0/option"."1.2.0 luau".pkg_ref.target]
|
||||
environment = "luau"
|
||||
lib = "lib/init.luau"
|
||||
|
||||
[graph."lukadev_0/result"."1.2.0 lune"]
|
||||
direct = ["result", { name = "lukadev_0/result", version = "^1.2.0" }]
|
||||
ty = "standard"
|
||||
|
||||
[graph."lukadev_0/result"."1.2.0 lune".target]
|
||||
environment = "lune"
|
||||
lib = "lib/init.luau"
|
||||
|
||||
[graph."lukadev_0/result"."1.2.0 lune".pkg_ref]
|
||||
ref_ty = "pesde"
|
||||
name = "lukadev_0/result"
|
||||
version = "1.2.0"
|
||||
index_url = "https://github.com/daimond113/pesde-index"
|
||||
|
||||
[graph."lukadev_0/result"."1.2.0 lune".pkg_ref.target]
|
||||
environment = "lune"
|
||||
lib = "lib/init.luau"
|
||||
|
||||
[graph."lukadev_0/result"."1.2.0 luau"]
|
||||
ty = "peer"
|
||||
|
||||
[graph."lukadev_0/result"."1.2.0 luau".target]
|
||||
environment = "luau"
|
||||
lib = "lib/init.luau"
|
||||
|
||||
[graph."lukadev_0/result"."1.2.0 luau".pkg_ref]
|
||||
ref_ty = "pesde"
|
||||
name = "lukadev_0/result"
|
||||
version = "1.2.0"
|
||||
index_url = "https://github.com/daimond113/pesde-index"
|
||||
|
||||
[graph."lukadev_0/result"."1.2.0 luau".pkg_ref.target]
|
||||
environment = "luau"
|
||||
lib = "lib/init.luau"
|
19
lune/pesde.toml
Normal file
19
lune/pesde.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
name = "compeydev/lune"
|
||||
version = "0.1.0"
|
||||
description = "A standalone Luau runtime"
|
||||
authors = ["CompeyDev <hi@devcomp.xyz>", "Filip Tibell <filip.tibell@gmail.com>"]
|
||||
repository = "https://github.com/CompeyDev/pesde-tooling/blob/main/lune"
|
||||
license = "MIT"
|
||||
|
||||
[target]
|
||||
environment = "lune"
|
||||
bin = "init.luau"
|
||||
|
||||
[dependencies]
|
||||
result = { name = "lukadev_0/result", version = "^1.2.0" }
|
||||
option = { name = "lukadev_0/option", version = "^1.2.0" }
|
||||
semver = { name = "0x5eal/semver", version = "^0.1.0", target = "luau" }
|
||||
core = { workspace = "compeydev/binlib", version = "^" }
|
||||
|
||||
[indices]
|
||||
default = "https://github.com/daimond113/pesde-index"
|
9
pesde.lock
Normal file
9
pesde.lock
Normal file
|
@ -0,0 +1,9 @@
|
|||
name = "compeydev/pesde_bins"
|
||||
version = "0.1.0"
|
||||
target = "lune"
|
||||
|
||||
[workspace."compeydev/binlib"]
|
||||
lune = "core"
|
||||
|
||||
[workspace."compeydev/lune"]
|
||||
lune = "lune"
|
13
pesde.toml
Normal file
13
pesde.toml
Normal file
|
@ -0,0 +1,13 @@
|
|||
name = "compeydev/pesde_bins"
|
||||
version = "0.1.0"
|
||||
private = true
|
||||
|
||||
workspace_members = ["lune", "core"]
|
||||
|
||||
[target]
|
||||
environment = "lune"
|
||||
|
||||
[indices]
|
||||
default = "https://github.com/daimond113/pesde-index"
|
||||
|
||||
|
Loading…
Reference in a new issue