diff --git a/src/nimble.nim b/src/nimble.nim index 4d3ef24..8ea5070 100644 --- a/src/nimble.nim +++ b/src/nimble.nim @@ -7,7 +7,8 @@ import httpclient, parseopt, os, strutils, osproc, pegs, tables, parseutils, from sequtils import toSeq import nimblepkg/packageinfo, nimblepkg/version, nimblepkg/tools, - nimblepkg/download, nimblepkg/config, nimblepkg/nimbletypes + nimblepkg/download, nimblepkg/config, nimblepkg/nimbletypes, + nimblepkg/publish when not defined(windows): from posix import getpid @@ -37,12 +38,13 @@ type nimbleData: JsonNode ## Nimbledata.json ActionType = enum - actionNil, actionUpdate, actionInit, actionInstall, actionSearch, + actionNil, actionUpdate, actionInit, actionPublish, + actionInstall, actionSearch, actionList, actionBuild, actionPath, actionUninstall, actionCompile Action = object case typ: ActionType - of actionNil, actionList, actionBuild: nil + of actionNil, actionList, actionBuild, actionPublish: nil of actionUpdate: optionalURL: string # Overrides default package list. of actionInstall, actionPath, actionUninstall: @@ -69,6 +71,9 @@ Usage: nimble COMMAND [opts] Commands: install [pkgname, ...] Installs a list of packages. init [pkgname] Initializes a new Nimble project. + publish Publishes a package on nim-lang/packages. + The current working directory needs to be the + toplevel directory of the Nimble package. uninstall [pkgname, ...] Uninstalls a list of packages. build Builds a package. c, cc, js [opts, ...] f.nim Builds a file inside a package. Passes options @@ -190,6 +195,8 @@ proc parseCmdLine(): Options = of "uninstall", "remove", "delete", "del", "rm": result.action.typ = actionUninstall result.action.packages = @[] + of "publish": + result.action.typ = actionPublish else: writeHelp() else: case result.action.typ @@ -215,7 +222,7 @@ proc parseCmdLine(): Options = result.action.projName = key of actionCompile: result.action.file = key - of actionList, actionBuild: + of actionList, actionBuild, actionPublish: writeHelp() else: discard @@ -965,6 +972,9 @@ proc doAction(options: Options) = compile(options) of actionInit: init(options) + of actionPublish: + var pkgInfo = getPkgInfo(getCurrentDir()) + publish(pkgInfo) of actionNil: assert false diff --git a/src/nimblepkg/nimbletypes.nim b/src/nimblepkg/nimbletypes.nim index 3ee3fc8..947764b 100644 --- a/src/nimblepkg/nimbletypes.nim +++ b/src/nimblepkg/nimbletypes.nim @@ -3,6 +3,28 @@ # Various miscellaneous common types reside here, to avoid problems with # recursive imports +import version + type NimbleError* = object of Exception BuildFailed* = object of NimbleError + + PackageInfo* = object + mypath*: string ## The path of this .nimble file + name*: string + version*: string + author*: string + description*: string + license*: string + skipDirs*: seq[string] + skipFiles*: seq[string] + skipExt*: seq[string] + installDirs*: seq[string] + installFiles*: seq[string] + installExt*: seq[string] + requires*: seq[PkgTuple] + bin*: seq[string] + binDir*: string + srcDir*: string + backend*: string + diff --git a/src/nimblepkg/packageinfo.nim b/src/nimblepkg/packageinfo.nim index c58e32d..45ca2d2 100644 --- a/src/nimblepkg/packageinfo.nim +++ b/src/nimblepkg/packageinfo.nim @@ -3,28 +3,6 @@ import parsecfg, json, streams, strutils, parseutils, os import version, tools, nimbletypes type - ## Tuple containing package name and version range. - PkgTuple* = tuple[name: string, ver: VersionRange] - - PackageInfo* = object - mypath*: string ## The path of this .nimble file - name*: string - version*: string - author*: string - description*: string - license*: string - skipDirs*: seq[string] - skipFiles*: seq[string] - skipExt*: seq[string] - installDirs*: seq[string] - installFiles*: seq[string] - installExt*: seq[string] - requires*: seq[PkgTuple] - bin*: seq[string] - binDir*: string - srcDir*: string - backend*: string - Package* = object # Required fields in a package. name*: string diff --git a/src/nimblepkg/publish.nim b/src/nimblepkg/publish.nim new file mode 100644 index 0000000..c8aaa15 --- /dev/null +++ b/src/nimblepkg/publish.nim @@ -0,0 +1,191 @@ +# Copyright (C) Andreas Rumpf. All rights reserved. +# BSD License. Look at license.txt for more info. + +## Implements 'nimble publish' to create a pull request against +## nim-lang/packages automatically. + +import httpclient, base64, strutils, rdstdin, json, os +import tools, nimbletypes + +type + Auth = object + user: string + pw: string + token: string ## base64 encoding of user:pw + +proc userAborted() = + raise newException(NimbleError, "User aborted the process.") + +proc getGithubAuth(): Auth = + var user = "" + let (output, exitCode) = doCmdEx("git config user.name") + if exitCode == 0: + user = output.string.strip + if user.len == 0: + user = readLineFromStdin("Github user name: ") + if user.len == 0: userAborted() + let pw = readPasswordFromStdin("Github password for " & user & ": ") + if pw.len == 0: userAborted() + result.user = user + result.pw = pw + result.token = encode(user & ':' & pw) + +proc searchFork(j: JsonNode): bool = + # Searches for: "fork":true recursively. + case j.kind + of JObject: + for k, v in items(j.fields): + if k == "fork" and v.kind == JBool: return v.bval + for k, v in items(j.fields): + if searchFork(v): return true + of JArray: + for x in j.elems: + if searchFork(x): return true + else: discard + +proc forkExists(a: Auth): bool = + try: + let x = getContent("https://api.github.com/repos/" & a.user & "/packages", + extraHeaders=("Authorization: Basic $1\c\L" % a.token) & + "Content-Type: application/x-www-form-urlencoded\c\L" & + "Accept: */*\c\L") + let j = parseJson(x) + result = searchFork(j) + except JsonParsingError, IOError: + result = false + +proc createFork(a: Auth) = + discard postContent("https://api.github.com/repos/nim-lang/packages/forks", + extraHeaders=("Authorization: Basic $1\c\L" % a.token) & + "Content-Type: application/x-www-form-urlencoded\c\L" & + "Accept: */*\c\L") + +proc createPullRequest(a: Auth; packageName: string) = + echo "creating PR" + discard postContent("https://api.github.com/repos/nim-lang/packages/pulls", + extraHeaders=("Authorization: Basic $1\c\L" % a.token) & + "Content-Type: application/x-www-form-urlencoded\c\L" & + "Accept: */*\c\L", + body="""{"title": "Add package $#", "head": "$#:master", + "base": "master"}""" % [packageName, a.user]) + +proc `%`(s: openArray[string]): JsonNode = + result = newJArray() + for x in s: result.add(%x) + +proc cleanupWhitespace(s: string): string = + ## Removes trailing whitespace and normalizes line endings to LF. + result = newStringOfCap(s.len) + var i = 0 + while i < s.len: + if s[i] == ' ': + var j = i+1 + while s[j] == ' ': inc j + if s[j] == '\c': + inc j + if s[j] == '\L': inc j + result.add '\L' + i = j + elif s[j] == '\L': + result.add '\L' + i = j+1 + else: + result.add ' ' + inc i + elif s[i] == '\c': + inc i + if s[i] == '\L': inc i + result.add '\L' + elif s[i] == '\L': + result.add '\L' + inc i + else: + result.add s[i] + inc i + +proc editJson(p: PackageInfo; url, tags, downloadMethod: string) = + var contents = parseFile("packages.json") + doAssert contents.kind == JArray + contents.add(%{ + "name": %p.name, + "url": %url, + "method": %downloadMethod, + "tags": %tags.split(), + "description": %p.description, + "license": %p.license, + "web": %url}) + writeFile("packages.json", contents.pretty.cleanupWhitespace) + +proc getPackageOriginUrl(a: Auth): string = + ## Adds 'user:pw' to the URL so that the user is not asked *again* for it. + ## We need this for 'git push'. + let (output, exitCode) = doCmdEx("git config --get remote.origin.url") + result = "origin" + if exitCode == 0: + result = output.string.strip + if result.endsWith(".git"): result.setLen(result.len - 4) + if result.startsWith("https://"): + result = "https://" & a.user & ':' & a.pw & '@' & + result["https://".len .. ^1] + +proc publish*(p: PackageInfo) = + ## Publishes the package p. + let auth = getGithubAuth() + let parent = os.getCurrentDir().parentDir() + var pkgsDir = parent / "nimble-packages-fork" + if not forkExists(auth): + createFork(auth) + echo "waiting 10s to let Github create a fork ..." + os.sleep(10_000) + if dirExists(pkgsDir): + pkgsDir = readLineFromStdin("Directory where to clone into: ") + if pkgsDir.len == 0: userAborted() + echo "... done; cloning packages into: ", pkgsDir + cd parent: + doCmd("git clone https://github.com/" & auth.user & "/packages " & pkgsDir) + # Use SSH instead of HTTPS so that the user isn't bothered with the + # password for 'git push': + doCmd("git remote set-url origin git@github.com:$1/packages.git" % + auth.user) + elif not dirExists(pkgsDir): + pkgsDir = readLineFromStdin("According to github, you already forked " & + "nim-lang/packages.\n" & + "Please give the path to it: ") + if pkgsDir.len == 0: userAborted() + if not dirExists(pkgsDir): + raise newException(NimbleError, + "Cannot find nimble-packages-fork git repository. Stopping.") + + # We need to do this **before** the cd: + var url = "" + var downloadMethod = "" + if dirExists(os.getCurrentDir() / ".git"): + let (output, exitCode) = doCmdEx("git config --get remote.origin.url") + if exitCode == 0: + url = output.string.strip + if url.endsWith(".git"): url.setLen(url.len - 4) + downloadMethod = "git" + elif dirExists(os.getCurrentDir() / ".hg"): + downloadMethod = "hg" + else: + raise newException(NimbleError, + "No .git nor .hg directory found. Stopping.") + + if url.len == 0: + url = readLineFromStdin("Github URL of " & p.name & ": ") + if url.len == 0: userAborted() + + let tags = readLineFromStdin("Please enter a whitespace separated list of tags: ") + + cd pkgsDir: + editJson(p, url, tags, downloadMethod) + doCmd("git commit packages.json -m \"Added package " & p.name & "\"") + echo pkgsDir, " git push origin master" + doCmd("git push " & getPackageOriginUrl(auth) & " master") + createPullRequest(auth, p.name) + echo "Pull request successful." + +when isMainModule: + import packageinfo + var p = getPkgInfo(getCurrentDir()) + publish(p) diff --git a/src/nimblepkg/version.nim b/src/nimblepkg/version.nim index 1960882..7eb1263 100644 --- a/src/nimblepkg/version.nim +++ b/src/nimblepkg/version.nim @@ -29,6 +29,9 @@ type of verAny: nil + ## Tuple containing package name and version range. + PkgTuple* = tuple[name: string, ver: VersionRange] + ParseVersionError* = object of ValueError proc newVersion*(ver: string): Version = return Version(ver)