diff options
Diffstat (limited to '')
| -rw-r--r-- | src/lapis.nim | 210 |
1 files changed, 210 insertions, 0 deletions
diff --git a/src/lapis.nim b/src/lapis.nim new file mode 100644 index 0000000..4a1940c --- /dev/null +++ b/src/lapis.nim @@ -0,0 +1,210 @@ +import std / [ + macros, + strformat, + strutils +] + +macro tessera*(packageName: untyped, body: untyped): untyped = + ## Small declerative DSL for defining packages + ## The schema is: + ## package "name": + ## source: "<url>" + ## patches: @[...] (if any) + ## dependencies: @[...] (if any) + ## build: @[...] + ## result: @[...] + ## + ## The macro expands at compile time into a build() procedure. + ## When run it: + ## - verifies the `dependencies` are installed properly + ## ( this rn means that it checks $PATH, + ## since I haven't worked on mosaic yet ) + ## ( if a dependency isn't installed, it installs it ) + ## - uses cURL to fetch the tarball from the `sourceURL` + ## - extracts the tarball, and cd's into it + ## - applies `patches` (if any) + ## - runs the build commands defined in `build: @[...]` + + if packageName.kind == nnkIdent: + error("Name of package must be a string: {packageName}->\"{packageName}\"") + + body.expectKind nnkStmtList + + var + nameLit = newLit($toStrLit(packageName)) + sourceNode = newLit("") + patchesNode = newLit(newSeq[string]()) + depsNode = newLit(newSeq[string]()) + buildNode = newLit(newSeq[string]()) + resultNode = newLit("") + + for stmt in body: + if stmt.kind == nnkCall: + let key = $stmt[0] + let value = stmt[1] + case key: + of "source": sourceNode = value + of "patches": patchesNode = value + of "dependencies": depsNode = value + of "build": buildNode = value + of "result": resultNode = value + else: + error("tessera: unknown field {key}".fmt) + else: + error("tessera: unexpected statement in package block: {stmt.repr}".fmt) + + let buildIdent = ident("build_{packageName}".fmt) + + result = quote do: + import os, osproc, strutils + + const + pkgName = `nameLit` + pkgSource = `sourceNode` + pkgPatches = `patchesNode` + pkgDeps = `depsNode` + pkgBuild = `buildNode` + pkgResult = `resultNode` + + # Helper procs + proc extractFilename(url: string): string = + if url.len == 0: + return "" + let parts = url.split('/') + result = parts[^1] + + proc stripExtension(filename: string): string = + result = filename + # list of tar suffixes from Wikipedia + let suffixList = @[ + ".tar.gz", + ".tar.xz", + ".tar.bz2", + ".tar.lz", + ".tar.lzma", + ".tar.lzo", + ".tar.Z", + ".tar.zst", + ".tb2", + ".tbz", + ".tbz2", + ".tz2", + ".taz", + ".tgz", + ".tlz", + ".txz", + ".tZ", + ".taZ", + ".tzst" + ] + for suffix in suffixList: + if result.endsWith(suffix): + result.removeSuffix(suffix) + + proc runCommand(command: string, workingDir: string = ""): string = + echo "Running command: " & command + let (output, exitCode) = execCmdEx(command, workingDir=workingDir) + if exitCode != 0: + quit("Command failed with exit code: \"" & $exitCode & "\":\n" & $output) + result = output + + proc isExecutable(filename: string): bool = + let permissions = getFilePermissions(filename) + result = ((fpUserExec in permissions) or (fpGroupExec in permissions)) + + proc isInstalled(name: string): bool = + result = false + let possibleFolders = @["/usr/bin/", "/usr/sbin/", "/usr/lib/"] + for folder in possibleFolders: + if fileExists(folder & name): + result = true + + # TODO: Make this not depend on the tar file + proc ensureDependency(dependency: string, tarFile: string) = + if dependency.len > 0: + # TODO: When I fix the declerativeness, I must also fix this + let localInstaller = "/mosaic/panel/" & $dependency + echo "checking " & localInstaller & "..." + + if not (fileExists(localInstaller) or isExecutable(localInstaller)): + quit("dependency \"" & $dependency & "\" not defined yet.") + + let output = runCommand("/mosaic/panel/" & $dependency) + echo output + + proc `buildIdent`() = + echo "Building " & $pkgName + + let + mosaicSourceFolder = "/mosaic/quarry/" + sourceFile = $mosaicSourceFolder & extractFilename(url=pkgSource) + + # Step 1: check dependencies + if isInstalled(pkgResult): + echo pkgName & " already installed..." + return + + if pkgDeps.len > 0: + for dependency in pkgDeps: + echo "checking dependency: " & $dependency + ensureDependency(dependency=dependency, tarFile=sourceFile) + + # Step 2: fetch source via cURL + var expectedFolder = stripExtension(sourceFile) & "/" + echo "Making " & expectedFolder & "..." + createDir(expectedFolder) + + if not fileExists(sourceFile): + echo "Fetching source: " & $pkgSource & " -> " & sourceFile + + let wgetOutput = runCommand(command="wget " & $pkgSource, workingDir=mosaicSourceFolder) + echo "wget " & wgetOutput + else: + echo $sourceFile & " exists. Continuing..." + + # Step 3: untar + echo "extracting " & $sourceFile & "..." + let tarOutput = runCommand(command="tar -xf " & $sourceFile, workingDir=mosaicSourceFolder) + echo tarOutput + + var untarFolders: seq[string] = @[] + for folder in walkDir(expectedFolder): + untarFolders.add(folder.path) + + echo untarFolders + if untarFolders.len == 0: + quit("No folders produces in untarring step (?)... Quitting...") + if untarFolders.len == 1: + expectedFolder = untarFolders[0] + + # Step 5 + if pkgPatches.len > 0: + for patchURL in pkgPatches: + if patchURL.len > 0: + let patchFile = extractFilename(url=patchURL) + + echo "Fetching patch: " & $patchURL & " -> " & $patchFile + + let wgetPatchOutput = runCommand(command="wget " & $patchURL, workingDir=mosaicSourceFolder) + echo wgetPatchOutput + assert fileExists(patchFile) + + # Step 6: Run the build commands: + if pkgBuild.len == 0: + quit("No commands given to run for " & $pkgName & "... Nothing to do.") + + var newWorkingFolder = expectedFolder + echo newWorkingFolder + for command in pkgBuild: + echo command + if command.startsWith("cd "): + newWorkingFolder = newWorkingFolder & command[3..^1] + continue + + let buildOutput = runCommand(command=command, workingDir=newWorkingFolder) + echo buildOutput + + echo "tessera " & $pkgName & "built." + + when isMainModule: + `buildIdent`() |
