import std / [ macros, strformat, strutils ] macro tessera*(packageName: untyped, body: untyped): untyped = ## Small declerative DSL for defining packages ## The schema is: ## package "name": ## source: "" ## 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`()