From 5e01fa0342fb382cd5902e6a0f4c968fcb8cb51e Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Sat, 1 Jun 2013 17:04:57 +0100 Subject: [PATCH] Many new features: `build` command and dependency resolution implemented. --- babel.nim | 231 ++++++++++++++++++++++++++++++++++++++---------- packageinfo.nim | 19 ++-- version.nim | 65 +++++++++++--- 3 files changed, 251 insertions(+), 64 deletions(-) diff --git a/babel.nim b/babel.nim index 6d66025..86174b6 100644 --- a/babel.nim +++ b/babel.nim @@ -1,23 +1,25 @@ # Copyright (C) Dominik Picheta. All rights reserved. # BSD License. Look at license.txt for more info. -import httpclient, parseopt, os, strutils, osproc +import httpclient, parseopt, os, strutils, osproc, pegs, tables, parseutils -import packageinfo +import packageinfo, version type TActionType = enum - ActionNil, ActionUpdate, ActionInstall, ActionSearch, ActionList + ActionNil, ActionUpdate, ActionInstall, ActionSearch, ActionList, ActionBuild TAction = object case typ: TActionType - of ActionNil, ActionList: nil + of ActionNil, ActionList, ActionBuild: nil of ActionUpdate: optionalURL: string # Overrides default package list. of ActionInstall: optionalName: seq[string] # When this is @[], installs package from current dir. of ActionSearch: search: seq[string] # Search string. + + EBabel = object of EBase const help = """ @@ -25,6 +27,7 @@ Usage: babel COMMAND [opts] Commands: install Installs a list of packages. + build Builds a package. update Updates package list. A package list URL can be optionally specificed. search Searches for a specified package. list Lists all packages. @@ -50,6 +53,8 @@ proc parseCmdLine(): TAction = of "install": result.typ = ActionInstall result.optionalName = @[] + of "build": + result.typ = ActionBuild of "update": result.typ = ActionUpdate result.optionalURL = "" @@ -69,7 +74,7 @@ proc parseCmdLine(): TAction = result.optionalURL = key of ActionSearch: result.search.add(key) - of ActionList: + of ActionList, ActionBuild: writeHelp() of cmdLongOption, cmdShortOption: case key @@ -90,13 +95,21 @@ proc prompt(question: string): bool = else: return false -proc getBabelDir: string = return getHomeDir() / ".babel" +proc getNimrodVersion: TVersion = + let vOutput = execProcess("nimrod -v") + var matches: array[0..MaxSubpatterns, string] + if vOutput.find(peg"'Version'\s{(\d\.)+\d}", matches) == -1: + quit("Couldn't find Nimrod version.", QuitFailure) + newVersion(matches[0]) -proc getLibsDir: string = return getBabelDir() / "libs" +let babelDir = getHomeDir() / ".babel" +let libsDir = babelDir / "libs" +let binDir = babelDir / "bin" +let nimVer = getNimrodVersion() proc update(url: string = defaultPackageURL) = echo("Downloading package list from " & url) - downloadFile(url, getBabelDir() / "packages.json") + downloadFile(url, babelDir / "packages.json") echo("Done.") proc findBabelFile(dir: string): string = @@ -127,6 +140,11 @@ proc changeRoot(origRoot, newRoot, path: string): string = raise newException(EInvalidValue, "Cannot change root of path: Path does not begin with original root.") +proc doCmd(cmd: string) = + let exitCode = execCmd(cmd) + if exitCode != QuitSuccess: + quit("Execution failed with exit code " & $exitCode, QuitFailure) + proc copyFilesRec(origDir, currentDir, dest: string, pkgInfo: TPackageInfo) = for kind, file in walkDir(currentDir): if kind == pcDir: @@ -149,78 +167,189 @@ proc copyFilesRec(origDir, currentDir, dest: string, pkgInfo: TPackageInfo) = var skip = false if file.splitFile().name[0] == '.': skip = true for ignoreFile in pkgInfo.skipFiles: + if ignoreFile.endswith("babel"): + quit(ignoreFile & " must be installed.") if samePaths(file, origDir / ignoreFile): skip = true break if not skip: copyFileD(file, changeRoot(origDir, dest, file)) - -proc installFromDir(dir: string, latest: bool) = + +proc getPkgInfo(dir: string): TPackageInfo = let babelFile = findBabelFile(dir) if babelFile == "": quit("Specified directory does not contain a .babel file.", QuitFailure) - var pkgInfo = readPackageInfo(babelFile) - - let pkgDestDir = getLibsDir() / (pkgInfo.name & + result = readPackageInfo(babelFile) + +# TODO: Move to packageinfo.nim + +proc getInstalledPkgs(): seq[tuple[path: string, info: TPackageInfo]] = + ## Gets a list of installed packages + result = @[] + for kind, path in walkDir(libsDir): + if kind == pcDir: + let babelFile = findBabelFile(path) + if babelFile != "": + result.add((path, readPackageInfo(babelFile))) + else: + # TODO: Abstract logging. + echo("WARNING: No .babel file found for ", path) + +proc findPkg(pkglist: seq[tuple[path: string, info: TPackageInfo]], + dep: tuple[name: string, ver: PVersionRange], + r: var tuple[path: string, info: TPackageInfo]): bool = + for pkg in pkglist: + if pkg.info.name != dep.name: continue + if withinRange(newVersion(pkg.info.version), dep.ver): + if not result or newVersion(r.info.version) < newVersion(pkg.info.version): + r = pkg + result = true + +proc install(packages: seq[String], verRange: PVersionRange): string {.discardable.} +proc processDeps(pkginfo: TPackageInfo): seq[string] = + ## Verifies and installs dependencies. + ## + ## Returns the list of paths to pass to the compiler during build phase. + result = @[] + let pkglist = getInstalledPkgs() + for dep in pkginfo.requires: + if dep.name == "nimrod": + if not withinRange(nimVer, dep.ver): + quit("Unsatisfied dependency: " & dep.name & " (" & $dep.ver & ")") + else: + echo("Looking for ", dep.name, " (", $dep.ver, ")...") + var pkg: tuple[path: string, info: TPackageInfo] + if not findPkg(pkglist, dep, pkg): + let dest = install(@[dep.name], dep.ver) + if dest != "": + # only add if not a binary package + result.add(dest) + else: + echo("Dependency already satisfied.") + if pkg.info.bin.len == 0: + result.add(pkg.path) + +proc buildFromDir(dir: string, paths: seq[string]) = + ## Builds a package which resides in ``dir`` + var pkgInfo = getPkgInfo(dir) + var args = "" + for path in paths: args.add("--path:" & path & " ") + for bin in pkgInfo.bin: + echo("Building ", pkginfo.name, "/", bin, "...") + echo(args) + doCmd("nimrod c -d:release " & args & dir / bin) + +proc installFromDir(dir: string, latest: bool): string = + ## Returns where package has been installed to. If package is a binary, + ## ``""`` is returned. + var pkgInfo = getPkgInfo(dir) + let pkgDestDir = libsDir / (pkgInfo.name & (if latest: "" else: '-' & pkgInfo.version)) - if not existsDir(pkgDestDir): - createDir(pkgDestDir) - else: + if existsDir(pkgDestDir): if not prompt("Package already exists. Overwrite?"): quit(QuitSuccess) removeDir(pkgDestDir) - createDir(pkgDestDir) - copyFilesRec(dir, dir, pkgDestDir, pkgInfo) - echo(pkgInfo.name & " installed successfully.") - -proc doCmd(cmd: string) = - let exitCode = execCmd(cmd) - if exitCode != QuitSuccess: - quit("Execution failed with exit code " & $exitCode, QuitFailure) + echo("Installing ", pkginfo.name, "-", pkginfo.version) + + # Dependencies need to be processed before the creation of the pkg dir. + let paths = processDeps(pkginfo) + + createDir(pkgDestDir) + if pkgInfo.bin.len > 0: + buildFromDir(dir, paths) + createDir(binDir) + for bin in pkgInfo.bin: + copyFileD(dir / bin, binDir / bin) + let currentPerms = getFilePermissions(binDir / bin) + setFilePermissions(binDir / bin, currentPerms + {fpUserExec}) + # Copy the .babel to lib/ + let babelFile = findBabelFile(dir) + copyFileD(babelFile, changeRoot(dir, pkgDestDir, babelFile)) + result = "" + else: + copyFilesRec(dir, dir, pkgDestDir, pkgInfo) + echo(pkgInfo.name & " installed successfully.") + result = pkgDestDir proc getDVCSTag(pkg: TPackage): string = result = pkg.dvcsTag if result == "": result = pkg.version -proc install(packages: seq[String]) = - if packages == @[]: - installFromDir(getCurrentDir(), false) +proc getTagsList(dir: string): seq[string] = + let output = execProcess("cd \"" & dir & "\" && git tag") + if output.len > 0: + result = output.splitLines() else: - if not existsFile(getBabelDir() / "packages.json"): + result = @[] + +proc getVersionList(dir: string): TTable[TVersion, string] = + # Returns: TTable of version -> git tag name + result = initTable[TVersion, string]() + let tags = getTagsList(dir) + for tag in tags: + let i = skipUntil(tag, digits) # skip any chars before the version + # TODO: Better checking, tags can have any names. Add warnings and such. + result[newVersion(tag[i .. -1])] = tag + +proc install(packages: seq[String], verRange: PVersionRange): string = + if packages == @[]: + result = installFromDir(getCurrentDir(), false) + else: + if not existsFile(babelDir / "packages.json"): quit("Please run babel update.", QuitFailure) for p in packages: var pkg: TPackage - if getPackage(p, getBabelDir() / "packages.json", pkg): + if getPackage(p, babelDir / "packages.json", pkg): let downloadDir = (getTempDir() / "babel" / pkg.name) - let dvcsTag = getDVCSTag(pkg) + #let dvcsTag = getDVCSTag(pkg) case pkg.downloadMethod of "git": echo("Executing git...") if existsDir(downloadDir / ".git"): - doCmd("cd "& downloadDir &" && git pull") + doCmd("cd " & downloadDir & " && git pull") else: removeDir(downloadDir) doCmd("git clone --depth 1 " & pkg.url & " " & downloadDir) - if dvcsTag != "": - doCmd("cd \"" & downloadDir & "\" && git checkout " & dvcsTag) - + # TODO: Determine if version is a commit hash, if it is. Move the + # git repo to ``babelDir/libs``, then babel can simply checkout + # the correct hash instead of constantly cloning and copying. + let versions = getVersionList(downloadDir) + if versions.len > 0: + let latest = findLatest(verRange, versions) + + if latest.tag != "": + doCmd("cd \"" & downloadDir & "\" && git checkout " & latest.tag) + elif verRange.kind != verAny: + let pkginfo = getPkgInfo(downloadDir) + if pkginfo.version.newVersion notin verRange: + raise newException(EBabel, + "No versions of " & pkg.name & + " exist (this usually means that `git tag` returned nothing)." & + "Git HEAD also does not satisfy version range: " & $verRange) + # We use GIT HEAD if it satisfies our ver range + else: quit("Unknown download method: " & pkg.downloadMethod, QuitFailure) - installFromDir(downloadDir, dvcsTag == "") + result = installFromDir(downloadDir, false) else: - quit("Package not found.", QuitFailure) + raise newException(EBabel, "Package not found.") + +proc build = + var pkgInfo = getPkgInfo(getCurrentDir()) + let paths = processDeps(pkginfo) + buildFromDir(getCurrentDir(), paths) proc search(action: TAction) = assert action.typ == ActionSearch if action.search == @[]: quit("Please specify a search string.", QuitFailure) - if not existsFile(getBabelDir() / "packages.json"): + if not existsFile(babelDir / "packages.json"): quit("Please run babel update.", QuitFailure) - let pkgList = getPackageList(getBabelDir() / "packages.json") + let pkgList = getPackageList(babelDir / "packages.json") var notFound = true for pkg in pkgList: for word in action.search: @@ -241,9 +370,9 @@ proc search(action: TAction) = echo("No package found.") proc list = - if not existsFile(getBabelDir() / "packages.json"): + if not existsFile(babelDir / "packages.json"): quit("Please run babel update.", QuitFailure) - let pkgList = getPackageList(getBabelDir() / "packages.json") + let pkgList = getPackageList(babelDir / "packages.json") for pkg in pkgList: echoPackage(pkg) echo(" ") @@ -256,22 +385,30 @@ proc doAction(action: TAction) = else: update() of ActionInstall: - install(action.optionalName) + # TODO: Allow user to specify version. + install(action.optionalName, PVersionRange(kind: verAny)) of ActionSearch: search(action) of ActionList: list() + of ActionBuild: + build() of ActionNil: assert false when isMainModule: - if not existsDir(getBabelDir()): - createDir(getBabelDir()) - if not existsDir(getLibsDir()): - createDir(getLibsDir()) - - parseCmdLine().doAction() + if not existsDir(babelDir): + createDir(babelDir) + if not existsDir(libsDir): + createDir(libsDir) + when defined(release): + try: + parseCmdLine().doAction() + except EBabel: + quit("FAILURE: " & getCurrentExceptionMsg()) + else: + parseCmdLine().doAction() diff --git a/packageinfo.nim b/packageinfo.nim index 0437ef0..2eecbee 100644 --- a/packageinfo.nim +++ b/packageinfo.nim @@ -12,6 +12,7 @@ type skipDirs*: seq[string] skipFiles*: seq[string] requires*: seq[tuple[name: string, ver: PVersionRange]] + bin*: seq[string] TPackage* = object name*: string @@ -32,6 +33,7 @@ proc initPackageInfo(): TPackageInfo = result.skipDirs = @[] result.skipFiles = @[] result.requires = @[] + result.bin = @[] proc validatePackageInfo(pkgInfo: TPackageInfo, path: string) = if pkgInfo.name == "": @@ -47,9 +49,13 @@ proc validatePackageInfo(pkgInfo: TPackageInfo, path: string) = proc parseRequires(req: string): tuple[name: string, ver: PVersionRange] = try: - var i = skipUntil(req, whitespace) - result.name = req[0 .. i] - result.ver = parseVersionRange(req[i .. -1]) + if ' ' in req: + var i = skipUntil(req, whitespace) + result.name = req[0 .. i].strip + result.ver = parseVersionRange(req[i .. -1]) + else: + result.name = req.strip + result.ver = PVersionRange(kind: verAny) except EParseVersion: quit("Unable to parse dependency version range: " & getCurrentExceptionMsg()) @@ -80,12 +86,15 @@ proc readPackageInfo*(path: string): TPackageInfo = result.skipDirs.add(ev.value.split(',')) of "skipfiles": result.skipFiles.add(ev.value.split(',')) + of "bin": + result.bin = ev.value.split(',') else: quit("Invalid field: " & ev.key, QuitFailure) of "deps", "dependencies": case ev.key.normalize of "requires": - result.requires.add(parseRequires(ev.value)) + for v in ev.value.split(','): + result.requires.add(parseRequires(v.strip)) else: quit("Invalid field: " & ev.key, QuitFailure) else: quit("Invalid section: " & currentSection, QuitFailure) @@ -94,7 +103,7 @@ proc readPackageInfo*(path: string): TPackageInfo = echo(ev.msg) close(p) else: - quit("Cannot open package info: " & path, QuitFailure) + raise newException(EInvalidValue, "Cannot open package info: " & path) validatePackageInfo(result, path) proc optionalField(obj: PJsonNode, name: string): string = diff --git a/version.nim b/version.nim index 7b56628..a8c629f 100644 --- a/version.nim +++ b/version.nim @@ -2,7 +2,7 @@ # BSD License. Look at license.txt for more info. ## Module for handling versions and version ranges such as ``>= 1.0 & <= 1.5`` -import strutils +import strutils, tables, hashes, parseutils type TVersion* = distinct string @@ -31,17 +31,18 @@ proc newVersion*(ver: string): TVersion = return TVersion(ver) proc `$`*(ver: TVersion): String {.borrow.} +proc hash*(ver: TVersion): THash {.borrow.} + proc `<`*(ver: TVersion, ver2: TVersion): Bool = var sVer = string(ver).split('.') var sVer2 = string(ver2).split('.') for i in 0..max(sVer.len, sVer2.len)-1: - if i > sVer.len-1: - return True - elif i > sVer2.len-1: - return False - - var sVerI = parseInt(sVer[i]) - var sVerI2 = parseInt(sVer2[i]) + var sVerI = 0 + if i < sVer.len: + discard parseInt(sVer[i], sVerI) + var sVerI2 = 0 + if i < sVer2.len: + discard parseInt(sVer2[i], sVerI2) if sVerI < sVerI2: return True elif sVerI == sVerI2: @@ -49,7 +50,20 @@ proc `<`*(ver: TVersion, ver2: TVersion): Bool = else: return False -proc `==`*(ver: TVersion, ver2: TVersion): Bool {.borrow.} +proc `==`*(ver: TVersion, ver2: TVersion): Bool = + var sVer = string(ver).split('.') + var sVer2 = string(ver2).split('.') + for i in 0..max(sVer.len, sVer2.len)-1: + var sVerI = 0 + if i < sVer.len: + discard parseInt(sVer[i], sVerI) + var sVerI2 = 0 + if i < sVer2.len: + discard parseInt(sVer2[i], sVerI2) + if sVerI == sVerI2: + result = true + else: + return False proc `<=`*(ver: TVersion, ver2: TVersion): Bool = return (ver == ver2) or (ver < ver2) @@ -71,6 +85,9 @@ proc withinRange*(ver: TVersion, ran: PVersionRange): Bool = of verAny: return True +proc contains*(ran: PVersionRange, ver: TVersion): bool = + return withinRange(ver, ran) + proc makeRange*(version: string, op: string): PVersionRange = new(result) if version == "": @@ -110,7 +127,7 @@ proc parseVersionRange*(s: string): PVersionRange = result.verIRight = parseVersionRange(substr(s, i + 1)) # Disallow more than one verIntersect. It's pointless and could lead to - # major unknown mistakes. + # major unpredictable mistakes. if result.verIRight.kind == verIntersect: raise newException(EParseVersion, "Having more than one `&` in a version range is pointless") @@ -135,7 +152,6 @@ proc parseVersionRange*(s: string): PVersionRange = inc(i) proc `$`*(verRange: PVersionRange): String = - echo(verRange.repr()) case verRange.kind of verLater: result = "> " @@ -150,7 +166,7 @@ proc `$`*(verRange: PVersionRange): String = of verIntersect: return $verRange.verILeft & " & " & $verRange.verIRight of verAny: - return "Any" + return "any version" result.add(string(verRange.ver)) @@ -168,10 +184,22 @@ proc newVREq*(ver: string): PVersionRange = result.kind = verEq result.ver = newVersion(ver) +proc findLatest*(verRange: PVersionRange, versions: TTable[TVersion, string]): tuple[ver: TVersion, tag: string] = + result = (newVersion(""), "") + for ver, tag in versions: + if not withinRange(ver, verRange): continue + if ver > result.ver: + result = (ver, tag) + when isMainModule: doAssert(newVersion("1.0") < newVersion("1.4")) doAssert(newVersion("1.0.1") > newVersion("1.0")) doAssert(newVersion("1.0.6") <= newVersion("1.0.6")) + #doAssert(not withinRange(newVersion("0.1.0"), parseVersionRange("> 0.1"))) + doAssert(not (newVersion("0.1.0") < newVersion("0.1"))) + doAssert(not (newVersion("0.1.0") > newVersion("0.1"))) + doAssert(newVersion("0.1.0") < newVersion("0.1.0.0.1")) + doAssert(newVersion("0.1.0") <= newVersion("0.1")) var inter1 = parseVersionRange(">= 1.0 & <= 1.5") var inter2 = parseVersionRange("1.0") @@ -182,5 +210,18 @@ when isMainModule: doAssert(withinRange(newVersion("1.0.2.3.4.5.6.7.8.9.10.11.12"), inter1)) doAssert(newVersion("1") == newVersion("1")) + doAssert(newVersion("1.0.2.4.6.1.2.123") == newVersion("1.0.2.4.6.1.2.123")) + doAssert(newVersion("1.0.2") != newVersion("1.0.2.4.6.1.2.123")) + + doAssert(not (newVersion("") < newVersion("0.0.0"))) + doAssert(newVersion("") < newVersion("1.0.0")) + doAssert(newVersion("") < newVersion("0.1.0")) + + var versions = toTable[TVersion, string]({newVersion("0.1.1"): "v0.1.1", newVersion("0.2.3"): "v0.2.3", newVersion("0.5"): "v0.5"}) + doAssert findLatest(parseVersionRange(">= 0.1 & <= 0.4"), versions) == (newVersion("0.2.3"), "v0.2.3") + + + doAssert newVersion("0.1-rc1") < newVersion("0.2") + doAssert newVersion("0.1-rc1") < newVersion("0.1") echo("Everything works!") \ No newline at end of file