nimble/babel.nim
2013-10-24 19:00:22 +01:00

377 lines
12 KiB
Nim

# Copyright (C) Dominik Picheta. All rights reserved.
# BSD License. Look at license.txt for more info.
import httpclient, parseopt, os, strutils, osproc, pegs, tables, parseutils
import packageinfo, version, common, tools, download
type
TActionType = enum
ActionNil, ActionUpdate, ActionInstall, ActionSearch, ActionList, ActionBuild
TAction = object
case typ: TActionType
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.
const
help = """
Usage: babel COMMAND [opts]
Commands:
install [pkgname, ...] Installs a list of packages.
build Builds a package.
update [url] Updates package list. A package list URL can be optionally specified.
search pkg/tag Searches for a specified package. Search is performed by tag and by name.
list Lists all packages.
Options:
-h Print this help message.
-v Print version information.
"""
babelVersion = "0.1.0"
defaultPackageURL = "https://github.com/nimrod-code/packages/raw/master/packages.json"
proc writeHelp() =
echo(help)
quit(QuitSuccess)
proc writeVersion() =
echo("babel v$# compiled at $# $#" % [babelVersion, compileDate, compileTime])
quit(QuitSuccess)
proc parseCmdLine(): TAction =
result.typ = ActionNil
for kind, key, val in getOpt():
case kind
of cmdArgument:
if result.typ == ActionNil:
case key
of "install":
result.typ = ActionInstall
result.optionalName = @[]
of "build":
result.typ = ActionBuild
of "update":
result.typ = ActionUpdate
result.optionalURL = ""
of "search":
result.typ = ActionSearch
result.search = @[]
of "list":
result.typ = ActionList
else: writeHelp()
else:
case result.typ
of ActionNil:
assert false
of ActionInstall:
result.optionalName.add(key)
of ActionUpdate:
result.optionalURL = key
of ActionSearch:
result.search.add(key)
of ActionList, ActionBuild:
writeHelp()
of cmdLongOption, cmdShortOption:
case key
of "help", "h": writeHelp()
of "version", "v": writeVersion()
of cmdEnd: assert(false) # cannot happen
if result.typ == ActionNil:
writeHelp()
proc prompt(question: string): bool =
echo(question & " [y/N]")
let yn = stdin.readLine()
case yn.normalize
of "y", "yes":
return true
of "n", "no":
return false
else:
return false
let babelDir = getHomeDir() / ".babel"
let pkgsDir = babelDir / "pkgs"
let binDir = babelDir / "bin"
let nimVer = getNimrodVersion()
proc update(url: string = defaultPackageURL) =
echo("Downloading package list from " & url)
downloadFile(url, babelDir / "packages.json")
echo("Done.")
proc checkInstallFile(pkgInfo: TPackageInfo,
origDir, file: string): bool =
## Checks whether ``file`` should be installed.
## ``True`` means file should be skipped.
for ignoreFile in pkgInfo.skipFiles:
if ignoreFile.endswith("babel"):
raise newException(EBabel, ignoreFile & " must be installed.")
if samePaths(file, origDir / ignoreFile):
result = true
break
for ignoreExt in pkgInfo.skipExt:
if file.splitFile.ext == ('.' & ignoreExt):
result = true
break
if file.splitFile().name[0] == '.': result = true
proc checkInstallDir(pkgInfo: TPackageInfo,
origDir, dir: string): bool =
## Determines whether ``dir`` should be installed.
## ``True`` means dir should be skipped.
for ignoreDir in pkgInfo.skipDirs:
if samePaths(dir, origDir / ignoreDir):
result = true
break
let thisDir = splitPath(dir).tail
assert thisDir != ""
if thisDir[0] == '.': result = true
if thisDir == "nimcache": result = true
proc copyWithExt(origDir, currentDir, dest: string, pkgInfo: TPackageInfo) =
for kind, path in walkDir(currentDir):
if kind == pcDir:
copyWithExt(origDir, path, dest, pkgInfo)
else:
for iExt in pkgInfo.installExt:
if path.splitFile.ext == ('.' & iExt):
createDir(changeRoot(origDir, dest, path).splitFile.dir)
copyFileD(path, changeRoot(origDir, dest, path))
proc copyFilesRec(origDir, currentDir, dest: string, pkgInfo: TPackageInfo) =
## Copies all the required files, skips files specified in the .babel file
## (TPackageInfo).
let whitelistMode =
pkgInfo.installDirs.len != 0 or
pkgInfo.installFiles.len != 0 or
pkgInfo.installExt.len != 0
if whitelistMode:
for file in pkgInfo.installFiles:
createDir(dest / file.splitFile.dir)
copyFileD(origDir / file, dest / file)
for dir in pkgInfo.installDirs:
# TODO: Allow skipping files inside dirs?
copyDirD(origDir / dir, dest / dir)
copyWithExt(origDir, currentDir, dest, pkgInfo)
else:
for kind, file in walkDir(currentDir):
if kind == pcDir:
let skip = pkgInfo.checkInstallDir(origDir, file)
if skip: continue
# Create the dir.
createDir(changeRoot(origDir, dest, file))
copyFilesRec(origDir, file, dest, pkgInfo)
else:
let skip = pkgInfo.checkInstallFile(origDir, file)
if skip: continue
copyFileD(file, changeRoot(origDir, dest, file))
copyFileD(pkgInfo.mypath,
changeRoot(pkgInfo.mypath.splitFile.dir, dest, pkgInfo.mypath))
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(pkgsDir)
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: TPackageInfo
if not findPkg(pkglist, dep, pkg):
echo("None found, installing...")
let dest = install(@[dep.name], dep.ver)
result.add(dest)
else:
echo("Dependency already satisfied.")
result.add(pkg.mypath.splitFile.dir)
proc buildFromDir(pkgInfo: TPackageInfo, paths: seq[string]) =
## Builds a package as specified by ``pkgInfo``.
let realDir = pkgInfo.getRealDir()
var args = ""
for path in paths: args.add("--path:\"" & path & "\" ")
for bin in pkgInfo.bin:
echo("Building ", pkginfo.name, "/", bin, " using ", pkgInfo.backend,
" backend...")
doCmd("nimrod $# -d:release $# \"$#\"" %
[pkgInfo.backend, args, realDir / bin.changeFileExt("nim")])
proc installFromDir(dir: string, latest: bool): string =
## Returns where package has been installed to.
## The return value of this function is used by
## ``processDeps`` to gather a list of paths to pass to the nimrod compiler.
var pkgInfo = getPkgInfo(dir)
let realDir = pkgInfo.getRealDir()
let pkgDestDir = pkgsDir / (pkgInfo.name &
(if latest: "" else: '-' & pkgInfo.version))
if existsDir(pkgDestDir):
if not prompt(pkgInfo.name & " already exists. Overwrite?"):
quit(QuitSuccess)
removeDir(pkgDestDir)
# Remove any symlinked binaries
for bin in pkgInfo.bin:
# TODO: Check that this binary belongs to the package being installed.
when defined(windows):
removeFile(binDir / bin.changeFileExt("bat"))
else:
removeFile(binDir / bin)
echo("Installing ", pkginfo.name, "-", pkginfo.version)
# Dependencies need to be processed before the creation of the pkg dir.
let paths = processDeps(pkginfo)
if pkgInfo.bin.len > 0: buildFromDir(pkgInfo, paths)
createDir(pkgDestDir)
if pkgInfo.bin.len > 0:
createDir(binDir)
# Copy all binaries and files that are not skipped
copyFilesRec(realDir, realDir, pkgDestDir, pkgInfo)
# Set file permissions to +x for all binaries built,
# and symlink them on *nix OS' to $babelDir/bin/
for bin in pkgInfo.bin:
if not existsFile(pkgDestDir / bin):
copyFileD(realDir / bin, pkgDestDir / bin)
let currentPerms = getFilePermissions(pkgDestDir / bin)
setFilePermissions(pkgDestDir / bin, currentPerms + {fpUserExec})
when defined(unix):
if existsFile(binDir / bin): removeFile(binDir / bin)
echo("Creating symlink: ", pkgDestDir / bin, " -> ", binDir / bin)
doCmd("ln -s \"" & pkgDestDir / bin & "\" " & binDir / bin)
elif defined(windows):
let dest = binDir / bin.changeFileExt("bat")
echo("Creating stub: ", pkgDestDir / bin, " -> ", dest)
writeFile(dest, "\"" & pkgDestDir / bin & "\" %*\n")
else:
{.error: "Sorry, your platform is not supported.".}
else:
copyFilesRec(realDir, realDir, pkgDestDir, pkgInfo)
result = pkgDestDir
echo(pkgInfo.name & " installed successfully.")
proc downloadPkg(pkg: TPackage, verRange: PVersionRange): string =
let downloadDir = (getTempDir() / "babel" / pkg.name)
if not existsDir(getTempDir() / "babel"): createDir(getTempDir() / "babel")
echo("Downloading ", pkg.name, " into ", downloadDir, "...")
doDownload(pkg, downloadDir, verRange)
result = downloadDir
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, babelDir / "packages.json", pkg):
let downloadDir = downloadPkg(pkg, verRange)
result = installFromDir(downloadDir, false)
else:
raise newException(EBabel, "Package not found.")
proc build =
var pkgInfo = getPkgInfo(getCurrentDir())
let paths = processDeps(pkginfo)
buildFromDir(pkgInfo, paths)
proc search(action: TAction) =
assert action.typ == ActionSearch
if action.search == @[]:
quit("Please specify a search string.", QuitFailure)
if not existsFile(babelDir / "packages.json"):
quit("Please run babel update.", QuitFailure)
let pkgList = getPackageList(babelDir / "packages.json")
var notFound = true
for pkg in pkgList:
for word in action.search:
if word in pkg.tags:
echoPackage(pkg)
echo(" ")
notFound = false
break
if notFound:
# Search by name.
for pkg in pkgList:
for word in action.search:
if word in pkg.name:
echoPackage(pkg)
echo(" ")
notFound = false
if notFound:
echo("No package found.")
proc list =
if not existsFile(babelDir / "packages.json"):
quit("Please run babel update.", QuitFailure)
let pkgList = getPackageList(babelDir / "packages.json")
for pkg in pkgList:
echoPackage(pkg)
echo(" ")
proc doAction(action: TAction) =
case action.typ
of ActionUpdate:
if action.optionalURL != "":
update(action.optionalURL)
else:
update()
of ActionInstall:
# 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(babelDir):
createDir(babelDir)
if not existsDir(pkgsDir):
createDir(pkgsDir)
when defined(release):
try:
parseCmdLine().doAction()
except EBabel:
quit("FAILURE: " & getCurrentExceptionMsg())
else:
parseCmdLine().doAction()