diff --git a/.gitignore b/.gitignore index 89c82db..faff7f5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,4 @@ -tests/tconfig -tests/tnimble -tests/tpackage -tests/tspec -tests/tgit -tests/ttags +nimblemeta.json nim.cfg nimph.exe libcurl.so* @@ -14,3 +9,6 @@ libmbedx509.so* libnghttp2.so* libssh2.so* libz.so* +bin +deps +cache diff --git a/bin/empty.txt b/bin/empty.txt new file mode 100644 index 0000000..e69de29 diff --git a/bootstrap-nonimble.sh b/bootstrap-nonimble.sh index 154ddbb..b62c3ee 100755 --- a/bootstrap-nonimble.sh +++ b/bootstrap-nonimble.sh @@ -23,8 +23,9 @@ git clone --depth 1 https://github.com/disruptek/badresults.git git clone --depth 1 https://github.com/disruptek/github.git git clone --depth 1 https://github.com/disruptek/rest.git git clone --depth 1 https://github.com/disruptek/foreach.git +git clone --depth 1 https://github.com/disruptek/ups.git nim c --define:release --path:nim-regex/src --path:nim-unicodedb/src --path:nim-unicodeplus/src --path:nim-segmentation/src --path:cligen nimterop/nimterop/toast.nim -nim c --outdir:.. --define:$RELEASE --path:cligen --path:foreach --path:github/src --path:rest --path:npeg/src --path:jsonconvert --path:badresults --path:bump --path:cutelog --path:gittyup --path:nimgit2 --path:nimterop --path:nim-regex/src --path:nim-unicodedb/src --path:nim-unicodeplus/src --path:nim-segmentation/src nimph.nim +nim c --outdir:.. --define:$RELEASE --path:ups --path:cligen --path:foreach --path:github/src --path:rest --path:npeg/src --path:jsonconvert --path:badresults --path:bump --path:cutelog --path:gittyup --path:nimgit2 --path:nimterop --path:nim-regex/src --path:nim-unicodedb/src --path:nim-unicodeplus/src --path:nim-segmentation/src nimph.nim cd .. if test -x nimph; then diff --git a/src/nimph.nim b/nimph.nim similarity index 86% rename from src/nimph.nim rename to nimph.nim index 252540e..3d1c498 100644 --- a/src/nimph.nim +++ b/nimph.nim @@ -11,17 +11,20 @@ import bump import gittyup import badresults +import ups/spec as upspec +import ups/paths +import ups/runner +import ups/config + import nimph/spec -import nimph/runner -import nimph/project +import nimph/projects import nimph/doctor import nimph/thehub -import nimph/config -import nimph/package -import nimph/dependency -import nimph/locker -import nimph/group -import nimph/requirement +import nimph/packages +import nimph/dependencies +import nimph/lockers +import nimph/groups +import nimph/requirements template crash(why: string) = ## a good way to exit nimph @@ -58,20 +61,19 @@ template prepareForTheWorst(body: untyped) = else: body -template setupLocalProject(project: var Project; body: untyped) = - if not findProject(project, getCurrentDir()): - body - else: +proc setupLocalProject(): Project = + prepareForTheWorst: + result = findProject(getCurrentDir().toAbsoluteDir) + if result.isNil: + error &"unable to find a project; try `git init .`?" + quit 1 try: - debug "load all configs" - project.cfg = loadAllCfgs(project.repo) + debug &"load all configs from {result.root}" + result.cfg = loadAllCfgs(result.root) debug "done loading configs" except Exception as e: - crash "unable to parse nim configuration: " & e.msg - -template setupLocalProject(project: var Project) = - setupLocalProject(project): - crash &"unable to find a project; try `nimble init`?" + raise newException(ValueError, + "unable to parse nim configuration: " & e.msg) template toggle(flags: set[Flag]; flag: Flag; switch: untyped) = when switch is bool: @@ -93,22 +95,21 @@ template composeFlags(defaults): set[Flag] = toggle(flags, Network, network) flags -proc findChildProjectUsing(group: DependencyGroup; name: string; - flags: set[Flag]): Result[Project, string] = +proc findChildProjectUsing(group: DependencyGroup; name: string): Result[Project, string] = ## search the group for a named project using options specified in flags + var name = importName name let - name = name.destylize found = group.projectForName(name) block complete: var nature = "dependency" if found.isSome: - result.ok found.get + result.ok get(found) break complete elif Strict notin flags: for child in group.projects.values: - if child.importName.destylize == name: + if child.importName == name: result.ok child break complete nature = "project" @@ -125,14 +126,16 @@ proc searcher*(args: seq[string]; strict = false; if args.len == 0: crash &"a search was requested but no query parameters were provided" - let - group = waitfor searchHub(args) - if group.isNone: - crash &"unable to retrieve search results from github" - for repo in group.get.reversed: - fatal "\n" & repo.renderShortly - if group.get.len == 0: - fatal &"😢no results" + else: + let + group = waitfor searchHub(args) + if group.isNone: + crash &"unable to retrieve search results from github" + elif get(group).len == 0: + fatal &"😢no results" + else: + for repo in get(group).backwards: + fatal "\n" & repo.renderShortly proc fixer*(strict = false; log_level = logLevel; safe_mode = false; quiet = false; @@ -143,9 +146,10 @@ proc fixer*(strict = false; setLogFilter(log_level) var - project: Project - setupLocalProject(project) + project = setupLocalProject() + if dry_run: + flags.push flags + {DryRun} if project.doctor(dry = dry_run): fatal &"👌{project.name} version {project.version} lookin' good" elif not dry_run: @@ -162,8 +166,7 @@ proc nimbler*(args: seq[string]; strict = false; setLogFilter(log_level) var - project: Project - setupLocalProject(project) + project = setupLocalProject() let nimble = project.runSomething("nimble", args) @@ -179,17 +182,15 @@ proc pather*(names: seq[string]; strict = false; setLogFilter(log_level) # setup flags for the operation - let flags = composeFlags(defaultFlags) - + flags.push composeFlags(defaultFlags): var - project: Project - setupLocalProject(project) + project = setupLocalProject() if names.len == 0: crash &"give me an import name to retrieve its filesystem path" # setup our dependency group - var group = project.newDependencyGroup(flags = flags) + var group = project.newDependencyGroup() if not project.resolve(group): notice &"unable to resolve all dependencies for {project}" @@ -200,14 +201,14 @@ proc pather*(names: seq[string]; strict = false; for name in names.items: var - child = group.findChildProjectUsing(name, flags = flags) + child = group.findChildProjectUsing(name) if child.isOk: - echo child.get.repo + echo get(child).root else: error child.error result = 1 -proc runner*(args: seq[string]; git = false; strict = false; +proc runion*(args: seq[string]; git = false; strict = false; log_level = logLevel; safe_mode = false; quiet = true; network = true; force = false; dry_run = false): int = ## this is another pather, basically, that invokes the arguments in the path @@ -219,28 +220,27 @@ proc runner*(args: seq[string]; git = false; strict = false; setLogFilter(log_level) # setup flags for the operation - let flags = composeFlags(defaultFlags) - - var - project: Project - setupLocalProject(project) - - # setup our dependency group - var group = project.newDependencyGroup(flags = flags) - if not project.resolve(group): - notice &"unable to resolve all dependencies for {project}" + flags.push composeFlags(defaultFlags) - # make sure we visit every project that fits the requirements - for req, dependency in group.pairs: - for child in dependency.projects.values: - if child.dist == Git or not git: - withinDirectory(child.repo): - info &"running {exe} in {child.repo}" - let - got = project.runSomething(exe, args) - if not got.ok: - error &"{exe} didn't like that in {child.repo}" - result = 1 + var + project = setupLocalProject() + + # setup our dependency group + var group = project.newDependencyGroup() + if not project.resolve(group): + notice &"unable to resolve all dependencies for {project}" + + # make sure we visit every project that fits the requirements + for req, dependency in group.pairs: + for child in dependency.projects.values: + if child.dist == Git or not git: + withinDirectory(child.root): + info &"running {exe} in {child.root}" + let + got = project.runSomething(exe, args) + if not got.ok: + error &"{exe} didn't like that in {child.root}" + result = 1 proc rollChild(child: var Project; requirement: Requirement; goal: RollGoal; safe_mode = false; dry_run = false): bool = @@ -299,14 +299,13 @@ proc updowner*(names: seq[string]; goal: RollGoal; strict = false; setLogFilter(log_level) # setup flags for the operation - let flags = composeFlags(defaultFlags) + flags.push composeFlags(defaultFlags) var - project: Project - setupLocalProject(project) + project = setupLocalProject() # setup our dependency group - var group = project.newDependencyGroup(flags = flags) + var group = project.newDependencyGroup() if not project.resolve(group): notice &"unable to resolve all dependencies for {project}" @@ -321,12 +320,12 @@ proc updowner*(names: seq[string]; goal: RollGoal; strict = false; for name in names.items: let found = group.projectForName(name) if found.isSome: - var child = found.get + var child = get(found) let require = group.reqForProject(child) if require.isNone: let emsg = &"found `{name}` but not its requirement" # noqa raise newException(ValueError, emsg) - if not child.rollChild(require.get, goal = goal, dry_run = dry_run): + if not child.rollChild(get(require), goal = goal, dry_run = dry_run): result = 1 else: error &"couldn't find `{name}` among our installed dependencies" @@ -345,14 +344,13 @@ proc roller*(names: seq[string]; strict = false; setLogFilter(log_level) # setup flags for the operation - let flags = composeFlags(defaultFlags) + flags.push composeFlags(defaultFlags) var - project: Project - setupLocalProject(project) + project = setupLocalProject() # setup our dependency group - var group = project.newDependencyGroup(flags = flags) + var group = project.newDependencyGroup() if not project.resolve(group): notice &"unable to resolve all dependencies for {project}" @@ -379,7 +377,7 @@ proc roller*(names: seq[string]; strict = false; # everything seems groovy at the beginning result = 0 # add each requirement to the dependency tree - for requirement in requires.get.values: + for requirement in get(requires).values: var dependency = newDependency(requirement) # we really don't care if requirements are added here @@ -404,8 +402,9 @@ proc roller*(names: seq[string]; strict = false; else: fatal &"👎{project.name} is not where you want it" -proc graphProject(project: var Project; path: string; log_level = logLevel) = - fatal " directory: " & path +proc graphProject(project: var Project; path: AbsoluteDir; + log_level = logLevel) = + fatal " directory: " & $path fatal " project: " & $project if project.dist == Git: # show tags for info or less @@ -450,8 +449,7 @@ proc grapher*(names: seq[string]; strict = false; let flags = composeFlags(defaultFlags) var - project: Project - setupLocalProject(project) + project = setupLocalProject() # setup our dependency group var group = project.newDependencyGroup(flags = flags) @@ -476,14 +474,14 @@ proc grapher*(names: seq[string]; strict = false; result = 1 else: fatal "" - let require = group.reqForProject(child.get) + let require = group.reqForProject(get child) if require.isNone: notice &"found `{name}` but not its requirement" # noqa - child.get.graphProject(child.get.repo, log_level = log_level) + get(child).graphProject(get(child).root, log_level = log_level) else: {.warning: "nim bug #12818".} for requirement, dependency in group.mpairs: - if requirement == require.get: + if requirement == get(require): dependency.graphDep(requirement, log_level = log_level) proc dumpLockList(project: Project) = @@ -501,8 +499,7 @@ proc lockfiler*(names: seq[string]; strict = false; setLogFilter(log_level) var - project: Project - setupLocalProject(project) + project = setupLocalProject() block: let name = names.join(" ") @@ -525,8 +522,7 @@ proc unlockfiler*(names: seq[string]; strict = false; setLogFilter(log_level) var - project: Project - setupLocalProject(project) + project = setupLocalProject() block: let name = names.join(" ") @@ -549,8 +545,7 @@ proc tagger*(strict = false; setLogFilter(log_level) var - project: Project - setupLocalProject(project) + project = setupLocalProject() if project.fixTags(dry_run = dry_run, force = force): if dry_run: @@ -572,8 +567,7 @@ proc forker*(names: seq[string]; strict = false; let flags = composeFlags(defaultFlags) var - project: Project - setupLocalProject(project) + project = setupLocalProject() # setup our dependency group var group = project.newDependencyGroup(flags = flags) @@ -593,21 +587,21 @@ proc forker*(names: seq[string]; strict = false; result = 1 continue let - fork = child.get.forkTarget + fork = get(child).forkTarget if not fork.ok: error fork.why result = 1 continue - info &"🍴forking {child.get}" + info &"🍴forking {get(child)}" let forked = waitfor forkHub(fork.owner, fork.repo) if forked.isNone: result = 1 continue - fatal &"🔱{forked.get.web}" - case child.get.dist: + fatal &"🔱{get(forked).web}" + case get(child).dist: of Git: let name = defaultRemote - if not child.get.promoteRemoteLike(forked.get.git, name = name): + if not get(child).promoteRemoteLike(get(forked).git, name = name): notice &"unable to promote new fork to {name}" else: {.warning: "optionally upgrade a gitless install to clone".} @@ -641,8 +635,8 @@ proc cloner*(args: seq[string]; strict = false; except: discard - var project: Project - setupLocalProject(project) + var + project = setupLocalProject() # if the input wasn't parsed to a url, if not url.isValid: @@ -655,7 +649,7 @@ proc cloner*(args: seq[string]; strict = false; # and pluck the first result, presumed to be the best block found: - for repo in hubs.get.values: + for repo in get(hubs).values: url = repo.git name = repo.name break found @@ -667,12 +661,12 @@ proc cloner*(args: seq[string]; strict = false; # perform the clone var - cloned: Project - if not project.clone(url, name, cloned): + cloned = project.clone(url, name) + if cloned.isNil: crash &"problem cloning {url}" # reset our paths to, hopefully, grab the new project - project.cfg = loadAllCfgs(project.repo) + project.cfg = loadAllCfgs(project.root) # setup our dependency group var group = project.newDependencyGroup(flags = flags) @@ -680,7 +674,7 @@ proc cloner*(args: seq[string]; strict = false; notice &"unable to resolve all dependencies for {project}" # see if we can find this project in the dependencies - let needed = group.projectForPath(cloned.repo) + let needed = group.projectForPath(cloned.root) # if it's in there, let's get its requirement and roll to meet it block relocated: @@ -690,11 +684,11 @@ proc cloner*(args: seq[string]; strict = false; warn &"unable to retrieve requirement for {cloned.name}" else: # rollTowards will relocate us, too - if cloned.rollTowards(requirement.get): + if cloned.rollTowards(get requirement): notice &"rolled {cloned.name} to {cloned.version}" # so skip the tail of this block (and a 2nd relocate) break relocated - notice &"unable to meet {requirement.get} with {cloned}" + notice &"unable to meet {get(requirement)} with {cloned}" # rename the directory to match head release project.relocateDependency(cloned) @@ -738,7 +732,7 @@ when isMainModule: const release = projectVersion() if release.isSome: - clCfg.version = $release.get + clCfg.version = $get(release) else: clCfg.version = "(unknown version)" @@ -767,7 +761,7 @@ when isMainModule: doc="graph project dependencies") dispatchGen(nimbler, cmdName = $scNimble, dispatchName = "run" & $scNimble, doc="Nimble handles other subcommands (with a proper nimbleDir)") - dispatchGen(runner, cmdName = $scRun, dispatchName = "run" & $scRun, + dispatchGen(runion, cmdName = $scRun, dispatchName = "run" & $scRun, stopWords = @["--"], doc="execute the program & arguments in every dependency directory") const @@ -800,8 +794,8 @@ when isMainModule: const # these are our subcommands that we want to include in help - dispatchees = [scDoctor, scSearch, scClone, scPath, scFork, scLock, scUnlock, - scTag, scUpDown, scRoll, scGraph, scRun] + dispatchees = [scDoctor, scSearch, scClone, scPath, scFork, scLock, + scUnlock, scTag, scUpDown, scRoll, scGraph, scRun] # these are nimble subcommands that we don't need to warn about passthrough = ["install", "uninstall", "build", "test", "doc", "dump", diff --git a/src/nimph.nim.cfg b/nimph.nim.cfg similarity index 66% rename from src/nimph.nim.cfg rename to nimph.nim.cfg index b809a7b..8994252 100644 --- a/src/nimph.nim.cfg +++ b/nimph.nim.cfg @@ -13,11 +13,16 @@ --define:ssl # specify our preferred version of libgit2 +# and our preferred method of retrieval +--define:git2Git --define:git2SetVer:"v1.0.1" -# and our preferred method of retrieval +# here's a valid alternative; note the missing `v` prefix in the version! #--define:git2JBB ---define:git2Git +#--define:git2SetVer:"1.0.1" + +# use the system's libgit2 +#--define:git2Std # remove dependency on rando nim cache #--define:git2Static @@ -29,6 +34,11 @@ # for gratuitous search path debugging #--define:debugPath -# fix nimble? +# our requirements will be adjacent to this file --path="$config" + +# we also need a path to Nim for compiler imports --path="$nim" + +# put the binaries in their own directory +--outdir="$config/bin/" diff --git a/nimph.nimble b/nimph.nimble index c66e5cf..34c490f 100644 --- a/nimph.nimble +++ b/nimph.nimble @@ -1,16 +1,19 @@ -version = "1.0.6" +version = "2.0.0" author = "disruptek" description = "nim package handler from the future" license = "MIT" -requires "github >= 2.0.3 & < 3.0.0" -requires "cligen >= 0.9.46 & < 2.0.0" -requires "bump >= 1.8.18 & < 2.0.0" -requires "npeg >= 0.21.3 & < 1.0.0" -requires "https://github.com/disruptek/testes >= 0.7.6 & < 1.0.0" +requires "https://github.com/disruptek/balls >= 2.0.0 & < 3.0.0" +requires "https://github.com/disruptek/github >= 2.0.3 & < 3.0.0" +requires "https://github.com/c-blake/cligen >= 0.9.46 & < 2.0.0" +requires "https://github.com/disruptek/bump >= 1.8.18 & < 2.0.0" +requires "https://github.com/disruptek/ups > 0.0.5 & < 2.0.0" +requires "https://github.com/zevv/npeg >= 0.21.3 & < 1.0.0" requires "https://github.com/disruptek/jsonconvert < 2.0.0" requires "https://github.com/disruptek/badresults < 2.0.0" +requires "https://github.com/disruptek/frosty < 2.0.0" requires "https://github.com/disruptek/cutelog >= 1.1.0 & < 2.0.0" requires "https://github.com/disruptek/gittyup >= 2.5.0 & < 3.0.0" +requires "https://github.com/narimiran/sorta" bin = @["nimph"] srcDir = "src" diff --git a/src/nimph/asjson.nim b/nimph/asjson.nim similarity index 93% rename from src/nimph/asjson.nim rename to nimph/asjson.nim index c6c3eb0..a1cd75f 100644 --- a/src/nimph/asjson.nim +++ b/nimph/asjson.nim @@ -7,9 +7,9 @@ import std/json import bump import nimph/spec -import nimph/version -import nimph/package -import nimph/requirement +import nimph/versions +import nimph/packages +import nimph/requirements proc toJson*(operator: Operator): JsonNode = result = newJString($operator) @@ -92,11 +92,11 @@ proc toRequirement*(js: JsonNode): Requirement = operator = js["operator"].toOperator, release = js["release"].toRelease) -proc toJson*(dist: DistMethod): JsonNode = +proc toJson*(dist: Dist): JsonNode = result = newJString($dist) -proc toDistMethod*(js: JsonNode): DistMethod = - result = parseEnum[DistMethod](js.getStr) +proc toDist*(js: JsonNode): Dist = + result = parseEnum[Dist](js.getStr) proc toJson*(uri: Uri): JsonNode = let url = case uri.scheme: diff --git a/nimph/config.nim b/nimph/config.nim new file mode 100644 index 0000000..937d4a2 --- /dev/null +++ b/nimph/config.nim @@ -0,0 +1,79 @@ +import std/json +import std/strutils +import std/logging + +import ups/config +import ups/paths + +type + NimphConfig* = ref object + path: AbsoluteFile + js: JsonNode + +proc isEmpty*(config: NimphConfig): bool = + result = config.js.kind == JNull + +proc newNimphConfig*(path: AbsoluteFile): NimphConfig = + ## instantiate a new nimph config using the given path + result = NimphConfig(path: path) + if not fileExists result.path: + result.js = newJNull() + else: + try: + result.js = parseFile $path + except Exception as e: + error "unable to parse $#: " % [ $path ] + error e.msg + +proc addLockerRoom*(config: var NimphConfig; name: string; room: JsonNode) = + ## add the named lockfile (in json form) to the configuration file + addLockerRoom(config.js, name, room) + +proc getAllLockerRooms*(config: NimphConfig): JsonNode = + ## retrieve a JObject holding all lockfiles in the configuration file + result = getAllLockerRooms config.js + +proc getLockerRoom*(config: NimphConfig; name: string): JsonNode = + ## retrieve the named lockfile (or JNull) from the configuration + result = getLockerRoom(config.js, name) + +when false: + import compiler/nimconf + export nimconf + + proc overlayConfig(config: var ConfigRef; + directory: string): bool {.deprecated.} = + ## true if new config data was added to the env + withinDirectory(directory): + var + priorProjectPath = config.projectPath + let + nextProjectPath = getCurrentDir().toAbsoluteDir + filename = nextProjectPath.string / NimCfg + + block complete: + # do not overlay above the current config + if nextProjectPath == priorProjectPath: + break complete + + # if there's no config file, we're done + result = filename.fileExists + if not result: + break complete + + try: + # set the new project path for substitution purposes + config.projectPath = nextProjectPath + + var cache = newIdentCache() + result = readConfigFile(filename.AbsoluteFile, cache, config) + + if result: + # this config is now authoritative, so force the project path + priorProjectPath = nextProjectPath + else: + let emsg = &"unable to read config in {nextProjectPath}" # noqa + warn emsg + finally: + # remember to reset the config's project path + config.projectPath = priorProjectPath diff --git a/src/nimph/dependency.nim b/nimph/dependencies.nim similarity index 92% rename from src/nimph/dependency.nim rename to nimph/dependencies.nim index dcbf7de..4f49735 100644 --- a/src/nimph/dependency.nim +++ b/nimph/dependencies.nim @@ -13,15 +13,16 @@ import bump import gittyup import nimph/spec -import nimph/package -import nimph/project -import nimph/version +import nimph/paths +import nimph/packages +import nimph/projects +import nimph/versions import nimph/versiontags -import nimph/requirement +import nimph/requirements import nimph/config -import nimph/group -export group +import nimph/groups +export groups type Dependency* = ref object @@ -126,9 +127,8 @@ proc determineDeps*(project: Project): Option[Requires] = break # this is (usually) gratuitous, but it's also the right place # to perform this assignment, so... go ahead and do it - for a, b in result.get.mpairs: - a.notes = project.name - b.notes = project.name + for r in mitems(get(result)): + r.notes = project.name proc determineDeps*(project: var Project): Option[Requires] = ## try to parse requirements of a project using the `nimble dump` output @@ -150,7 +150,7 @@ proc peelRelease*(project: Project; release: Release): Release = break # else, open the repo - repository := openRepository(project.gitDir): + repository := openRepository($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break @@ -251,7 +251,7 @@ iterator symbolicMatch*(project: Project; req: Requirement): Release = for branch in project.matchingBranches(req.release.reference): debug &"found {req.release.reference} in {project}" yield newRelease($branch.oid, operator = Tag) - repository := openRepository(project.gitDir): + repository := openRepository($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break # else, it's a random oid, maybe? look it up! @@ -319,7 +319,7 @@ proc isSatisfiedBy*(req: Requirement; project: Project; release: Release): bool break satisfied block: - repository := openRepository(project.gitDir): + repository := openRepository($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break thing := repository.lookupThing(name = release.reference): @@ -425,7 +425,8 @@ proc add(dependency: var Dependency; packages: PackageGroup) = for package in packages.values: dependency.add package -proc add(dependency: var Dependency; directory: string; project: Project) = +proc add(dependency: var Dependency; directory: AbsoluteDir; + project: Project) = ## add a local project in the given directory to an existing dependency if dependency.projects.hasKey(directory): raise newException(Defect, "attempt to duplicate project dependency") @@ -440,7 +441,7 @@ proc newDependency*(project: Project): Dependency = requirement = newRequirement(project.name, Equal, project.release) requirement.notes = project.name result = newDependency(requirement) - result.add project.repo, project + result.add project.root, project proc mergeContents(existing: var Dependency; dependency: Dependency): bool = ## combine two dependencies and yield true if a new project is added @@ -460,10 +461,10 @@ proc addName(group: var DependencyGroup; req: Requirement; dep: Dependency) = for directory, project in dep.projects.pairs: let name = project.importName if name notin group.imports: - group.imports[name] = directory - elif group.imports[name] != directory: + group.imports[name] = $directory + elif group.imports[name] != $directory: warn &"name collision for import `{name}`:" - for path in [directory, group.imports[name]]: + for path in [$directory, group.imports[name]]: warn &"\t{path}" when defined(debugImportNames): when not defined(release) and not defined(danger): @@ -524,19 +525,22 @@ proc addedRequirements*(dependencies: var DependencyGroup; # point to the merged dependency dependency = existing -proc pathForName*(dependencies: DependencyGroup; name: string): Option[string] = +proc pathForName*(dependencies: DependencyGroup; + name: string): Option[AbsoluteDir] = ## try to retrieve the directory for a given import if dependencies.imports.hasKey(name): - result = dependencies.imports[name].some + result = some(dependencies.imports[name].toAbsoluteDir) -proc projectForPath*(deps: DependencyGroup; path: string): Option[Project] = +proc projectForPath*(deps: DependencyGroup; + path: AbsoluteDir): Option[Project] = ## retrieve a project from the dependencies using its path for dependency in deps.values: if dependency.projects.hasKey(path): - result = dependency.projects[path].some + result = some(dependency.projects[path]) break -proc reqForProject*(group: DependencyGroup; project: Project): Option[Requirement] = +proc reqForProject*(group: DependencyGroup; + project: Project): Option[Requirement] = ## try to retrieve a requirement given a project for requirement, dependency in group.pairs: if project in dependency.projects: @@ -593,19 +597,18 @@ proc resolveUsing*(projects: ProjectGroup; packages: PackageGroup; result.add findurl.get break success -proc isUsing*(dependencies: DependencyGroup; target: Target; +proc isUsing*(dependencies: DependencyGroup; target: AbsoluteDir; outside: Dependency = nil): bool = - ## true if the target points to a repo we're importing + ## true if the target directory holds a project we're importing block found: - for requirement, dependency in dependencies.pairs: - if dependency == outside: - continue - for directory, project in dependency.projects.pairs: - if directory == target.repo: - result = true - break found + for requirement, dependency in pairs(dependencies): + if dependency != outside: + for directory, project in pairs(dependency.projects): + result = directory == target + if result: + break found when defined(debug): - debug &"is using {target.repo}: {result}" + debug &"is using {target}: {result}" proc resolve*(project: Project; deps: var DependencyGroup; req: Requirement): bool @@ -647,7 +650,7 @@ proc resolve*(project: Project; deps: var DependencyGroup; req: Requirement): bool = ## resolve a single project's requirement, storing the result var resolved = resolveUsing(deps.projects, deps.packages, req) - case resolved.packages.len: + case len(resolved.packages): of 0: warn &"unable to resolve requirement `{req}`" result = false @@ -667,27 +670,30 @@ proc resolve*(project: Project; deps: var DependencyGroup; # else, we'll resolve dependencies introduced in any new dependencies. # note: we're using project.cfg and project.repo as a kind of scope - for recurse in resolved.projects.asFoundVia(project.cfg, project.repo): + for recurse in resolved.projects.asFoundVia(project.cfg): # if one of the existing dependencies is using the same project, then # we won't bother to recurse into it and process its requirements - if deps.isUsing(recurse.nimble, outside = resolved): - continue - result = result and recurse.resolve(deps) - # if we failed, there's no point in continuing - if not result: - break complete + if not deps.isUsing(recurse.root, outside = resolved): + result = result and recurse.resolve(deps) + # if we failed, there's no point in continuing + if not result: + break complete -proc getOfficialPackages(project: Project): PackagesResult = - result = getOfficialPackages(project.nimbleDir) +when AndNimble: + proc getOfficialPackages(project: Project): PackagesResult = + result = getOfficialPackages(project.nimbleDir) proc newDependencyGroup*(project: Project; flags = defaultFlags): DependencyGroup = ## a convenience to load packages and projects for resolution result = newDependencyGroup(flags) - # try to load the official packages list; either way, a group will exist - let official = project.getOfficialPackages - result.packages = official.packages + when AndNimble: + # try to load the official packages list; either way, a group will exist + let official = project.getOfficialPackages + result.packages = official.packages + else: + result.packages = newPackageGroup(flags) # collect all the packages from the environment result.projects = project.childProjects @@ -697,7 +703,7 @@ proc reset*(dependencies: var DependencyGroup; project: var Project) = # empty the group of all requirements and dependencies dependencies.clear # reset the project's configuration to find new paths, etc. - project.cfg = loadAllCfgs(project.repo) + project.cfg = loadAllCfgs(project.root) # rescan for package dependencies applicable to this project dependencies.projects = project.childProjects diff --git a/src/nimph/doctor.nim b/nimph/doctor.nim similarity index 76% rename from src/nimph/doctor.nim rename to nimph/doctor.nim index 26c6c25..c323735 100644 --- a/src/nimph/doctor.nim +++ b/nimph/doctor.nim @@ -9,15 +9,15 @@ import bump import gittyup import nimph/spec -import nimph/project +import nimph/paths +import nimph/projects import nimph/nimble import nimph/config import nimph/thehub -import nimph/package -import nimph/dependency -import nimph/group - -import nimph/requirement +import nimph/packages +import nimph/dependencies +import nimph/groups +import nimph/requirements type StateKind* = enum @@ -50,7 +50,7 @@ proc fixTags*(project: var Project; dry_run = true; force = false): bool = break # open the repo so we can keep it in memory for tagging purposes - repository := openRepository(project.gitDir): + repository := openRepository($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break @@ -137,7 +137,7 @@ proc fixDependencies*(project: var Project; group: var DependencyGroup; elif project.addSearchPath(path): info &"added path `{path}` to `{project.nimcfg}`" # yay, we get to reload again - project.cfg = loadAllCfgs(project.repo) + project.cfg = loadAllCfgs(project.root) else: warn &"couldn't add path `{path}` to `{project.nimcfg}`" result = false @@ -164,8 +164,11 @@ proc fixDependencies*(project: var Project; group: var DependencyGroup; else: block cloneokay: for package in dependency.packages.mvalues: - var cloned: Project - if project.clone(package.url, package.name, cloned): + var cloned = project.clone(package.url, package.name) + if cloned.isNil: + error &"error cloning {package}" + # a subsequent iteration could clone successfully + else: if cloned.rollTowards(requirement): notice &"rolled to {cloned.release} to meet {requirement}" else: @@ -173,9 +176,6 @@ proc fixDependencies*(project: var Project; group: var DependencyGroup; project.relocateDependency(cloned) state.kind = DrRetry break cloneokay - else: - error &"error cloning {package}" - # a subsequent iteration could clone successfully # no package was successfully cloned notice &"unable to satisfy {requirement.describe}" result = false @@ -216,7 +216,7 @@ proc doctor*(project: var Project; dry = true; strict = true): bool = error "and i wasn't able to make a new one" else: let - parsed = parseConfigFile($nimcfg) + parsed = parseConfigFile(nimcfg) if parsed.isNone: error &"i had some issues trying to parse {nimcfg}" result = false @@ -232,24 +232,22 @@ proc doctor*(project: var Project; dry = true; strict = true): bool = result = false # try to parse all nim configuration files - block globalconfig: - when defined(debugPath): - for path in project.cfg.likelySearch(libsToo = true): - debug &"\tsearch: {path}" - for path in project.cfg.likelyLazy: - debug &"\t lazy: {path}" - else: - ## this space intentionally left blank + when defined(debugPath): + for path in project.cfg.likelySearch(libsToo = true): + debug &"\tsearch: {path}" + for path in project.cfg.likelyLazy: + debug &"\t lazy: {path}" - block whoami: - debug "checking project version" - # check our project version - let - version = project.knowVersion - # contextual errors are output by knowVersion - result = version.isValid - if result: - debug &"{project.name} version {version}" + when AndNimble: + block whoami: + debug "checking project version" + # check our project version + let + version = project.knowVersion + # contextual errors are output by knowVersion + result = version.isValid + if result: + debug &"{project.name} version {version}" block dependencies: debug "checking dependencies" @@ -263,28 +261,29 @@ proc doctor*(project: var Project; dry = true; strict = true): bool = # $NIMBLE_DIR could screw with our head if envDir != "": - if absolutePath(envDir) != depsDir: + if toAbsoluteDir(envDir) != depsDir: notice "i'm not sure what to do with an alternate $NIMBLE_DIR set" result = false else: info "your $NIMBLE_DIR is set, but it's set correctly" - block checknimble: - debug "checking nimble" - # make sure nimble is a thing - if findExe("nimble") == "": - error "i can't find nimble in the path" - result = false + when AndNimble: + block checknimble: + debug "checking nimble" + # make sure nimble is a thing + if findExe("nimble") == "": + error "i can't find nimble in the path" + result = false - debug "checking nimble dump of our project" - # make sure we can dump our project - let - damp = fetchNimbleDump(project.nimble.repo) - if not damp.ok: - error damp.why - result = false - else: - project.dump = damp.table + debug "checking nimble dump of our project" + # make sure we can dump our project + let + damp = fetchNimbleDump(project.nimble.repo) + if not damp.ok: + error damp.why + result = false + else: + project.dump = damp.table # see if we can find a github token block github: @@ -307,35 +306,36 @@ proc doctor*(project: var Project; dry = true; strict = true): bool = else: debug "git init/shut seems to be working" - # see if we can get the packages list; try to refresh it if necessary - block packages: - while true: - let - packs = getOfficialPackages(project.nimbleDir) - once: - block skiprefresh: - if not packs.ok: - if packs.why != "": - error packs.why - notice &"couldn't get nimble's package list from {project.nimbleDir}" - elif packs.ageInDays > stalePackages: - notice &"the nimble package list in {project.nimbleDir} is stale" - elif packs.ageInDays > 1: - info "the nimble package list is " & - &"{packs.ageInDays} days old" - break skiprefresh - else: - break skiprefresh - if not dry: - let refresh = project.runSomething("nimble", @["refresh", "--accept"]) - if refresh.ok: - info "nimble refreshed the package list" - continue - result = false - if packs.ok: - let packages {.used.} = packs.packages - debug &"loaded {packages.len} packages from nimble" - break + when AndNimble: + # see if we can get the packages list; try to refresh it if necessary + block packages: + while true: + let + packs = getOfficialPackages(project.nimbleDir) + once: + block skiprefresh: + if not packs.ok: + if packs.why != "": + error packs.why + notice &"couldn't get nimble's package list from {project.nimbleDir}" + elif packs.ageInDays > stalePackages: + notice &"the nimble package list in {project.nimbleDir} is stale" + elif packs.ageInDays > 1: + info "the nimble package list is " & + &"{packs.ageInDays} days old" + break skiprefresh + else: + break skiprefresh + if not dry: + let refresh = project.runSomething("nimble", @["refresh", "--accept"]) + if refresh.ok: + info "nimble refreshed the package list" + continue + result = false + if packs.ok: + let packages {.used.} = packs.packages + debug &"loaded {packages.len} packages from nimble" + break # check dependencies and maybe install some block dependencies: @@ -343,6 +343,10 @@ proc doctor*(project: var Project; dry = true; strict = true): bool = group = project.newDependencyGroup(flags) state = DrState(kind: DrRetry) + # we'll cache the old result so we can reset it if we are able to + # fix all the dependencies + prior = result + while state.kind == DrRetry: # we need to reload the config each repeat through this loop so that we # can correctly identify new search paths after adding new packages @@ -352,6 +356,9 @@ proc doctor*(project: var Project; dry = true; strict = true): bool = state.kind = DrError elif not project.fixDependencies(group, state): result = false + else: + # reset the state in the event that dependencies are fixed + result = prior # maybe we're done here if state.kind notin {DrRetry}: break @@ -365,19 +372,18 @@ proc doctor*(project: var Project; dry = true; strict = true): bool = # warning if local deps exist or multiple nimblePaths are found block extradeps: if project.hasLocalDeps or project.numberOfNimblePaths > 1: - let imports = project.cfg.allImportTargets(project.repo) - for target, linked in imports.pairs: - if group.isUsing(target): - continue - # ignore standard library targets - if project.cfg.isStdLib(target.repo): - continue - let name = linked.importName - warn &"no `{name}` requirement for {target.repo}" - - # identify packages that aren't named according to their versions; rename - # local dependencies and merely warn about others - {.warning: "mislabeled project directories unimplemented".} + let available = availableProjects(project) + for directory, other in pairs(available): + # ignore anything in our dependency group + if not group.isUsing(directory): + # ignore standard library targets + if not project.cfg.isStdLib(directory): + warn &"no `{other.importName}` requirement for {other.root}" + + when AndNimble: + # identify packages that aren't named according to their versions; + # rename local dependencies and merely warn about others + {.warning: "mislabeled project directories unimplemented".} # remove missing paths from nim.cfg if possible block missingpaths: @@ -428,16 +434,17 @@ proc doctor*(project: var Project; dry = true; strict = true): bool = block shadoweddeps: {.warning: "shadowed deps needs implementing".} - # if a package exists and is local to the project and picked up by the - # config (search paths or lazy paths) and it isn't listed in the - # requirements, then we should warn about it - block unspecifiedrequirement: - {.warning: "unspecified requirements needs implementing".} + when AndNimble: + # if a package exists and is local to the project and picked up by the + # config (search paths or lazy paths) and it isn't listed in the + # requirements, then we should warn about it + block unspecifiedrequirement: + {.warning: "unspecified requirements needs implementing".} - # if a required packaged has a srcDir defined in the .nimble, then it needs to - # be specified in the search paths - block unspecifiedsearchpath: - {.warning: "unspecified search path needs implementing".} + # if a required package has a srcDir defined in the .nimble, then it + # needs to be specified in the search paths + block unspecifiedsearchpath: + {.warning: "unspecified search path needs implementing".} # warn of tags missing for a particular version/commit pair block identifymissingtags: @@ -453,4 +460,4 @@ proc doctor*(project: var Project; dry = true; strict = true): bool = fatal "❔it looks like you have multiple --nimblePaths defined:" for index, path in found.paths.pairs: fatal &"❔\t{index + 1}\t{path}" - fatal "❔nim and nimph support this, but some humans find it confusing 😏" + fatal "❔nim and nimph support this, but humans can find it confusing 😏" diff --git a/nimph/groups.nim b/nimph/groups.nim new file mode 100644 index 0000000..65c97d3 --- /dev/null +++ b/nimph/groups.nim @@ -0,0 +1,225 @@ +import std/hashes +import std/os +from std/sequtils import toSeq +import std/uri except Url + +import gittyup + +import ups/paths + +import nimph/spec +import nimph/requirements +import nimph/versions + +##[ + +these are just collection concepts that assert a little more convenience. + +a Group is a collection that holds items that may be indexed by some +stable index. deletion should preserve order whenever possible. singleton +iteration yields the original item. pairwise iteration also yields unique +indices that can be used for deletion. + +a FlaggedGroup additionally has a flags field/proc that yields set[Flag]; +this is used to alter group operations to, for example, silently omit +errors and warnings (Quiet) or prevent destructive modification (DryRun). + +]## + +type + Suitable = concept s, t ## the most basic of identity assumptions + hash(s) is Hash + `==`(s, t) is bool + + Collectable[T] = concept c, var w ## a collection of suitable items + contains(c, T) is bool + len(c) is Ordinal + for item in items(c): # ...that you can iterate + item is T + item is Suitable + for index, item in pairs(c): # pairs iteration yields the index + item is T + item is Suitable + del(w, index) # the index can be used for deletion + T is Suitable + + Groupable[T] = concept g, var w ## a collection we can grow or shrink + g is Collectable + g is Collectable[T] + add(w, T) + del(w, T) + + Group[T] = concept g, var w ## add the concept of a unique index + g is Groupable[T] + incl(w, T) # do nothing if T's index exists + excl(w, T) # do nothing if T's index does not exist + for index, item in pairs(g): # pairs iteration yields the index + item is T + item is Suitable + add(w, index, T) # it will raise if the index exists + `[]`(w, index) is T # get via index + `[]=`(w, index, T) # set via index + + IdentityGroup*[T] = concept g, var w ## + ## an IdentityGroup lets you test for membership via Identity, + ## PackageName, or Uri + g is Group[T] + w is Group[T] + contains(g, Identity) is bool + contains(g, PackageName) is bool + contains(g, Uri) is bool + `[]`(g, PackageName) is T # indexing by identity types + `[]`(g, Uri) is T # indexing by identity types + `[]`(g, Identity) is T # indexing by identity types + + ImportGroup*[T] = concept g, var w ## + ## an ImportGroup lets you test for membership via ImportName + g is Group[T] + w is Group[T] + importName(T) is ImportName + contains(g, ImportName) is bool + excl(w, ImportName) # delete any T that yields ImportName + `[]`(g, ImportName) is T # index by ImportName + + GitGroup*[T] = concept g, var w ## + ## a GitGroup is designed to hold Git objects like tags, references, + ## commits, and so on + g is Group[T] + w is Group[T] + oid(T) is GitOid + contains(g, GitOid) is bool + excl(w, GitOid) # delete any T that yields GitOid + `[]`(g, GitOid) is T # index by GitOid + free(T) # ensure we can free the group + + ReleaseGroup*[T] = concept g, var w ## + ## a ReleaseGroup lets you test for membership via Release, + ## (likely Version, Tag, and such as well) + g is Group[T] + w is Group[T] + contains(g, Release) is bool + for item in g[Release]: # indexing iteration by Release + item is T + +proc incl*[T](group: Groupable[T]; value: T) = + if value notin group: + group.add value + +proc excl*[T](group: Groupable[T]; value: T) = + if value in group: + group.del value + +proc hash*(group: Collectable): Hash = + var h: Hash = 0 + for item in items(group): + h = h !& hash(item) + result = !$h + +iterator backwards*[T](group: Collectable): T = + ## yield values in reverse order + let items = toSeq items(group) + for index in countDown(items.high, items.low): + yield items[index] + +proc contains*(group: Group; name: ImportName): bool = + for item in items(group): + result = item.importName == name + if result: + break + +proc excl*(group: Group; name: ImportName) = + while name in group: + for item in items(group): + if importName(item) == name: + group.del item + break + +proc `[]`*[T](group: Group[T]; name: ImportName): T = + block found: + for item in items(group): + if item.importName == name: + result = item + break found + raise newException(KeyError, "not found") + +proc free*(group: Group) = + ## free GitGroup members + while len(group) > 0: + for item in items(group): + group.del item + break + +proc contains*(group: Group; identity: Identity): bool = + for item in items(group): + result = item == identity + if result: + break + +proc contains*(group: Group; name: PackageName): bool = + result = newIdentity(name) in group + +proc contains*(group: Group; url: Uri): bool = + result = newIdentity(url) in group + +proc add*[T](group: Group[T]; value: T) = + if value in group: + raise newException(KeyError, "duplicates not supported") + group.incl value + +proc `[]`*[T](group: Group[T]; identity: Identity): T = + block found: + for item in items(group): + if item == identity: + result = item + break found + raise newException(KeyError, "not found") + +proc `[]`*[T](group: Group[T]; url: Uri): T = + result = group[newIdentity(url)] + +proc `[]`*[T](group: Group[T]; name: PackageName): T = + result = group[newIdentity(name)] + +proc del*[T](group: var Collectable[T]; value: T) = + for index, item in pairs(group): + if item == value: + group.del index + break + +when isMainModule: + import balls + + suite "concepts test": + ## we start with a simple "collection". + var g: seq[string] + ## add some values. + g.add "Goats" + g.add "pigs" + ## make an immutable copy. + let h = g + ## a string is a suitable type for tests. + assert string is Suitable + ## g is a Collectable of strings. + assert g is Collectable[string] + ## sure, fine, as expected. + assert g is Collectable + ## del() was written against Collectable + g.del "pigs" + ## ok, great. and this works, for now! + assert g is Groupable[string] + ## this does not -- but why not? + assert g is Groupable + ## h is immutable, so isn't Groupable[string], right? + assert h isnot Groupable[string] + ## so does that mean we can add to h? + h.add "horses" + ## but wait, i thought h was Collectable + assert h is Collectable + ## so we can iterate and delete, right? + for index, item in pairs(h): + h.del index + break + ## oh, but we can use our del(var w, T)? + h.del "pigs" + ## right, so, uh, how is h a Groupable[string]? + assert h is Groupable[string] diff --git a/src/nimph/locker.nim b/nimph/lockers.nim similarity index 74% rename from src/nimph/locker.nim rename to nimph/lockers.nim index 94819c3..6bc8496 100644 --- a/src/nimph/locker.nim +++ b/nimph/lockers.nim @@ -6,24 +6,24 @@ import std/tables import std/uri import nimph/spec -import nimph/version -import nimph/group +import nimph/versions +import nimph/groups import nimph/config -import nimph/project -import nimph/dependency -import nimph/package +import nimph/projects +import nimph/dependencies +import nimph/packages import nimph/asjson import nimph/doctor -import nimph/requirement +import nimph/requirements type Locker* = ref object name*: string url*: Uri requirement*: Requirement - dist*: DistMethod + dist*: Dist release*: Release - LockerRoom* = ref object of Group[string, Locker] + Lockers* = ref object of Group[string, Locker] name*: string root*: Locker @@ -39,22 +39,22 @@ proc hash*(locker: Locker): Hash = h = h !& locker.release.hash result = !$h -proc hash*(room: LockerRoom): Hash = - ## the hash of a lockerroom is the hash of its root and all lockers +proc hash*(group: Lockers): Hash = + ## the hash of a lockers is the hash of its root and all lockers var h: Hash = 0 - for locker in room.values: + for locker in group.values: h = h !& locker.hash - h = h !& room.root.hash + h = h !& group.root.hash result = !$h proc `==`(a, b: Locker): bool = result = a.hash == b.hash -proc `==`(a, b: LockerRoom): bool = +proc `==`(a, b: Lockers): bool = result = a.hash == b.hash -proc newLockerRoom*(name = ""; flags = defaultFlags): LockerRoom = - result = LockerRoom(name: name, flags: flags) +proc newLockers*(name = ""; flags = defaultFlags): Lockers = + result = Lockers(name: name, flags: flags) result.init(flags, mode = modeStyleInsensitive) proc newLocker(requirement: Requirement): Locker = @@ -69,27 +69,27 @@ proc newLocker(req: Requirement; name: string; project: Project): Locker = result.dist = project.dist result.release = project.release -proc newLockerRoom*(project: Project; flags = defaultFlags): LockerRoom = - ## a new lockerroom using the project release as the root +proc newLockers*(project: Project; flags = defaultFlags): Lockers = + ## a new lockers using the project release as the root let requirement = newRequirement(project.name, Equal, project.release) - result = newLockerRoom(flags = flags) + result = newLockers(flags = flags) result.root = newLocker(requirement, rootName, project) -proc add*(room: var LockerRoom; req: Requirement; name: string; +proc add*(room: var Lockers; req: Requirement; name: string; project: Project) = ## create a new locker for the requirement from the project and - ## safely add it to the lockerroom + ## safely add it to the lockers var locker = newLocker(req, name, project) block found: for existing in room.values: if existing == locker: error &"unable to add equivalent lock for `{name}`" break found - room.add name, locker + room.add name.string, locker -proc fillRoom(room: var LockerRoom; dependencies: DependencyGroup): bool = - ## fill a lockerroom with lockers constructed from the dependency tree; +proc fillLockers(room: var Lockers; dependencies: DependencyGroup): bool = + ## fill a lockers with lockers constructed from the dependency tree; ## returns true if there were no missing/unready/shadowed dependencies result = true for requirement, dependency in dependencies.pairs: @@ -118,7 +118,7 @@ proc fillRoom(room: var LockerRoom; dependencies: DependencyGroup): bool = result = false proc fillDeps(dependencies: var DependencyGroup; - room: LockerRoom; project: Project): bool = + room: Lockers; project: Project): bool = ## fill a dependency tree with lockers and run dependency resolution ## using the project; returns true if there were no resolution failures result = true @@ -146,18 +146,18 @@ proc toLocker*(js: JsonNode): Locker = result.name = js["name"].getStr result.url = js["url"].toUri result.release = js["release"].toRelease - result.dist = js["dist"].toDistMethod + result.dist = js["dist"].toDist -proc toJson*(room: LockerRoom): JsonNode = - ## convert a LockerRoom to a JObject +proc toJson*(room: Lockers): JsonNode = + ## convert a Lockers to a JObject result = newJObject() for name, locker in room.pairs: result[locker.name] = locker.toJson result[room.root.name] = room.root.toJson -proc toLockerRoom*(js: JsonNode; name = ""): LockerRoom = - ## convert a JObject to a LockerRoom - result = newLockerRoom(name) +proc toLockers*(js: JsonNode; name = ""): Lockers = + ## convert a JObject to a Lockers + result = newLockers(name) for name, locker in js.pairs: if name == rootName: result.root = locker.toLocker @@ -166,27 +166,27 @@ proc toLockerRoom*(js: JsonNode; name = ""): LockerRoom = else: result.add name, locker.toLocker -proc getLockerRoom*(project: Project; name: string; room: var LockerRoom): bool = - ## true if we pulled the named lockerroom out of the project's configuration +proc getLockers*(project: Project; name: string; room: var Lockers): bool = + ## true if we pulled the named lockers out of the project's configuration let - js = project.config.getLockerRoom(name) + js = project.config.getLockers(name) if js != nil and js.kind == JObject: - room = js.toLockerRoom(name) + room = js.toLockers(name) result = true -iterator allLockerRooms*(project: Project): LockerRoom = - ## emit each lockerroom in the project's configuration - for name, js in project.config.getAllLockerRooms.pairs: - yield js.toLockerRoom(name) +iterator allLockers*(project: Project): Lockers = + ## emit each lockers in the project's configuration + for name, js in project.config.getAllLockers.pairs: + yield js.toLockers(name) proc unlock*(project: var Project; name: string; flags = defaultFlags): bool = ## unlock a project using the named lockfile var dependencies = project.newDependencyGroup(flags = {Flag.Quiet} + flags) - room = newLockerRoom(name, flags) + room = newLockers(name, flags) block unlocked: - if not project.getLockerRoom(name, room): + if not project.getLockers(name, room): notice &"unable to find a lock named `{name}`" break unlocked @@ -222,10 +222,10 @@ proc lock*(project: var Project; name: string; flags = defaultFlags): bool = ## store a project's dependencies into the named lockfile var dependencies = project.newDependencyGroup(flags = {Flag.Quiet} + flags) - room = newLockerRoom(project, flags) + room = newLockers(project, flags) block locked: - if project.getLockerRoom(name, room): + if project.getLockers(name, room): notice &"lock `{name}` already exists; choose a new name" break locked @@ -235,18 +235,18 @@ proc lock*(project: var Project; name: string; flags = defaultFlags): bool = notice &"unable to resolve all dependencies for {project}" break locked - # if the lockerroom isn't confident, we can't lock the project - result = room.fillRoom(dependencies) + # if the lockers isn't confident, we can't lock the project + result = room.fillLockers(dependencies) if not result: notice &"not confident enough to lock {project}" break locked - # compare this lockerroom to pre-existing lockerrooms and don't dupe it - for exists in project.allLockerRooms: + # compare this lockers to pre-existing lockers and don't dupe it + for exists in project.allLockers: if exists == room: notice &"already locked these dependencies as `{exists.name}`" result = false break locked - # write the lockerroom to the project's configuration - project.config.addLockerRoom name, room.toJson + # write the lockers to the project's configuration + project.config.addLockers name, room.toJson diff --git a/nimph/nimble.nim b/nimph/nimble.nim new file mode 100644 index 0000000..ea97d1a --- /dev/null +++ b/nimph/nimble.nim @@ -0,0 +1,199 @@ +import std/uri +import std/json +import std/options +import std/strtabs +import std/strutils +import std/os +import std/osproc +import std/strformat + +import npeg +import bump + +import ups/runner +import ups/paths + +import nimph/spec + +type + DumpResult* = object + table*: StringTableRef + why*: string + ok*: bool + + NimbleMeta* = ref object + js: JsonNode + link: seq[string] + + LinkedSearchResult* = ref object + via: LinkedSearchResult + source: string + search: SearchResult + +proc parseNimbleDump*(input: string): Option[StringTableRef] = + ## parse output from `nimble dump` + var + table = newStringTable(modeStyleInsensitive) + let + peggy = peg "document": + nl <- ?'\r' * '\n' + white <- {'\t', ' '} + key <- +(1 - ':') + value <- '"' * *(1 - '"') * '"' + errline <- white * >*(1 - nl) * +nl: + warn $1 + line <- >key * ':' * +white * >value * +nl: + table[$1] = unescape($2) + anyline <- line | errline + document <- +anyline * !1 + parsed = peggy.match(input) + if parsed.ok: + result = table.some + +proc fetchNimbleDump*(path: AbsoluteDir; + nimbleDir = AbsoluteDir""): DumpResult = + ## parse nimble dump output into a string table + result = DumpResult(ok: false) + withinDirectory(path): + let + nimble = runSomething("nimble", @["dump", $path], {poDaemon}, + nimbleDir = nimbleDir) + + result.ok = nimble.ok + if result.ok: + let + parsed = parseNimbleDump(nimble.output) + result.ok = parsed.isSome + if result.ok: + result.table = get(parsed) + else: + result.why = &"unable to parse `nimble dump` output" + else: + result.why = "nimble execution failed" + if nimble.output.len > 0: + error nimble.output + +proc hasUrl*(meta: NimbleMeta): bool = + ## true if the metadata includes a url + result = "url" in meta.js + result = result and meta.js["url"].kind == JString + result = result and meta.js["url"].getStr != "" + +proc url*(meta: NimbleMeta): Uri = + ## return the url associated with the package + if not meta.hasUrl: + raise newException(ValueError, "url not available") + result = parseUri(meta.js["url"].getStr) + if result.anchor == "": + if "vcsRevision" in meta.js: + result.anchor = meta.js["vcsRevision"].getStr + removePrefix(result.anchor, {'#'}) + +proc writeNimbleMeta*(path: AbsoluteDir; url: Uri; revision: string): bool = + ## try to write a new nimblemeta.json + block complete: + if not dirExists(path): + warn &"{path} is not a directory; cannot write {nimbleMeta}" + break complete + var + revision = revision + removePrefix(revision, {'#'}) + var + metafn = path / RelativeFile(nimbleMeta) + js = %* { + "url": $url, + "vcsRevision": revision, + "files": @[], + "binaries": @[], + "isLink": false, + } + writer = open($metafn, fmWrite) + try: + writer.write($js) + result = true + finally: + writer.close + +proc isLink*(meta: NimbleMeta): bool = + ## true if the metadata says it's a link + if meta.js.kind == JObject: + result = meta.js.getOrDefault("isLink").getBool + +proc isValid*(meta: NimbleMeta): bool = + ## true if the metadata appears to hold some data + result = meta.js != nil and meta.js.len > 0 + +proc fetchNimbleMeta*(path: AbsoluteDir): NimbleMeta = + ## parse the nimblemeta.json file if it exists + result = NimbleMeta(js: newJObject()) + let + metafn = path / RelativeFile(nimbleMeta) + try: + if fileExists(metafn): + let + content = readFile($metafn) + result.js = parseJson(content) + except Exception as e: + discard e # noqa + warn &"error while trying to parse {nimbleMeta}: {e.msg}" + +proc parseNimbleLink*(path: string): tuple[nimble: string; source: string] = + ## parse a dotNimbleLink file into its constituent components + let + lines = readFile(path).splitLines + if lines.len != 2: + raise newException(ValueError, "malformed " & path) + result = (nimble: lines[0], source: lines[1]) + +proc linkedFindTarget*(dir: AbsoluteDir; target = ""; nimToo = false; + ascend = true): LinkedSearchResult = + ## recurse through .nimble-link files to find the .nimble + var + extensions = @[dotNimble, dotNimbleLink] + if nimToo: + extensions = @["".addFileExt("nim")] & extensions + + # perform the search with our cleverly-constructed extensions + result = LinkedSearchResult() + result.search = findTarget($dir, extensions = extensions, + target = target, ascend = ascend) + + # if we found nothing, or we found a dotNimble, then we're done + let found = result.search.found + if found.isNone or found.get.ext != dotNimbleLink: + return + + # now we need to parse this dotNimbleLink and recurse on the target + try: + let parsed = parseNimbleLink($get(found)) + if fileExists(parsed.nimble): + result.source = parsed.source + let parent = parentDir(parsed.nimble).toAbsoluteDir + # specify the path to the .nimble and the .nimble filename itself + var recursed = linkedFindTarget(parent, nimToo = nimToo, + target = parsed.nimble.extractFilename, + ascend = ascend) + # if the recursion was successful, add ourselves to the chain and return + if recursed.search.found.isSome: + recursed.via = result + return recursed + + # a failure mode yielding a useful explanation + result.search.message = &"{found.get} didn't lead to a {dotNimble}" + except ValueError as e: + # a failure mode yielding a less-useful explanation + result.search.message = e.msg + + # critically, set the search to none because ultimately, we found nothing + result.search.found = none(Target) + +proc importName*(target: Target): ImportName = + result = target.package.importName + +proc importName*(linked: LinkedSearchResult): ImportName = + ## a uniform name usable in code for imports + if linked.via != nil: + result = linked.via.importName + else: + # if found isn't populated, we SHOULD crash here + result = linked.search.found.get.importName diff --git a/src/nimph/package.nim b/nimph/packages.nim similarity index 60% rename from src/nimph/package.nim rename to nimph/packages.nim index 7eb8291..0181918 100644 --- a/src/nimph/package.nim +++ b/nimph/packages.nim @@ -6,57 +6,106 @@ import std/hashes import std/strformat import std/sequtils import std/strutils -import std/uri +import std/uri except Url import std/json import std/options import npeg +import sorta -import nimph/spec -import nimph/requirement +import ups/paths -import nimph/group -export group +import nimph/spec +import nimph/requirements +import nimph/groups type - DistMethod* = enum + Dist* = enum Local = "local" Git = "git" Nest = "nest" Merc = "hg" + Nimble = "nimble" - Package* = ref object - name*: string + Package* = object + name*: PackageName url*: Uri - dist*: DistMethod + dist*: Dist tags*: seq[string] description*: string license*: string web*: Uri naive*: bool local*: bool - path*: string + path*: AbsoluteDir author*: string - PackageGroup* = Group[string, Package] + Packages* = SortedTable[PackageName, Package] PackagesResult* = object ok*: bool why*: string - packages*: PackageGroup + packages*: Packages info: FileInfo -proc importName*(package: Package): string = - result = package.name.importName.toLowerAscii - error &"import name {result} from {package.name}" +proc importName*(package: Package): ImportName = + ## calculate how a package will be imported by the compiler + importName(package.name) + +iterator items*(packages: Packages): Package = + for item in values(packages): + yield item + +proc contains*(packages: Packages; package: Package): bool = + result = package.name in packages + assert packages[package.name] == package + +proc contains*(packages: Packages; url: Uri): bool = + for package in values(packages): + assert bare(package.url) == package.url + result = package.url == url + if result: + break -proc newPackage*(name: string; path: string; dist: DistMethod; - url: Uri): Package = +proc contains*(packages: Packages; identity: Identity): bool = + case identity.kind + of Name: + result = identity.name in packages + of Url: + result = identity.url in packages + +proc add*(packages: var Packages; id: Identity; package: Package) = + # this effectively asserts Identity.kind == Name + if id.name in packages: + raise newException(ValueError, "duplicates unsupported") + packages[id.name] = package + +iterator pairs*(packages: Packages): tuple[key: Identity; val: Package] = + for name, package in sorta.pairs(packages): + yield (key: newIdentity(name), val: package) + +proc `[]`*(packages: Packages; url: Uri): Package = + block found: + for package in values(packages): + if package.url == url: + result = package + break found + raise newException(KeyError, "not found") + +proc `[]`*(packages: Packages; identity: Identity): Package = + case identity.kind + of Name: + result = packages[identity.name] + of Url: + result = packages[identity.url] + +proc newPackage*(name: PackageName; path: AbsoluteDir; + dist: Dist; url: Uri): Package = ## create a new package that probably points to a local repo result = Package(name: name, dist: dist, url: url, - path: path, local: path.dirExists) + path: path, local: dirExists(path)) -proc newPackage*(name: string; dist: DistMethod; url: Uri): Package = +proc newPackage*(name: PackageName; dist: Dist; url: Uri): Package = ## create a new package result = Package(name: name, dist: dist, url: url) @@ -68,19 +117,18 @@ proc newPackage*(url: Uri): Package = # we had to guess at what the final name might be... result.naive = true -proc newPackage(name: string; license: string; description: string): Package = +proc newPackage(name: PackageName; license: string; desc: string): Package = ## create a new package for nimble's package list consumer - result = Package(name: name, license: license, description: description) + result = Package(name: name, license: license, description: desc) proc `$`*(package: Package): string = - result = package.name + result = $package.name if package.naive: result &= " (???)" -proc newPackageGroup*(flags: set[Flag] = defaultFlags): PackageGroup = +proc newPackages*(): Packages = ## instantiate a new package group for collecting a list of packages - result = PackageGroup(flags: flags) - result.init(flags, mode = modeStyleInsensitive) + result = initSortedTable[PackageName, Package]() proc aimAt*(package: Package; req: Requirement): Package = ## produce a refined package which might meet the requirement @@ -96,16 +144,16 @@ proc aimAt*(package: Package; req: Requirement): Package = result.naive = false result.web = package.web -proc add(group: PackageGroup; js: JsonNode) = +proc add(group: Packages; js: JsonNode) = ## how packages get added to a group from the json list var - name = js["name"].getStr + name = packageName js["name"].getStr package = newPackage(name = name, license = js.getOrDefault("license").getStr, - description = js.getOrDefault("description").getStr) + desc = js.getOrDefault("description").getStr) if "alias" in js: - raise newException(ValueError, "don't add aliases thusly") + raise newException(Defect, "don't add aliases thusly") if "url" in js: package.url = js["url"].getStr.parseUri @@ -114,7 +162,7 @@ proc add(group: PackageGroup; js: JsonNode) = else: package.web = package.url if "method" in js: - package.dist = parseEnum[DistMethod](js["method"].getStr) + package.dist = parseEnum[Dist](js["method"].getStr) if "author" in js: package.author = js["author"].getStr else: @@ -122,19 +170,19 @@ proc add(group: PackageGroup; js: JsonNode) = if "tags" in js: package.tags = mapIt(js["tags"], it.getStr.toLowerAscii) - group.add name, package + group.add newIdentity(name), package -proc getOfficialPackages*(nimbleDir: string): PackagesResult {.raises: [].} = +proc getOfficialPackages*(nimbleDir: AbsoluteDir): PackagesResult = ## parse the official packages list from nimbledir var - filename = ///nimbleDir - if filename.endsWith(//////PkgDir): - filename = nimbledir.parentDir / officialPackages - else: - filename = nimbledir / officialPackages + filename = + if nimbleDir.endsWith PkgDir: + nimbleDir.parentDir / officialPackages.RelativeFile + else: + nimbleDir / officialPackages.RelativeFile # make sure we have a sane return value - result = PackagesResult(ok: false, why: "", packages: newPackageGroup()) + result = PackagesResult(ok: false, why: "", packages: newPackages()) var group = result.packages block parsing: @@ -145,29 +193,28 @@ proc getOfficialPackages*(nimbleDir: string): PackagesResult {.raises: [].} = break # grab the file info for aging purposes - result.info = getFileInfo(filename) + result.info = getFileInfo($filename) # okay, i guess we have to read and parse this silly thing let - content = readFile(filename) + content = readFile($filename) js = parseJson(content) # consume the json array var - aliases: seq[tuple[name: string; alias: string]] + aliases: seq[tuple[name: PackageName; alias: PackageName]] for node in js.items: # if it's an alias, stash it for later if "alias" in node: - aliases.add (node.getOrDefault("name").getStr, - node["alias"].getStr) - continue - - # else try to add it to the group - try: - group.add node - except Exception as e: - notice node - warn &"error parsing package: {e.msg}" + aliases.add (packageName node.getOrDefault("name").getStr, + packageName node["alias"].getStr) + else: + # else try to add it to the group + try: + group.add node + except Exception as e: + notice node + warn &"error parsing package: {e.msg}" # now add in the aliases we collected for name, alias in aliases.items: @@ -194,7 +241,7 @@ proc ageInDays*(found: PackagesResult): int64 = ## days since the packages file was last refreshed result = (getTime() - found.info.lastWriteTime).inDays -proc toUrl*(requirement: Requirement; group: PackageGroup): Option[Uri] = +proc toUrl*(requirement: Requirement; group: Packages): Option[Uri] = ## try to determine the distribution url for a requirement var url: Uri @@ -218,17 +265,17 @@ proc toUrl*(requirement: Requirement; group: PackageGroup): Option[Uri] = url.anchor = requirement.release.asUrlAnchor result = url.some -proc hasUrl*(group: PackageGroup; url: Uri): bool = +proc hasUrl*(group: Packages; url: Uri): bool = ## true if the url seems to match a package in the group - for value in group.values: + for value in items(group): result = bareUrlsAreEqual(value.url.convertToGit, url.convertToGit) if result: break -proc matching*(group: PackageGroup; req: Requirement): PackageGroup = +proc matching*(group: Packages; req: Requirement): Packages = ## select a subgroup of packages that appear to match the requirement - result = newPackageGroup() + result = newPackages() if req.isUrl: let findurl = req.toUrl(group) @@ -248,10 +295,13 @@ proc matching*(group: PackageGroup; req: Requirement): PackageGroup = when defined(debug): debug "matched the package by name" -iterator urls*(group: PackageGroup): Uri = +iterator urls*(group: Packages): Uri = ## yield (an ideally git) url for each package in the group for package in group.values: yield if package.dist == Git: package.url.convertToGit else: package.url + +assert Packages is ImportGroup[Package] +assert Packages is IdentityGroup[Package] diff --git a/src/nimph/project.nim b/nimph/projects.nim similarity index 71% rename from src/nimph/project.nim rename to nimph/projects.nim index 1f9d0c2..d3e3b6a 100644 --- a/src/nimph/project.nim +++ b/nimph/projects.nim @@ -1,30 +1,8 @@ -#[ - -this is the workflow we want... - -git clone --depth 1 --branch 1.8.0 someurl somedir - - ... later ... - -git fetch origin tag 1.8.1 -git checkout 1.8.1 - -some outstanding issues: - -✅clone a repo from a url; -❌shallow clone with only the most recent reference? -✅rename package directory to match nimble semantics; -✅determine a url for the original repo -- use origin; -✅determine the appropriate reference to add to the anchor; -✅does the current commit match an existing tag? - - -]# - import std/math import std/hashes import std/strutils import std/tables +import std/sets import std/uri import std/options import std/strformat @@ -38,74 +16,166 @@ import std/sequtils import bump import gittyup +import ups/runner + import nimph/spec import nimph/config -import nimph/runner import nimph/nimble -import nimph/package -import nimph/version +import nimph/packages +import nimph/versions import nimph/thehub import nimph/versiontags -import nimph/requirement +import nimph/requirements +import nimph/paths -import nimph/group -export group +import nimph/groups +export groups type Project* = ref object + case dist*: Dist + of Nimble: + develop*: LinkedSearchResult + nimble*: DotNimble + meta*: NimbleMeta + dump*: StringTableRef + version*: Version + of Git: + repository*: AbsoluteDir + tags*: GitTagTable + else: + discard name*: string - nimble*: Target - version*: Version - dist*: DistMethod release*: Release - dump*: StringTableRef config*: NimphConfig cfg*: ConfigRef mycfg*: ConfigRef - tags*: GitTagTable - meta*: NimbleMeta url*: Uri parent*: Project - develop*: LinkedSearchResult - ProjectGroup* = Group[string, Project] + Projects* = OrderedTable[AbsoluteDir, Project] + + Requirements* = seq[Requirement] + RequirementsTags* = Table[Requirements, GitThing] + +proc `$`*(project: Project): string = + if project.isNil: + result = &"(nil project)" + else: + result = &"{project.name}-{project.release}" + +proc root*(project: Project): AbsoluteDir + +proc findDotNimble(project: Project): Option[DotNimble] = + debug &"find .nimble for {project}" + block: + if project.dist == Nimble: + if fileExists(project.nimble): + result = some(project.nimble) + break + let search = findTarget($project.root, ascend = false, + extensions = @[dotNimble]) + if search.found.isSome: + result = some(get(search.found).toDotNimble) + +proc root(file: DotNimble): AbsoluteDir = + ## find the project directory given a .nimble + assert not file.AbsoluteFile.isEmpty + result = parentDir(file.AbsoluteFile) + +proc root(dir: AbsoluteDir): AbsoluteDir = + ## find the project directory given a repository directory + assert not dir.isEmpty + result = dir + if endsWith(result, dotGit): + result = parentDir(result) + +proc root*(project: Project): AbsoluteDir = + ## it's the directory holding our .nimble + assert not project.isNil + case project.dist + of Nimble: + result = project.nimble.root + of Git: + result = project.repository.root + else: + raise newException(Defect, "unimplemented") + +proc gitDir*(project: Project): AbsoluteDir = + assert not project.isNil + case project.dist + of Git: + project.repository + of Nimble: + project.root / dotGit.RelativeDir + else: + raise newException(Defect, "unimplemented") + +proc hasNimble*(project: Project): bool = + case project.dist + of Git, Local: + false + of Nimble: + fileExists($project.nimble) + else: + raise newException(Defect, "unimplemented") + +proc hasGit*(project: Project): bool = + case project.dist + of Git: + true + of Nimble: + dirExists(project.gitDir) or fileExists($project.gitDir) + else: + raise newException(Defect, "unimplemented") + +when not AndNimble: + proc nimble*(project: Project): DotNimble = + if project.dist != Nimble: + raise newException(Defect, "nimble() on non-Nimble project") + let + search = findDotNimble(project) + if search.isNone: + error &"unable to find a {dotNimble} file for {project}" + quit 1 + else: + result = get(search) - Releases* = TableRef[string, Release] +template repo*(project: Project): string {.deprecated: "use root".} = + $project.root - LinkedSearchResult* = ref object - via: LinkedSearchResult - source: string - search: SearchResult +template nimCfg*(project: Project): AbsoluteFile = + ## the location of the project's nim.cfg + project.root / NimCfg.RelativeFile - # same as Requires, for now - Requirements* = OrderedTableRef[Requirement, Requirement] - RequirementsTags* = Group[Requirements, GitThing] +template isSubmodule*(project: Project): bool = + fileExists($project.gitDir) -template repo*(project: Project): string = project.nimble.repo -template gitDir*(project: Project): string = project.repo / dotGit -template hasGit*(project: Project): bool = - dirExists(project.gitDir) or fileExists(project.gitDir) template hgDir*(project: Project): string = project.repo / dotHg template hasHg*(project: Project): bool = dirExists(project.hgDir) -template nimphConfig*(project: Project): string = project.repo / configFile -template hasNimph*(project: Project): bool = fileExists(project.nimphConfig) -template localDeps*(project: Project): string = `///`(project.repo / DepDir) + +proc nimphConfig*(project: Project): AbsoluteFile = + result = project.root / RelativeFile(configFile) + +template hasNimph*(project: Project): bool = + fileExists(project.nimphConfig) + +template localDeps*(project: Project): string = + project.root / RelativeDir(DepDir) + template packageDirectory*(project: Project): string {.deprecated.} = project.nimbleDir / PkgDir template hasReleaseTag*(project: Project): bool = project.release.kind == Tag -template nimCfg*(project: Project): Target = - newTarget(project.nimble.repo / NimCfg) - template hasLocalDeps*(project: Project): bool = dirExists(project.localDeps) -proc nimbleDir*(project: Project): string = +proc nimbleDir*(project: Project): AbsoluteDir = ## the path to the project's dependencies var - globaldeps = getHomeDir() / ///dotNimble + globaldeps = getHomeDir().toAbsoluteDir / RelativeDir(dotNimble) # if we instantiated this project from another, the implication is that we # want to point at whatever that parent project is using as its nimbleDir. @@ -115,33 +185,32 @@ proc nimbleDir*(project: Project): string = # otherwise, if we have configuration data, we should use it to determine # what the user might be using as a package directory -- local or elsewise elif project.cfg != nil: - result = project.cfg.suggestNimbleDir(local = project.localDeps, - global = globaldeps) + result = suggestNimbleDir(project.cfg, + local = project.localDeps, + global = globaldeps) # otherwise, we'll just presume some configuration-free defaults else: - if project.hasLocalDeps: - result = project.localDeps - else: - result = globaldeps - result = absolutePath(result).normalizedPath - -proc `$`*(project: Project): string = - result = &"{project.name}-{project.release}" + result = toAbsoluteDir: + if project.hasLocalDeps: + project.localDeps + else: + globaldeps proc fetchConfig*(project: var Project; force = false): bool = ## ensure we've got a valid configuration to work with if project.cfg == nil or force: if project.parent == nil: debug &"config fetch for parent {project}" - project.cfg = loadAllCfgs(project.repo) + project.cfg = loadAllCfgs(project.root) else: project.cfg = project.parent.cfg result = true else: discard when defined(debug): - notice &"unnecessary config fetch for {project}" + if not force: + notice &"unnecessary config fetch for {project}" proc runSomething*(project: Project; exe: string; args: seq[string]; opts = {poParentStreams}): RunOutput = @@ -173,27 +242,25 @@ proc guessVersion*(project: Project): Version = if not result.isValid: error &"the version in {project.nimble} seems to be invalid" -proc fetchDump*(project: var Project; package: string; refresh = false): bool = - ## make sure the nimble dump is available - if project.dump == nil or refresh: - discard project.fetchConfig - let - dumped = fetchNimbleDump(package, nimbleDir = project.nimbleDir) - result = dumped.ok - if not result: - # puke on this for now... - raise newException(IOError, dumped.why) - # try to prevent a bug when the above changes - project.dump = dumped.table - else: - result = true - proc fetchDump*(project: var Project; refresh = false): bool {.discardable.} = ## make sure the nimble dump is available - result = project.fetchDump(project.nimble.repo, refresh = refresh) + if project.hasNimble: + if project.dump != nil and not refresh: + result = true + else: + discard project.fetchConfig + let + dumped = fetchNimbleDump(project.root, nimbleDir = project.nimbleDir) + result = dumped.ok + if not result: + # puke on this for now... + raise newException(IOError, dumped.why) + # try to prevent a bug when the above changes + project.dump = dumped.table proc knowVersion*(project: var Project): Version = ## pull out all the stops to determine the version of a project + assert project.dist == Nimble block: # this is really the most likely to work, so start with a dump if project.dump != nil: @@ -216,31 +283,46 @@ proc knowVersion*(project: var Project): Version = result = project.knowVersion if not result.isValid: - raise newException(IOError, "unable to determine {project.package} version") + let msg = &"unable to determine {project.name} version" + error msg + raise newException(ValueError, msg) + +proc newProject*(repo: GitRepository): Project = + ## instantiate a new project from the given repo + result = Project(dist: Git, repository: repositoryPath(repo).toAbsoluteDir) + var + splat = splitFile($result.root) + when false: + let + search = findDotNimble(project) + if search.found.isSome: + result.nimble = get(search.found) + # truncate any extension from the directory name (weirdos) + result.name = splat.name # this is our nominal project name + assert result.name != "", repr(splat) + result.config = newNimphConfig(result.nimphConfig) proc newProject*(nimble: Target): Project = ## instantiate a new project from the given .nimble - new result if not fileExists($nimble): raise newException(ValueError, "unable to instantiate a project w/o a " & dotNimble) - let - splat = absolutePath($nimble).normalizedPath.splitFile - result.nimble = (repo: splat.dir, package: splat.name, ext: splat.ext) - result.name = splat.name - result.config = newNimphConfig(splat.dir / configFile) + result = Project(dist: Nimble, nimble: nimble.toDotNimble) + result.name = result.nimble.package + result.config = newNimphConfig(result.nimphConfig) proc getHeadOid*(project: Project): GitResult[GitOid] = ## retrieve the #head oid from the project's repository if project.dist != Git: let emsg = &"{project} lacks a git repository to load" # noqa raise newException(Defect, emsg) - block: - repository := openRepository(project.gitDir): - error &"unable to open repo at `{project.repo}`: {code.dumpError}" - result.err code - break - result = repository.getHeadOid + else: + block: + repository := repositoryOpen($project.gitDir): + error &"unable to open repo at `{project.repo}`: {code.dumpError}" + result.err code + break + result = repository.getHeadOid proc demandHead*(repository: GitRepository): string = ## retrieve the repository's #head oid as a string, or "" @@ -268,7 +350,7 @@ proc shortOid(oid: GitOid; size = 6): string = template matchingBranches(project: Project; body: untyped): untyped = block: - repository := openRepository(project.gitDir): + repository := repositoryOpen($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break for bref in repository.branches: @@ -351,7 +433,7 @@ proc fetchTagTable*(project: var Project) = block: if project.dist != Git: break - repository := openRepository(project.gitDir): + repository := repositoryOpen($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break let @@ -373,7 +455,7 @@ proc releaseSummary*(project: Project): string = else: # else, lookup the summary for the tag or commit block: - repository := openRepository(project.gitDir): + repository := repositoryOpen($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break thing := repository.lookupThing(project.release.reference): @@ -460,11 +542,11 @@ proc inventRelease*(project: var Project) = if project.url.anchor.len > 0: project.release = newRelease(project.url.anchor, operator = Tag) # else if we have a version for the project, use that - elif project.version.isValid: + elif project.dist == Nimble and project.version.isValid: project.release = newRelease(project.version) else: # grab the directory name - let name = repo(project).lastPathPart + let name = lastPathPart($project.root) # maybe it's package-something var prefix = project.name & "-" if name.startsWith(prefix): @@ -476,9 +558,9 @@ proc inventRelease*(project: var Project) = else: warn &"unable to parse reference from directory `{name}`" -proc guessDist(project: Project): DistMethod = +proc guessDist(project: Project): Dist {.deprecated.} = ## guess at the distribution method used to deposit the assets - if project.hasGit: + if project.hasGit or project.isSubmodule: result = Git elif project.hasHg: result = Merc @@ -487,60 +569,11 @@ proc guessDist(project: Project): DistMethod = else: result = Local -proc parseNimbleLink(path: string): tuple[nimble: string; source: string] = - ## parse a dotNimbleLink file into its constituent components - let - lines = readFile(path).splitLines - if lines.len != 2: - raise newException(ValueError, "malformed " & path) - result = (nimble: lines[0], source: lines[1]) - -proc linkedFindTarget(dir: string; target = ""; nimToo = false; - ascend = true): LinkedSearchResult = - ## recurse through .nimble-link files to find the .nimble - var - extensions = @[dotNimble, dotNimbleLink] - if nimToo: - extensions = @["".addFileExt("nim")] & extensions - - # perform the search with our cleverly-constructed extensions - result = LinkedSearchResult() - result.search = findTarget(dir, extensions = extensions, - target = target, ascend = ascend) - - # if we found nothing, or we found a dotNimble, then we're done - let found = result.search.found - if found.isNone or found.get.ext != dotNimbleLink: - return - - # now we need to parse this dotNimbleLink and recurse on the target - try: - let parsed = parseNimbleLink($found.get) - if fileExists(parsed.nimble): - result.source = parsed.source - # specify the path to the .nimble and the .nimble filename itself - var recursed = linkedFindTarget(parsed.nimble.parentDir, nimToo = nimToo, - target = parsed.nimble.extractFilename, - ascend = ascend) - # if the recursion was successful, add ourselves to the chain and return - if recursed.search.found.isSome: - recursed.via = result - return recursed - - # a failure mode yielding a useful explanation - result.search.message = &"{found.get} didn't lead to a {dotNimble}" - except ValueError as e: - # a failure mode yielding a less-useful explanation - result.search.message = e.msg - - # critically, set the search to none because ultimately, we found nothing - result.search.found = none(Target) - proc findRepositoryUrl*(project: Project; name = defaultRemote): Option[Uri] = ## find the (remote?) url to a given local repository block complete: block found: - repository := openRepository(project.gitDir): + repository := repositoryOpen($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break found let @@ -582,10 +615,15 @@ proc createUrl*(project: Project; refresh = false): Uri = # make something up case project.dist: of Local: - # sometimes nimble provides a url during installation - if project.meta.hasUrl: - # sometimes... - result = project.meta.url + when AndNimble: + # sometimes nimble provides a url during installation + if project.meta.hasUrl: + # sometimes... + result = project.meta.url + else: + # we don't handle "Local" projects anymore + error &"unable to determine source url for {project.name}" + raise newException(Defect, "define AndNimble to support Nimble") of Git: # try looking at remotes let url = findRepositoryUrl(project, defaultRemote) @@ -611,7 +649,7 @@ proc createUrl*(project: var Project; refresh = false): Uri = result = readonly.createUrl(refresh = refresh) if result != project.url: # update the nimble metadata with this new url - if not writeNimbleMeta(project.repo, result, result.anchor): + if not writeNimbleMeta(project.root, result, result.anchor): warn &"unable to update {project.name}'s {nimbleMeta}" # cache the result if the project is mutable @@ -620,63 +658,81 @@ proc createUrl*(project: var Project; refresh = false): Uri = proc refresh*(project: var Project) = ## appropriate to run to scan for and set some basic project data let - mycfg = parseConfigFile($project.nimCfg) + mycfg = parseConfigFile(project.nimCfg) if mycfg.isSome: project.mycfg = mycfg.get project.url = project.createUrl(refresh = true) - project.dump = nil - project.version = project.knowVersion + case project.dist + of Nimble: + project.dump = nil + project.version = project.knowVersion + else: + discard project.inventRelease -proc findProject*(project: var Project; dir: string; - parent: Project = nil): bool = +proc findProject*(dir: AbsoluteDir; parent: Project = nil): Project = ## locate a project starting from `dir` and set its parent if applicable block complete: - let - target = linkedFindTarget(dir, ascend = true) - # a failure lets us out early - if target.search.found.isNone: - if target.search.message != "": - error target.search.message - break complete - - elif target.via != nil: - var - target = target # shadow linked search result - # output some debugging data to show how we got from here to there - while target.via != nil: - debug &"--> via {target.via.search.found.get}" - target = target.via - - # there's really no scenario in which we need to instantiate a - # new parent project when looking for children... - if parent != nil: - if parent.nimble == target.search.found.get: + when AndNimble: + let + target = linkedFindTarget(dir, ascend = true) + # a failure lets us out early + if target.search.found.isNone: + if target.search.message != "": + error target.search.message + break complete + elif target.via != nil: + var + target = target # shadow linked search result + # output some debugging data to show how we got from here to there + while target.via != nil: + debug &"--> via {target.via.search.found.get}" + target = target.via + + # there's really no scenario in which we need to instantiate a + # new parent project when looking for children... + if parent != nil: + if parent.nimble == target.search.found.get: + break complete + + # create an instance and setup some basic (cheap) data + result = newProject(target.search.found.get) + + # the parent will be set on child dependencies + result.parent = parent + + # this is the nimble-link chain that we might have a use for + result.develop = target.via + result.meta = fetchNimbleMeta(result.repo) + else: + # get the buffer holding the path, if possible + path := repositoryDiscover($dir): + error &"no git repository found in any parent of `{dir}`" break complete - # create an instance and setup some basic (cheap) data - project = newProject(target.search.found.get) - # the parent will be set on child dependencies - project.parent = parent - # this is the nimble-link chain that we might have a use for - project.develop = target.via - project.meta = fetchNimbleMeta(project.repo) - project.dist = project.guessDist - # load configs, create urls, set version and release, etc. - project.refresh + # we have a path, so try to open the repo + repo := repositoryOpen(path): + error &"unable to open repo at `{path}`: {code.dumpError}" + break complete - # if we cannot determine what release this project is, just bail - if not project.release.isValid: - # but make sure to zero out the result so as not to confuse the user - project = nil - error &"unable to determine reference for {project}" - break complete + # create an instance and setup some basic (cheap) data + result = newProject(repo) - # otherwise, we're golden - debug &"{project} version {project.version}" - result = true + # the parent will be set on child dependencies + result.parent = parent + + # load configs, create urls, set version and release, etc. + result.refresh + + if not result.release.isValid: + # we cannot determine what release this project is + notice &"unable to determine reference for {result.name}" + info "maybe add a commit?" + else: + # otherwise, we're golden + debug "found " & $result -iterator packageDirectories(project: Project): string = +iterator packageDirectories(project: Project): AbsoluteDir = ## yield directories according to the project's path configuration if project.parent != nil: raise newException(Defect, "nonsensical outside root project") @@ -685,54 +741,44 @@ iterator packageDirectories(project: Project): string = for directory in project.cfg.packagePaths(exists = true): yield directory -proc newProjectGroup*(flags: set[Flag] = defaultFlags): ProjectGroup = - const mode = - when FilesystemCaseSensitive: - modeCaseSensitive - else: - modeCaseInsensitive - result = ProjectGroup(flags: flags) - result.init(flags, mode = mode) - -proc importName*(linked: LinkedSearchResult): string = - ## a uniform name usable in code for imports - if linked.via != nil: - result = linked.via.importName - else: - # if found isn't populated, we SHOULD crash here - result = linked.search.found.get.importName +proc newProjects*(): Projects = + result = Projects() proc importName*(project: Project): string = ## a uniform name usable in code for imports - if project.develop != nil: - result = project.develop.importName + if project.dist == Nimble: + if project.develop != nil: + result = project.develop.importName + else: + result = project.nimble.importName else: - result = project.nimble.importName + result = project.root.importName -proc hasProjectIn*(group: ProjectGroup; directory: string): bool = +proc hasProjectIn*(group: Projects; directory: AbsoluteDir): bool = ## true if a project is stored at the given directory result = group.hasKey(directory) -proc getProjectIn*(group: ProjectGroup; directory: string): Project = +proc getProjectIn*(group: Projects; directory: AbsoluteDir): Project = ## retrieve a project via its path result = group.get(directory) -proc mgetProjectIn*(group: var ProjectGroup; directory: string): var Project = +proc mgetProjectIn*(group: var Projects; + directory: AbsoluteDir): var Project = ## retrieve a mutable project via its path result = group.mget(directory) -proc availableProjects*(project: Project): ProjectGroup = +proc availableProjects*(project: Project): Projects = ## find packages locally available to a project; note that ## this will include the project itself - result = newProjectGroup() - result.add project.repo, project + result = newProjects() + result[project.root] = project for directory in project.packageDirectories: - var proj: Project - if findProject(proj, directory, parent = project): - if proj.repo notin result: - result.add proj.repo, proj - else: + let proj = findProject(directory, parent = project) + if proj.isNil: debug &"no package found in {directory}" + else: + if proj.root notin result: + result[proj.root] = proj proc `==`*(a, b: Project): bool = ## a dirty (if safe) way to compare equality of projects @@ -740,8 +786,8 @@ proc `==`*(a, b: Project): bool = result = a.isNil == b.isNil else: let - apath = $a.nimble - bpath = $b.nimble + apath = $a.root + bpath = $b.root if apath == bpath: result = true else: @@ -749,11 +795,11 @@ proc `==`*(a, b: Project): bool = debug &"had to use samefile to compare {apath} to {bpath}" result = sameFile(apath, bpath) -proc removeSearchPath*(project: Project; path: string): bool = +proc removeSearchPath*(project: Project; path: AbsoluteDir): bool = ## remove a search path from the project's nim.cfg result = project.cfg.removeSearchPath(project.nimCfg, path) -proc removeSearchPath*(project: var Project; path: string): bool = +proc removeSearchPath*(project: var Project; path: AbsoluteDir): bool = ## remove a search path from the project's nim.cfg; reload config let readonly = project @@ -762,11 +808,11 @@ proc removeSearchPath*(project: var Project; path: string): bool = if not project.fetchConfig(force = true): warn &"unable to read config for {project}" -proc excludeSearchPath*(project: Project; path: string): bool = +proc excludeSearchPath*(project: Project; path: AbsoluteDir): bool = ## exclude a search path from the project's nim.cfg result = project.cfg.excludeSearchPath(project.nimCfg, path) -proc excludeSearchPath*(project: var Project; path: string): bool = +proc excludeSearchPath*(project: var Project; path: AbsoluteDir): bool = ## exclude a search path from the project's nim.cfg; reload config let readonly = project @@ -775,7 +821,7 @@ proc excludeSearchPath*(project: var Project; path: string): bool = if not project.fetchConfig(force = true): warn &"unable to read config for {project}" -proc addSearchPath*(project: Project; path: string): bool = +proc addSearchPath*(project: Project; path: AbsoluteDir): bool = ## add a search path to the given project's configuration; ## true if we added the search path block complete: @@ -786,7 +832,7 @@ proc addSearchPath*(project: Project; path: string): bool = raise newException(Defect, "load a configuration first") result = project.cfg.addSearchPath(project.nimCfg, path) -proc addSearchPath*(project: var Project; path: string): bool = +proc addSearchPath*(project: var Project; path: AbsoluteDir): bool = ## add a search path to the project's nim.cfg; reload config let readonly = project @@ -795,7 +841,7 @@ proc addSearchPath*(project: var Project; path: string): bool = if not project.fetchConfig(force = true): warn &"unable to read config for {project}" -proc determineSearchPath(project: Project): string = +proc determineSearchPath(project: Project): AbsoluteDir = ## produce the search path to add for a given project if project.dump == nil: raise newException(Defect, "no dump available") @@ -804,20 +850,20 @@ proc determineSearchPath(project: Project): string = if "srcDir" in project.dump: let srcDir = project.dump["srcDir"] if srcDir != "": - withinDirectory(project.repo): - result = srcDir.absolutePath - if result.dirExists: + withinDirectory(project.root): + result = srcDir.toAbsoluteDir + if dirExists(result): break - result = project.repo - result = ///result + result = project.root -iterator missingSearchPaths*(project: Project; target: Project): string = - ## one (or more?) paths to the target package which are - ## apparently missing from the project's search paths +iterator missingSearchPaths*(project: Project; + target: Project): AbsoluteDir = + ## one (or more?) paths to the target package which are apparently + ## missing from the project's search paths let - path = ///determineSearchPath(target) + path = determineSearchPath(target) block found: - if not path.dirExists: + if not dirExists(path): warn &"search path for {project.name} doesn't exist" break for search in project.cfg.packagePaths(exists = false): @@ -825,10 +871,12 @@ iterator missingSearchPaths*(project: Project; target: Project): string = break found yield path -iterator missingSearchPaths*(project: Project; target: var Project): string = - ## one (or more?) path to the target package which are apparently missing from - ## the project's search paths; this will resolve up the parent tree to find - ## the highest project in which to modify a configuration +iterator missingSearchPaths*(project: Project; + target: var Project): AbsoluteDir = + ## one (or more?) path to the target package which are apparently + ## missing from the project's search paths; this will resolve up + ## the parent tree to find the highest project in which to modify a + ## configuration if not target.fetchDump: warn &"unable to fetch dump for {target}; this won't end well" @@ -853,7 +901,7 @@ proc addMissingSearchPathsTo*(project: var Project; cloned: var Project) = then vary. ]# - project.cfg = loadAllCfgs(project.repo) + project.cfg = loadAllCfgs(project.root) # a future relocation will break this, of course for path in project.missingSearchPaths(cloned): if project.addSearchPath(path): @@ -900,7 +948,13 @@ proc relocateDependency*(parent: var Project; project: var Project) = # now we can actually move the repo... moveDir(repository, future) # reset the package configuration target - project.nimble = newTarget(nimble) + case project.dist + of Nimble: + project.nimble = nimble.toDotNimble + of Git: + project.repository = future.toAbsoluteDir + else: + discard # the path changed, so remove the old path (if you can) discard parent.removeSearchPath(previous) # and point the parent to the new one @@ -909,7 +963,7 @@ proc relocateDependency*(parent: var Project; project: var Project) = proc addMissingUpstreams*(project: Project) = ## review the local branches and add any missing tracking branches block: - repository := openRepository(project.gitDir): + repository := repositoryOpen($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break @@ -939,13 +993,12 @@ proc addMissingUpstreams*(project: Project) = warn &"error fetching upstream for {name}: {code}" warn code.dumpError -proc clone*(project: var Project; url: Uri; name: string; - cloned: var Project): bool = +proc clone*(project: var Project; url: Uri; name: string): Project = ## clone a package into the project's nimbleDir var bare = url tag: string - directory = project.nimbleDir.stripPkgs / PkgDir + directory = project.nimbleDir.stripPkgs / RelativeDir(PkgDir) if bare.anchor != "": tag = bare.anchor @@ -954,107 +1007,113 @@ proc clone*(project: var Project; url: Uri; name: string; {.warning: "clone into a temporary directory".} # we have to strip the # from a version tag for the compiler's benefit let loose = parseVersionLoosely(tag) - if loose.isSome and loose.get.isValid: - directory = directory / name & "-" & $loose.get + if loose.isSome and get(loose).isValid: + directory = directory / RelativeDir(name & "-" & $get(loose)) elif tag.len != 0: - directory = directory / name & "-#" & tag + directory = directory / RelativeDir(name & "-#" & tag) else: - directory = directory / name & "-#head" + directory = directory / RelativeDir(name & "-#head") - if directory.dirExists: + if dirExists(directory): error &"tried to clone into {directory}, but it already exists" return # don't clone the compiler when we're debugging nimph when defined(debug): if "github.com/nim-lang/Nim" in $bare: - raise newException(Defect, "won't clone the compiler when debugging nimph") + raise newException(Defect, + "won't clone the compiler when debugging nimph") fatal &"👭cloning {bare}..." info &"... into {directory}" # clone the bare url into the given directory, yielding a repository object - repository := clone(bare, directory): + repository := clone(bare, $directory): # or, if there was a problem, dump some error messages and bail out error &"unable to clone into `{directory}`: {code.dumpError}" return # make sure the project we find is in the directory we cloned to; # this could differ if the repo does not feature a dotNimble file - if findProject(cloned, directory, parent = project) and - cloned.repo == directory: + result = findProject(directory, parent = project) + if result.root == directory: {.warning: "gratuitous nimblemeta write?".} let oid = repository.demandHead - if not writeNimbleMeta(directory, bare, oid): - warn &"unable to write {nimbleMeta} in {directory}" + if result.dist == Nimble: + if not writeNimbleMeta(directory, bare, oid): + warn &"unable to write {nimbleMeta} in {directory}" # review the local branches and add any missing tracking branches - cloned.addMissingUpstreams - - result = true + result.addMissingUpstreams else: error "couldn't make sense of the project i just cloned" + # if we're gonna fail, ensure that failure is felt + result = nil - # if we're gonna fail, ensure that failure is felt - if not result: - cloned = nil - -proc allImportTargets*(config: ConfigRef; repo: string): - OrderedTableRef[Target, LinkedSearchResult] = - ## yield projects from the group in the same order that they may be - ## resolved by the compiler, if at all, given a particular configuration - result = newOrderedTable[Target, LinkedSearchResult]() +when AndNimble: + iterator asFoundVia*(group: var Projects; config: ConfigRef): var Project = + ## yield projects from the group in the same order that they may be + ## resolved by the compiler, if at all, given a particular configuration + var + dedupe = initHashSet[string]() - for path in config.extantSearchPaths: - let - target = linkedFindTarget(path, target = path.pathToImport.importName, - nimToo = true, ascend = false) - found = target.search.found - if found.isNone: - continue - result.add found.get, target - -iterator asFoundVia*(group: var ProjectGroup; config: ConfigRef; - repo: string): var Project = - ## yield projects from the group in the same order that they may be - ## resolved by the compiler, if at all, given a particular configuration - var - dedupe = newTable[string, Project](nextPowerOfTwo(group.len)) + # procede in path order to try to find projects using the paths + for path in config.packagePaths(exists = true): + let + target = linkedFindTarget(path, ascend = false) + found = target.search.found + if found.isNone: + continue + # see if the target project is in our group + for project in group.mvalues: + if found.get == project.nimble: + # if it is, put it in the dedupe and yield it + if project.importName notin dedupe: + dedupe.incl project.importName + yield project + break - # procede in path order to try to find projects using the paths - for path in config.packagePaths(exists = true): - let - target = linkedFindTarget(path, ascend = false) - found = target.search.found - if found.isNone: - continue - # see if the target project is in our group - for project in group.mvalues: - if found.get == project.nimble: - # if it is, put it in the dedupe and yield it - if project.importName notin dedupe: - dedupe.add project.importName, project - yield project - break + # now report on anything we weren't able to discover + for project in group.values: + if project.importName notin dedupe: + notice &"no path to {project.root} as `{project.importName}`" +else: + iterator asFoundVia*(group: var Projects; config: ConfigRef): var Project = + ## yield projects from the group in the same order that they may be + ## resolved by the compiler, if at all, given a particular configuration + var + dedupe = initHashSet[AbsoluteDir]() - # now report on anything we weren't able to discover - for project in group.values: - if project.importName notin dedupe: - notice &"no path to {project.repo} as `{project.importName}`" + # procede in path order to try to find projects using the paths + for path in config.packagePaths(exists = true): + repository := repositoryDiscover($path, ceilings = @[$path]): + continue + # turn the .git (?) repository directory into the project root + let root = repository.toAbsoluteDir.root + # if we have this project in the group, + if root in group: + # dedupe it and yield it + dedupe.incl root + yield group[root] + + # now report on anything we weren't able to discover + for project in group.values: + if project.root notin dedupe: + notice &"no path to {project.root}" proc countNimblePaths*(project: Project): - tuple[local: int; global: int; paths: seq[string]] = + tuple[local: int; global: int; paths: seq[AbsoluteDir]] = ## try to count the effective number of --nimblePaths - let - repository = project.repo - # we start with looking for the most-used directories and then resort to the - # least-frequently used entries, eventually settling for *any* lazy path at all + + # we start with looking for the most-used directories and then resort to + # the least-frequently used entries, eventually settling for *any* lazy + # path at all for iteration in countDown(2, 0): - for path in likelyLazy(project.cfg, repository, least = iteration): + for path in likelyLazy(project.cfg, project.root, least = iteration): # we'll also differentiate between lazy # paths inside/outside the project tree - if path.startsWith(repository): + if startsWith($path, $project.root): result.local.inc else: result.global.inc @@ -1099,7 +1158,7 @@ proc promoteRemoteLike*(project: Project; url: Uri; name = defaultRemote): bool # we'll add missing upstreams after this block block donehere: - repository := openRepository(project.gitDir): + repository := repositoryOpen($project.gitDir): error &"unable to open repo at `{path}`: {code.dumpError}" break @@ -1187,9 +1246,8 @@ proc promote*(project: Project; name = defaultRemote; if target.ok and target.owner == user.login: result = project.promoteRemoteLike(project.url, name = name) -proc newRequirementsTags(flags = defaultFlags): RequirementsTags = - result = RequirementsTags(flags: flags) - result.init(flags, mode = modeCaseSensitive) +proc newRequirementsTags(): RequirementsTags = + result = RequirementsTags() proc requirementChangingCommits*(project: Project): RequirementsTags = # a table of the commits that changed the Requirements in a Project's @@ -1202,7 +1260,7 @@ proc repoLockReady*(project: Project): bool = return block: - repository := openRepository(project.gitDir): + repository := repositoryOpen($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break @@ -1313,7 +1371,7 @@ proc setHeadToRelease*(project: var Project; release: Release): bool = {.warning: "roll to arbitrary releases".} if not release.isValid or release.kind != Tag: break - repository := openRepository(project.gitDir): + repository := repositoryOpen($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break # we want the code because it'll tell us what went wrong @@ -1343,7 +1401,7 @@ template returnToHeadAfter*(project: var Project; body: untyped) = error "refusing to roll the repo when it's dirty" break - repository := openRepository(project.gitDir): + repository := repositoryOpen($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break @@ -1376,7 +1434,7 @@ proc versionChangingCommits*(project: var Project): VersionTags = project.returnToHeadAfter: block: - repository := openRepository(project.gitDir): + repository := repositoryOpen($project.gitDir): error &"unable to open repo at `{project.repo}`: {code.dumpError}" break # iterate over commits to the dotNimble file @@ -1391,10 +1449,9 @@ proc versionChangingCommits*(project: var Project): VersionTags = project.refresh result[project.version] = thing.get -proc pathForName*(group: ProjectGroup; name: string): Option[string] = +proc pathForName*(group: Projects; name: ImportName): Option[AbsoluteDir] = ## try to retrieve the directory for a given import name in the group - let name = name.destylize for project in group.values: - if project.importName.destylize == name: - result = project.repo.some + if project.importName == name: + result = some(project.root) break diff --git a/src/nimph/requirement.nim b/nimph/requirements.nim similarity index 76% rename from src/nimph/requirement.nim rename to nimph/requirements.nim index 65da6ff..3128e65 100644 --- a/src/nimph/requirement.nim +++ b/nimph/requirements.nim @@ -9,18 +9,51 @@ import bump import npeg import nimph/spec -import nimph/version +import nimph/versions type + IdKind* {.pure.} = enum Name, Url + Identity* = object + case kind*: IdKind + of Name: + name*: PackageName + of Url: + url*: Uri + # the specification of a package requirement Requirement* = ref object - identity*: string + identity*: Identity operator*: Operator release*: Release child*: Requirement notes*: string - Requires* = OrderedTableRef[Requirement, Requirement] + Requires* = seq[Requirement] + +proc newIdentity*(name: PackageName): Identity = + result = Identity(kind: Name, name: name) + +proc newIdentity*(url: Uri): Identity = + assert bare(url) == url + result = Identity(kind: Url, url: url) + +proc newIdentity(s: string): Identity = + ## create a new identity from an arbitrary string; it's crude! + if ':' in s: + try: + result = newIdentity(parseUri s) + except: + raise newException(ValueError, &"unable to parse requirement `{s}`") + else: + result = newIdentity(packageName s) + +proc `$`*(id: Identity): string = + ## you won't be able to guess what this does + result = case id.kind + of Name: + $id.name + of Url: + $id.url proc `$`*(req: Requirement): string = result = &"{req.identity}{req.operator}{req.release}" @@ -123,6 +156,20 @@ proc isSatisfiedBy*(req: Requirement; spec: Release): bool = else: result = req.isSatisfiedBy spec.effectively +proc hash*(id: Identity): Hash = + ## uniquely identify a requirement's package name or url + var h: Hash = 0 + h = h !& hash(id.kind) + case id.kind + of Name: + h = h !& hash(id.name) + of Url: + h = h !& hash(id.url) + result = !$h + +proc `==`*(a, b: Identity): bool = + hash(a) == hash(b) + proc hash*(req: Requirement): Hash = ## uniquely identify a requirement var h: Hash = 0 @@ -150,15 +197,10 @@ iterator children*(parent: Requirement; andParent = false): Requirement = req = req.child yield req -proc newRequirement*(id: string; operator: Operator; +proc newRequirement*(id: Identity; operator: Operator; release: Release, notes = ""): Requirement = ## create a requirement from a release, eg. that of a project - when defined(debug): - if id != id.strip: - warn &"whitespace around requirement identity: `{id}`" - if id == "": - raise newException(ValueError, "requirements must have length, if not girth") - result = Requirement(identity: id.strip, release: release, notes: notes) + result = Requirement(identity: id, release: release, notes: notes) # if it parsed as Caret, Tilde, or Wild, then paint the requirement as such if result.release.kind in Wildlings: result.operator = result.release.kind @@ -169,11 +211,13 @@ proc newRequirement*(id: string; operator: Operator; else: result.operator = operator -proc newRequirement*(id: string; operator: Operator; spec: string): Requirement = +proc newRequirement*(id: Identity; operator: Operator; + spec: string): Requirement = ## parse a requirement from a string result = newRequirement(id, operator, newRelease(spec, operator = operator)) -proc newRequirement(id: string; operator: string; spec: string): Requirement = +proc newRequirement(id: Identity; operator: string; + spec: string): Requirement = ## parse a requirement with the given operator from a string var op = Equal @@ -193,8 +237,8 @@ proc parseRequires*(input: string): Option[Requires] = ## parse a `requires` string output from `nimble dump` ## also supports `~` and `^` and `*` operators a la cargo var - requires = Requires() - lastname: string + requires: seq[Requirement] + lastname: Identity let peggy = peg "document": @@ -208,19 +252,19 @@ proc parseRequires*(input: string): Option[Requires] = tag <- '#' * +(1 - ending) spec <- tag | ver anyrecord <- >name: - lastname = $1 - let req = newRequirement(id = $1, operator = Wild, spec = "*") + lastname = newIdentity $1 + let req = newRequirement(id = lastname, operator = Wild, spec = "*") if req notin requires: - requires[req] = req + requires.add req andrecord <- *white * >ops * *white * >spec: let req = newRequirement(id = lastname, operator = $1, spec = $2) if req notin requires: - requires[req] = req + requires.add req inrecord <- >name * *white * >ops * *white * >spec: - lastname = $1 - let req = newRequirement(id = $1, operator = $2, spec = $3) + lastname = newIdentity $1 + let req = newRequirement(id = lastname, operator = $2, spec = $3) if req notin requires: - requires[req] = req + requires.add req record <- (inrecord | andrecord | anyrecord) * ending document <- *record parsed = peggy.match(input) @@ -229,11 +273,11 @@ proc parseRequires*(input: string): Option[Requires] = proc isVirtual*(requirement: Requirement): bool = ## is the requirement something we should overlook? - result = requirement.identity.toLowerAscii in ["nim"] + if requirement.identity.kind == Name: + result = requirement.identity.name.importName in virtualNimImports proc isUrl*(requirement: Requirement): bool = - ## a terrible way to determine if the requirement is a url - result = ':' in requirement.identity + requirement.identity.kind == Url proc asUrlAnchor*(release: Release): string = ## produce a suitable url anchor referencing a release @@ -250,23 +294,21 @@ proc toUrl*(requirement: Requirement): Option[Uri] = ## try to determine the distribution url for a requirement # if it could be a url, try to parse it as such if requirement.isUrl: - try: - var url = parseUri(requirement.identity) - if requirement.release.kind in {Equal, Tag}: - url.anchor = requirement.release.asUrlAnchor - result = url.some - except: - warn &"unable to parse requirement `{requirement.identity}`" + var url = requirement.identity.url + if requirement.release.kind in {Equal, Tag}: + url.anchor = requirement.release.asUrlAnchor + result = some(url) + +proc importName*(identity: Identity): ImportName = + case identity.kind + of Name: + result = importName identity.name + of Url: + result = importName identity.url -proc importName*(requirement: Requirement): string = +proc importName*(requirement: Requirement): ImportName = ## guess the import name given only a requirement - block: - if requirement.isUrl: - let url = requirement.toUrl - if url.isSome: - result = url.get.importName - break - result = requirement.identity.importName + result = importName requirement.identity proc describe*(requirement: Requirement): string = ## describe a requirement and where it may have come from, if possible diff --git a/nimph/spec.nim b/nimph/spec.nim new file mode 100644 index 0000000..31897bf --- /dev/null +++ b/nimph/spec.nim @@ -0,0 +1,56 @@ +import std/options +import std/uri +import std/os +import std/times + +import bump +import cutelog +export cutelog + +import ups/sanitize +import ups/spec as upsspec +export upsspec + +type + Flag* {.pure.} = enum + Quiet + Strict + Force + Dry + Safe + Network + + FlagStack = seq[set[Flag]] + + RollGoal* = enum + Upgrade = "upgrade" + Downgrade = "downgrade" + Specific = "roll" + +const + hubTokenFn* {.strdefine.} = "".addFileExt("config") / "hub" + stalePackages* {.intdefine.} = 14 + configFile* {.strdefine.} = "nimph".addFileExt("json") + # add Safe to defaultFlags to, uh, default to Safe mode + defaultFlags*: set[Flag] = {Quiet, Strict} + shortDate* = initTimeFormat "yyyy-MM-dd" + AndNimble* = false # when true, try to support nimble + +# we track current options as a stack of flags +var flags*: FlagStack = @[defaultFlags] +proc contains*(flags: FlagStack; f: Flag): bool = f in flags[^1] +proc contains*(flags: FlagStack; fs: set[Flag]): bool = fs <= flags[^1] +template push*(flags: var FlagStack; fs: set[Flag]) = flags.add fs +template withFlags*(fs: set[Flag]; body: untyped) = + try: + flags.push fs + var flags {.inject.} = flags[^1] + body + finally: + flags.pop + +template timer*(name: string; body: untyped) = + ## crude timer for debugging purposes + let clock = epochTime() + body + debug name & " took " & $(epochTime() - clock) diff --git a/src/nimph/thehub.nim b/nimph/thehub.nim similarity index 96% rename from src/nimph/thehub.nim rename to nimph/thehub.nim index 959590a..3a145ed 100644 --- a/src/nimph/thehub.nim +++ b/nimph/thehub.nim @@ -17,7 +17,6 @@ import github import jsonconvert import nimph/spec -import nimph/group const hubTime* = initTimeFormat "yyyy-MM-dd\'T\'HH:mm:ss\'Z\'" @@ -113,7 +112,7 @@ type original*: bool score*: float - HubGroup* = ref object of Group[Uri, HubResult] + HubGroup* = OrderedTable[Uri, HubResult] HubSort* {.pure.} = enum Ascending = "asc" @@ -125,6 +124,8 @@ type Forks = "forks" Updated = "updated" +proc url*(r: HubResult): Uri = r.htmlUrl + proc shortly(stamp: DateTime): string = ## render a date shortly result = stamp.format(shortDate) @@ -318,13 +319,8 @@ proc newHubResult*(kind: HubKind; js: JsonNode): HubResult = result.htmlUrl = js.get("html_url", "").parseUri result.init(js) -proc newHubGroup*(flags: set[Flag] = defaultFlags): HubGroup = - result = HubGroup(flags: flags) - result.init(flags, mode = modeCaseSensitive) - -proc add*(group: var HubGroup; hub: HubResult) = - {.warning: "nim bug #12818".} - add[Uri, HubResult](group, hub.htmlUrl, hub) +proc newHubGroup*(): HubGroup = + result = HubGroup() proc authorize*(request: Recallable): bool = ## find and inject credentials into a github request @@ -374,21 +370,20 @@ proc queryMany(recallable: Recallable; kind: HubKind): Future[Option[HubGroup]] let js = parseJson(await response.body) # we know now that we'll be returning a group of some size - var - group = newHubGroup() - result = group.some + var group = newHubGroup() # add any parseable results to the group for node in js["items"].items: try: let item = newHubResult(kind, node) # if these are repositories, ignore forks - if kind == HubRepo and not item.original: - continue - group.add item + if kind != HubRepo or item.original: + group[item.url] = item except Exception as e: warn "error parsing repo: " & e.msg + result = some(group) + proc getGitHubUser*(): Future[Option[HubResult]] {.async.} = ## attempt to retrieve the authorized user var diff --git a/src/nimph/version.nim b/nimph/versions.nim similarity index 98% rename from src/nimph/version.nim rename to nimph/versions.nim index dacf00a..78f03e4 100644 --- a/src/nimph/version.nim +++ b/nimph/versions.nim @@ -337,11 +337,6 @@ proc toMask*(version: Version): VersionMask = for i, field in version.pairs: result[i] = field.some -proc importName*(target: Target): string = - ## a uniform name usable in code for imports - assert target.repo.len > 0 - result = target.repo.pathToImport.importName - iterator likelyTags*(version: Version): string = ## produce tags with/without silly `v` prefixes let v = $version diff --git a/src/nimph/versiontags.nim b/nimph/versiontags.nim similarity index 98% rename from src/nimph/versiontags.nim rename to nimph/versiontags.nim index f4b1f48..0deeb1f 100644 --- a/src/nimph/versiontags.nim +++ b/nimph/versiontags.nim @@ -9,10 +9,10 @@ import bump import gittyup import nimph/spec -import nimph/version +import nimph/versions -import nimph/group -export group +import nimph/groups +export groups type VersionTags* = Group[Version, GitThing] diff --git a/src/nimph/config.nim b/src/nimph/config.nim deleted file mode 100644 index 219adad..0000000 --- a/src/nimph/config.nim +++ /dev/null @@ -1,621 +0,0 @@ -import std/osproc -import std/json -import std/nre -import std/strtabs -import std/strformat -import std/tables -import std/os -import std/options -import std/strutils -import std/algorithm - -import compiler/ast -import compiler/idents -import compiler/nimconf -import compiler/options as compileropts -import compiler/pathutils -import compiler/condsyms -import compiler/lineinfos - -export compileropts -export nimconf - -import npeg -import bump - -import nimph/spec -import nimph/runner - -when defined(debugPath): - from std/sequtils import count - -type - ProjectCfgParsed* = object - table*: TableRef[string, string] - why*: string - ok*: bool - - ConfigSection = enum - LockerRooms = "lockfiles" - - NimphConfig* = ref object - path: string - js: JsonNode - -template excludeAllNotes(config: ConfigRef; n: typed) = - config.notes.excl n - when compiles(config.mainPackageNotes): - config.mainPackageNotes.excl n - when compiles(config.foreignPackageNotes): - config.foreignPackageNotes.excl n - -template setDefaultsForConfig(result: ConfigRef) = - # maybe we should turn off configuration hints for these reads - when defined(debugPath): - result.notes.incl hintPath - elif not defined(debug): - excludeAllNotes(result, hintConf) - excludeAllNotes(result, hintLineTooLong) - -proc parseConfigFile*(path: string): Option[ConfigRef] = - ## use the compiler to parse a nim.cfg without changing to its directory - var - cache = newIdentCache() - filename = path.absolutePath - config = newConfigRef() - - # define symbols such as, say, nimbabel; - # this allows us to correctly parse conditions in nim.cfg(s) - initDefines(config.symbols) - - setDefaultsForConfig(config) - - if readConfigFile(filename.AbsoluteFile, cache, config): - result = some(config) - -when false: - proc overlayConfig(config: var ConfigRef; - directory: string): bool {.deprecated.} = - ## true if new config data was added to the env - withinDirectory(directory): - var - priorProjectPath = config.projectPath - let - nextProjectPath = AbsoluteDir getCurrentDir() - filename = nextProjectPath.string / NimCfg - - block complete: - # do not overlay above the current config - if nextProjectPath == priorProjectPath: - break complete - - # if there's no config file, we're done - result = filename.fileExists - if not result: - break complete - - try: - # set the new project path for substitution purposes - config.projectPath = nextProjectPath - - var cache = newIdentCache() - result = readConfigFile(filename.AbsoluteFile, cache, config) - - if result: - # this config is now authoritative, so force the project path - priorProjectPath = nextProjectPath - else: - let emsg = &"unable to read config in {nextProjectPath}" # noqa - warn emsg - finally: - # remember to reset the config's project path - config.projectPath = priorProjectPath - -# a global that we set just once per invocation -var - compilerPrefixDir: AbsoluteDir - -proc findPrefixDir(): AbsoluteDir = - ## determine the prefix directory for the current compiler - if compilerPrefixDir.isEmpty: - debug "find prefix" - let - compiler = runSomething("nim", - @["--hints:off", - "--dump.format:json", "dump", "dummy"], {poDaemon}) - if not compiler.ok: - warn "couldn't run the compiler to determine its location" - raise newException(OSError, "cannot find a nim compiler") - try: - let - js = parseJson(compiler.output) - compilerPrefixDir = AbsoluteDir js["prefixdir"].getStr - except JsonParsingError as e: - warn "`nim dump` json parse error: " & e.msg - raise - except KeyError: - warn "couldn't parse the prefix directory from `nim dump` output" - compilerPrefixDir = AbsoluteDir parentDir(findExe"nim") - debug "found prefix" - result = compilerPrefixDir - -proc loadAllCfgs*(directory: string): ConfigRef = - ## use the compiler to parse all the usual nim.cfgs; - ## optionally change to the given (project?) directory first - - result = newConfigRef() - - # define symbols such as, say, nimbabel; - # this allows us to correctly parse conditions in nim.cfg(s) - initDefines(result.symbols) - - setDefaultsForConfig(result) - - # stuff the prefixDir so we load the compiler's config/nim.cfg - # just like the compiler would if we were to invoke it directly - result.prefixDir = findPrefixDir() - - withinDirectory(directory): - # stuff the current directory as the project path - result.projectPath = AbsoluteDir getCurrentDir() - - # now follow the compiler process of loading the configs - var cache = newIdentCache() - - # thanks, araq - when (NimMajor, NimMinor) >= (1, 5): - var idgen = IdGenerator(module: 0.int32, item: 0.int32) - loadConfigs(NimCfg.RelativeFile, cache, result, idgen) - else: - loadConfigs(NimCfg.RelativeFile, cache, result) - - when defined(debugPath): - debug "loaded", result.searchPaths.len, "search paths" - debug "loaded", result.lazyPaths.len, "lazy paths" - for path in result.lazyPaths.items: - debug "\t", path - for path in result.lazyPaths.items: - if result.lazyPaths.count(path) > 1: - raise newException(Defect, "duplicate lazy path: " & path.string) - -proc appendConfig*(path: Target; config: string): bool = - # make a temp file in an appropriate spot, with a significant name - let - temp = createTemporaryFile(path.package, dotNimble) - debug &"writing {temp}" - # but remember to remove the temp file later - defer: - debug &"removing {temp}" - if not tryRemoveFile(temp): - warn &"unable to remove temporary file `{temp}`" - - block complete: - try: - # if there's already a config, we'll start there - if fileExists($path): - debug &"copying {path} to {temp}" - copyFile($path, temp) - except Exception as e: - warn &"unable make a copy of {path} to to {temp}: {e.msg}" - break complete - - block writing: - # open our temp file for writing - var - writer = temp.open(fmAppend) - try: - # add our new content with a trailing newline - writer.writeLine "# added by nimph:\n" & config - finally: - # remember to close the temp file in any event - writer.close - - # make sure the compiler can parse our new config - if parseConfigFile(temp).isNone: - break complete - - # copy the temp file over the original config - try: - debug &"copying {temp} over {path}" - copyFile(temp, $path) - except Exception as e: - warn &"unable make a copy of {temp} to to {path}: {e.msg}" - break complete - - # it worked, thank $deity - result = true - -proc parseProjectCfg*(input: Target): ProjectCfgParsed = - ## parse a .cfg for any lines we are entitled to mess with - result = ProjectCfgParsed(ok: false, table: newTable[string, string]()) - var - table = result.table - - block success: - if not fileExists($input): - result.why = &"config file {input} doesn't exist" - break success - - var - content = readFile($input) - if not content.endsWith("\n"): - content &= "\n" - let - peggy = peg "document": - nl <- ?'\r' * '\n' - white <- {'\t', ' '} - equals <- *white * {'=', ':'} * *white - assignment <- +(1 - equals) - comment <- '#' * *(1 - nl) - strvalue <- '"' * *(1 - '"') * '"' - endofval <- white | comment | nl - anyvalue <- +(1 - endofval) - hyphens <- '-'[0..2] - ending <- *white * ?comment * nl - nimblekeys <- i"nimblePath" | i"clearNimblePath" | i"noNimblePath" - otherkeys <- i"path" | i"p" | i"define" | i"d" - keys <- nimblekeys | otherkeys - strsetting <- hyphens * >keys * equals * >strvalue * ending: - table.add $1, unescape($2) - anysetting <- hyphens * >keys * equals * >anyvalue * ending: - table.add $1, $2 - toggle <- hyphens * >keys * ending: - table.add $1, "it's enabled, okay?" - line <- strsetting | anysetting | toggle | (*(1 - nl) * nl) - document <- *line * !1 - parsed = peggy.match(content) - try: - result.ok = parsed.ok - if result.ok: - break success - result.why = parsed.repr - except Exception as e: - result.why = &"parse error in {input}: {e.msg}" - -proc isEmpty*(config: NimphConfig): bool = - result = config.js.kind == JNull - -proc newNimphConfig*(path: string): NimphConfig = - ## instantiate a new nimph config using the given path - result = NimphConfig(path: path.absolutePath) - if not result.path.fileExists: - result.js = newJNull() - else: - try: - result.js = parseFile(path) - except Exception as e: - error &"unable to parse {path}:" - error e.msg - -template isStdLib*(config: ConfigRef; path: string): bool = - path.startsWith(///config.libpath) - -template isStdlib*(config: ConfigRef; path: AbsoluteDir): bool = - path.string.isStdLib - -iterator likelySearch*(config: ConfigRef; libsToo: bool): string = - ## yield /-terminated directory paths likely added via --path - for search in config.searchPaths.items: - let - search = ///search - # we don't care about library paths - if not libsToo and config.isStdLib(search): - continue - yield search - -iterator likelySearch*(config: ConfigRef; repo: string; libsToo: bool): string = - ## yield /-terminated directory paths likely added via --path - when defined(debug): - if repo != repo.absolutePath: - error &"repo {repo} wasn't normalized" - - for search in config.likelySearch(libsToo = libsToo): - # limit ourselves to the repo? - when WhatHappensInVegas: - if search.startsWith(repo): - yield search - else: - yield search - -iterator likelyLazy*(config: ConfigRef; least = 0): string = - ## yield /-terminated directory paths likely added via --nimblePath - # build a table of sightings of directories - var popular = newCountTable[string]() - for search in config.lazyPaths.items: - let - search = ///search - parent = ///parentDir(search) - when defined(debugPath): - if search in popular: - raise newException(Defect, "duplicate lazy path: " & search) - if search notin popular: - popular.inc search - if search != parent: # silly: elide / - if parent in popular: # the parent has to have been added - popular.inc parent - - # sort the table in descending order - popular.sort - - # yield the directories that exist - for search, count in popular.pairs: - # maybe we can ignore unpopular paths - if least > count: - continue - yield search - -iterator likelyLazy*(config: ConfigRef; repo: string; least = 0): string = - ## yield /-terminated directory paths likely added via --nimblePath - when defined(debug): - if repo != repo.absolutePath: - error &"repo {repo} wasn't normalized" - - for search in config.likelyLazy(least = least): - # limit ourselves to the repo? - when WhatHappensInVegas: - if search.startsWith(repo): - yield search - else: - yield search - -iterator packagePaths*(config: ConfigRef; exists = true): string = - ## yield package paths from the configuration as /-terminated strings; - ## if the exists flag is passed, then the path must also exist. - ## this should closely mimic the compiler's search - - # the method by which we de-dupe paths - const mode = - when FilesystemCaseSensitive: - modeCaseSensitive - else: - modeCaseInsensitive - var - paths: seq[string] - dedupe = newStringTable(mode) - - template addOne(p: AbsoluteDir) = - let - path = ///path - if path in dedupe: - continue - dedupe[path] = "" - paths.add path - - if config == nil: - raise newException(Defect, "attempt to load search paths from nil config") - - for path in config.searchPaths: - addOne(path) - for path in config.lazyPaths: - addOne(path) - when defined(debugPath): - debug &"package directory count: {paths.len}" - - # finally, emit paths as appropriate - for path in paths: - if exists and not path.dirExists: - continue - yield path - -proc suggestNimbleDir*(config: ConfigRef; local = ""; global = ""): string = - ## come up with a useful nimbleDir based upon what we find in the - ## current configuration, the location of the project, and the provided - ## suggestions for local or global package directories - var - local = local - global = global - - block either: - # if a local directory is suggested, see if we can confirm its use - if local != "" and local.dirExists: - local = ///local - assert local.endsWith(DirSep) - for search in config.likelySearch(libsToo = false): - if search.startsWith(local): - # we've got a path statement pointing to a local path, - # so let's assume that the suggested local path is legit - result = local - break either - - # nim 1.1.1 supports nimblePath storage in the config; - # we follow a "standard" that we expect Nimble to use, - # too, wherein the last-added --nimblePath wins - when NimMajor >= 1 and NimMinor >= 1: - if config.nimblePaths.len > 0: - result = config.nimblePaths[0].string - break either - - # otherwise, try to pick a global .nimble directory based upon lazy paths - for search in config.likelyLazy: - if search.endsWith(PkgDir & DirSep): - result = search.parentDir # ie. the parent of pkgs - else: - result = search # doesn't look like pkgs... just use it - break either - - # otherwise, try to make one up using the suggestion - if global == "": - raise newException(IOError, "can't guess global {dotNimble} directory") - global = ///global - assert global.endsWith(DirSep) - result = global - break either - -iterator pathSubsFor(config: ConfigRef; sub: string; conf: string): string = - ## a convenience to work around the compiler's broken pathSubs; the `conf` - ## string represents the path to the "current" configuration file - block: - if sub.toLowerAscii notin ["nimbledir", "nimblepath"]: - yield ///config.pathSubs(&"${sub}", conf) - break - - when declaredInScope nimbleSubs: - for path in config.nimbleSubs(&"${sub}"): - yield ///path - else: - # we have to pick the first lazy path because that's what Nimble does - for search in config.lazyPaths: - let - search = ///search - if search.endsWith(PkgDir & DirSep): - yield ///parentDir(search) - else: - yield search - break - -iterator pathSubstitutions(config: ConfigRef; path: string; - conf: string; write: bool): string = - ## compute the possible path substitions, including the original path - const - readSubs = @["nimcache", "config", "nimbledir", "nimblepath", - "projectdir", "projectpath", "lib", "nim", "home"] - writeSubs = - when writeNimbleDirPaths: - readSubs - else: - @["nimcache", "config", "projectdir", "lib", "nim", "home"] - var - matchedPath = false - when defined(debug): - if not conf.dirExists: - raise newException(Defect, "passed a config file and not its path") - let - path = ///path - conf = if conf.dirExists: conf else: conf.parentDir - substitutions = if write: writeSubs else: readSubs - - for sub in substitutions.items: - for attempt in config.pathSubsFor(sub, conf): - # ignore any empty substitutions - if attempt == "/": - continue - # note if any substitution matches the path - if path == attempt: - matchedPath = true - if path.startsWith(attempt): - yield path.replace(attempt, ///fmt"${sub}") - # if a substitution matches the path, don't yield it at the end - if not matchedPath: - yield path - -proc bestPathSubstitution(config: ConfigRef; path: string; conf: string): string = - ## compute the best path substitution, if any - block found: - for sub in config.pathSubstitutions(path, conf, write = true): - result = sub - break found - result = path - -proc removeSearchPath*(config: ConfigRef; nimcfg: Target; path: string): bool = - ## try to remove a path from a nim.cfg; true if it was - ## successful and false if any error prevented success - let - fn = $nimcfg - - block complete: - # well, that was easy - if not fn.fileExists: - break complete - - # make sure we can parse the configuration with the compiler - if parseConfigFile(fn).isNone: - error &"the compiler couldn't parse {nimcfg}" - break complete - - # make sure we can parse the configuration using our "naive" npeg parser - let - parsed = nimcfg.parseProjectCfg - if not parsed.ok: - error &"could not parse {nimcfg} naïvely:" - error parsed.why - break complete - - # sanity - when defined(debug): - if path.absolutePath != path: - raise newException(Defect, &"path `{path}` is not absolute") - - var - content = fn.readFile - # iterate over the entries we parsed naively, - for key, value in parsed.table.pairs: - # skipping anything that it's a path, - if key.toLowerAscii notin ["p", "path", "nimblepath"]: - continue - # and perform substitutions to see if one might match the value - # we are trying to remove; the write flag is false so that we'll - # use any $nimbleDir substitutions available to us, if possible - for sub in config.pathSubstitutions(path, nimcfg.repo, write = false): - if sub notin [value, ///value]: - continue - # perform a regexp substition to remove the entry from the content - let - regexp = re("(*ANYCRLF)(?i)(?s)(-{0,2}" & key.escapeRe & - "[:=]\"?" & value.escapeRe & "/?\"?)\\s*") - swapped = content.replace(regexp, "") - # if that didn't work, cry a bit and move on - if swapped == content: - notice &"failed regex edit to remove path `{value}`" - continue - # make sure we search the new content next time through the loop - content = swapped - result = true - # keep performing more substitutions - - # finally, write the edited content - fn.writeFile(content) - -proc addSearchPath*(config: ConfigRef; nimcfg: Target; path: string): bool = - ## add the given path to the given config file, using the compiler's - ## configuration as input to determine the best path substitution - let - best = config.bestPathSubstitution(path, $nimcfg.repo) - result = appendConfig(nimcfg, &"""--path="{best}"""") - -proc excludeSearchPath*(config: ConfigRef; nimcfg: Target; path: string): bool = - ## add an exclusion for the given path to the given config file, using the - ## compiler's configuration as input to determine the best path substitution - let - best = config.bestPathSubstitution(path, $nimcfg.repo) - result = appendConfig(nimcfg, &"""--excludePath="{best}"""") - -iterator extantSearchPaths*(config: ConfigRef; least = 0): string = - ## yield existing search paths from the configuration as /-terminated strings; - ## this will yield library paths and nimblePaths with at least `least` uses - if config == nil: - raise newException(Defect, "attempt to load search paths from nil config") - # path statements - for path in config.likelySearch(libsToo = true): - if dirExists(path): - yield path - # nimblePath statements - for path in config.likelyLazy(least = least): - if dirExists(path): - yield path - -proc addLockerRoom*(config: var NimphConfig; name: string; room: JsonNode) = - ## add the named lockfile (in json form) to the configuration file - if config.isEmpty: - config.js = newJObject() - if $LockerRooms notin config.js: - config.js[$LockerRooms] = newJObject() - config.js[$LockerRooms][name] = room - writeFile(config.path, config.js.pretty) - -proc getAllLockerRooms*(config: NimphConfig): JsonNode = - ## retrieve a JObject holding all lockfiles in the configuration file - block found: - if not config.isEmpty: - if $LockerRooms in config.js: - result = config.js[$LockerRooms] - break - result = newJObject() - -proc getLockerRoom*(config: NimphConfig; name: string): JsonNode = - ## retrieve the named lockfile (or JNull) from the configuration - let - rooms = config.getAllLockerRooms - if name in rooms: - result = rooms[name] - else: - result = newJNull() diff --git a/src/nimph/group.nim b/src/nimph/group.nim deleted file mode 100644 index d116b1d..0000000 --- a/src/nimph/group.nim +++ /dev/null @@ -1,186 +0,0 @@ -import std/os -import std/strtabs -import std/tables -from std/sequtils import toSeq -import std/uri except Url - -export strtabs.StringTableMode - -import nimph/spec - -type - Group*[K; V: ref object] = ref object of RootObj - table*: OrderedTableRef[K, V] - imports*: StringTableRef - flags*: set[Flag] - mode: StringTableMode - -proc init*[K, V](group: Group[K, V]; flags: set[Flag]; mode = modeStyleInsensitive) = - ## initialize the table and name cache - group.table = newOrderedTable[K, V]() - when K is Uri: - group.mode = modeCaseSensitive - else: - group.mode = mode - group.imports = newStringTable(group.mode) - group.flags = flags - -proc addName[K: string, V](group: Group[K, V]; name: K; value: string) = - ## add a name to the group, which points to value - assert group.table.hasKey(value) - group.imports[name] = value - -proc addName[K: Uri, V](group: Group[K, V]; url: K) = - ## add a url to the group, which points to value - assert group.table.hasKey(url) - group.imports[$url] = $url - when defined(debug): - assert $url.bare notin group.imports - group.imports[$url.bare] = $url - -proc delName*(group: Group; key: string) = - ## remove a name from the group - var - remove: seq[string] - # don't trust anyone; if the value matches, pull the name - for name, value in group.imports.pairs: - if value == key: - remove.add name - for name in remove: - group.imports.del name - -proc del*[K: string, V](group: Group[K, V]; name: K) = - ## remove from the group the named key and its associated value - group.table.del name - group.delName name - -proc del*[K: Uri, V](group: Group[K, V]; url: K) = - ## remove from the group the url key and its associated value - group.table.del url - group.delName $url - -{.warning: "nim bug #12818".} -proc len*[K, V](group: Group[K, V]): int = - ## number of elements in the group - result = group.table.len - -proc len*(group: Group): int = - ## number of elements in the group - result = group.table.len - -proc get*[K: string, V](group: Group[K, V]; key: K): V = - ## fetch a value from the group using style-insensitive lookup - if group.table.hasKey(key): - result = group.table[key] - elif group.imports.hasKey(key.importName): - result = group.table[group.imports[key.importName]] - else: - let emsg = &"{key.importName} not found" - raise newException(KeyError, emsg) - -proc mget*[K: string, V](group: var Group[K, V]; key: K): var V = - ## fetch a value from the group using style-insensitive lookup - if group.table.hasKey(key): - result = group.table[key] - elif group.imports.hasKey(key.importName): - result = group.table[group.imports[key.importName]] - else: - let emsg = &"{key.importName} not found" - raise newException(KeyError, emsg) - -proc `[]`*[K, V](group: var Group[K, V]; key: K): var V = - ## fetch a value from the group using style-insensitive lookup - result = group.mget(key) - -proc `[]`*[K, V](group: Group[K, V]; key: K): V = - ## fetch a value from the group using style-insensitive lookup - result = group.get(key) - -proc add*[K: string, V](group: Group[K, V]; key: K; value: V) = - ## add a key and value to the group - group.table.add key, value - group.addName(key.importName, key) - -proc add*[K: string, V](group: Group[K, V]; url: Uri; value: V) = - ## add a (bare) url as a key - let - naked = url.bare - key = $naked - group.table.add key, value - # this gets picked up during instant-instantiation of a package from - # a project's url, a la asPackage(project: Project): Package ... - group.addName naked.importName, key - -proc `[]=`*[K, V](group: Group[K, V]; key: K; value: V) = - ## set a key to a single value - if group.hasKey(key): - group.del key - group.add key, value - -{.warning: "nim bug #12818".} -proc add*[K: Uri, V](group: Group[K, V]; url: Uri; value: V) = - ## add a (full) url as a key - group.table.add url, value - group.addName url - -iterator pairs*[K, V](group: Group[K, V]): tuple[key: K; val: V] = - ## standard key/value pairs iterator - for key, value in group.table.pairs: - yield (key: key, val: value) - -{.warning: "nim bug #13510".} -#iterator mpairs*[K, V](group: var Group[K, V]): tuple[key: K; val: var V] = -iterator mpairs*[K, V](group: Group[K, V]): tuple[key: K; val: var V] = - for key, value in group.table.mpairs: - #yield (key: key, val: value) - yield (key, value) - -iterator values*[K, V](group: Group[K, V]): V = - ## standard value iterator - for value in group.table.values: - yield value - -iterator keys*[K, V](group: Group[K, V]): K = - ## standard key iterator - for key in group.table.keys: - yield key - -iterator mvalues*[K, V](group: var Group[K, V]): var V = - ## standard mutable value iterator - for value in group.table.mvalues: - yield value - -proc hasKey*[K, V](group: Group[K, V]; key: K): bool = - ## true if the group contains the given key - result = group.table.hasKey(key) - -proc contains*[K, V](group: Group[K, V]; key: K): bool = - ## true if the group contains the given key or its importName - result = group.table.contains(key) or group.imports.contains(key.importName) - -proc contains*[K, V](group: Group[K, V]; url: Uri): bool = - ## true if a member of the group has the same (bare) url - for value in group.values: - if bareUrlsAreEqual(value.url, url): - result = true - break - -proc contains*[K, V](group: Group[K, V]; value: V): bool = - ## true if the group contains the given value - for v in group.values: - if v == value: - result = true - break - -iterator reversed*[K, V](group: Group[K, V]): V = - ## yield values in reverse order of entry - let - elems = toSeq group.values - - for index in countDown(elems.high, elems.low): - yield elems[index] - -proc clear*[K, V](group: Group[K, V]) = - ## clear the group without any other disruption - group.table.clear - group.imports.clear(group.mode) diff --git a/src/nimph/nimble.nim b/src/nimph/nimble.nim deleted file mode 100644 index 3b5b963..0000000 --- a/src/nimph/nimble.nim +++ /dev/null @@ -1,127 +0,0 @@ -import std/uri -import std/json -import std/options -import std/strtabs -import std/strutils -import std/os -import std/osproc -import std/strformat - -import npeg - -import nimph/spec -import nimph/runner - -type - DumpResult* = object - table*: StringTableRef - why*: string - ok*: bool - - NimbleMeta* = ref object - js: JsonNode - link: seq[string] - -proc parseNimbleDump*(input: string): Option[StringTableRef] = - ## parse output from `nimble dump` - var - table = newStringTable(modeStyleInsensitive) - let - peggy = peg "document": - nl <- ?'\r' * '\n' - white <- {'\t', ' '} - key <- +(1 - ':') - value <- '"' * *(1 - '"') * '"' - errline <- white * >*(1 - nl) * +nl: - warn $1 - line <- >key * ':' * +white * >value * +nl: - table[$1] = unescape($2) - anyline <- line | errline - document <- +anyline * !1 - parsed = peggy.match(input) - if parsed.ok: - result = table.some - -proc fetchNimbleDump*(path: string; nimbleDir = ""): DumpResult = - ## parse nimble dump output into a string table - result = DumpResult(ok: false) - block fetched: - withinDirectory(path): - let - nimble = runSomething("nimble", - @["dump", path], {poDaemon}, nimbleDir = nimbleDir) - if not nimble.ok: - result.why = "nimble execution failed" - if nimble.output.len > 0: - error nimble.output - break fetched - - let - parsed = parseNimbleDump(nimble.output) - if parsed.isNone: - result.why = &"unable to parse `nimble dump` output" - break fetched - result.table = parsed.get - result.ok = true - -proc hasUrl*(meta: NimbleMeta): bool = - ## true if the metadata includes a url - result = "url" in meta.js - result = result and meta.js["url"].kind == JString - result = result and meta.js["url"].getStr != "" - -proc url*(meta: NimbleMeta): Uri = - ## return the url associated with the package - if not meta.hasUrl: - raise newException(ValueError, "url not available") - result = parseUri(meta.js["url"].getStr) - if result.anchor == "": - if "vcsRevision" in meta.js: - result.anchor = meta.js["vcsRevision"].getStr - removePrefix(result.anchor, {'#'}) - -proc writeNimbleMeta*(path: string; url: Uri; revision: string): bool = - ## try to write a new nimblemeta.json - block complete: - if not dirExists(path): - warn &"{path} is not a directory; cannot write {nimbleMeta}" - break complete - var - revision = revision - removePrefix(revision, {'#'}) - var - js = %* { - "url": $url, - "vcsRevision": revision, - "files": @[], - "binaries": @[], - "isLink": false, - } - writer = open(path / nimbleMeta, fmWrite) - defer: - writer.close - writer.write($js) - result = true - -proc isLink*(meta: NimbleMeta): bool = - ## true if the metadata says it's a link - if meta.js.kind == JObject: - result = meta.js.getOrDefault("isLink").getBool - -proc isValid*(meta: NimbleMeta): bool = - ## true if the metadata appears to hold some data - result = meta.js != nil and meta.js.len > 0 - -proc fetchNimbleMeta*(path: string): NimbleMeta = - ## parse the nimblemeta.json file if it exists - result = NimbleMeta(js: newJObject()) - let - metafn = path / nimbleMeta - try: - if metafn.fileExists: - let - content = readFile(metafn) - result.js = parseJson(content) - except Exception as e: - discard e # noqa - warn &"error while trying to parse {nimbleMeta}: {e.msg}" diff --git a/src/nimph/runner.nim b/src/nimph/runner.nim deleted file mode 100644 index f284447..0000000 --- a/src/nimph/runner.nim +++ /dev/null @@ -1,80 +0,0 @@ -import std/strutils -import std/strformat -import std/logging -import std/os -import std/sequtils -import std/osproc - -import nimph/spec - -type - RunOutput* = object - arguments*: seq[string] - output*: string - ok*: bool - -proc stripPkgs*(nimbleDir: string): string = - ## omit and trailing /PkgDir from a path - result = ///nimbleDir - # the only way this is a problem is if the user stores deps in pkgs/pkgs, - # but we can remove this hack once we have nimblePaths in nim-1.0 ... - if result.endsWith(//////PkgDir): - result = ///parentDir(result) - -proc runSomething*(exe: string; args: seq[string]; options: set[ProcessOption]; - nimbleDir = ""): RunOutput = - ## run a program with arguments, perhaps with a particular nimbleDir - var - command = findExe(exe) - arguments = args - opts = options - block ran: - if command == "": - result = RunOutput(output: &"unable to find {exe} in path") - warn result.output - break ran - - if exe == "nimble": - when defined(debug): - arguments = @["--verbose"].concat arguments - when defined(debugNimble): - arguments = @["--debug"].concat arguments - - if nimbleDir != "": - # we want to strip any trailing PkgDir arriving from elsewhere... - var nimbleDir = nimbleDir.stripPkgs - if not nimbleDir.dirExists: - let emsg = &"{nimbleDir} is missing; can't run {exe}" # noqa - raise newException(IOError, emsg) - # the ol' belt-and-suspenders approach to specifying nimbleDir - if exe == "nimble": - arguments = @["--nimbleDir=" & nimbleDir].concat arguments - putEnv("NIMBLE_DIR", nimbleDir) - - if poParentStreams in opts or poInteractive in opts: - # sorry; i just find this easier to read than union() - opts.incl poInteractive - opts.incl poParentStreams - # the user wants interactivity - when defined(debug): - debug command, arguments.join(" ") - let - process = startProcess(command, args = arguments, options = opts) - result = RunOutput(ok: process.waitForExit == 0) - else: - # the user wants to capture output - command &= " " & quoteShellCommand(arguments) - when defined(debug): - debug command - let - (output, code) = execCmdEx(command, opts) - result = RunOutput(output: output, ok: code == 0) - - # for utility, also return the arguments we used - result.arguments = arguments - - # a failure is worth noticing - if not result.ok: - notice exe & " " & arguments.join(" ") - when defined(debug): - debug "done running" diff --git a/src/nimph/sanitize.nim b/src/nimph/sanitize.nim deleted file mode 100644 index 57a1348..0000000 --- a/src/nimph/sanitize.nim +++ /dev/null @@ -1,81 +0,0 @@ -import std/macros -import std/options -import std/strutils - -const - elideUnderscoresInIdentifiers {.booldefine.} = false - -when nimvm: - discard -else: - import cutelog - -proc isValidNimIdentifier*(s: string): bool = - ## true for strings that are valid identifier names - block complete: - if s.len > 0 and s[0] in IdentStartChars: - if s.len > 1 and '_' in [s[0], s[^1]]: - break complete - for i in 1..s.len-1: - if s[i] notin IdentChars: - break complete - if s[i] == '_' and s[i-1] == '_': - break complete - result = true - -template cappableAdd(s: var string; c: char) = - ## add a char to a string, perhaps capitalizing it - if s.len > 0 and s[^1] == '_': - s.add c.toUpperAscii() - else: - s.add c - -proc sanitizeIdentifier*(name: string; capsOkay=false): Option[string] = - ## convert any string to a valid nim identifier in camel_Case - var id = "" - block sanitized: - if name.len == 0: - break sanitized - for c in name: - if id.len == 0: - if c in IdentStartChars: - id.cappableAdd c - continue - elif c in IdentChars: - id.cappableAdd c - continue - # help differentiate words case-insensitively - id.add '_' - when not elideUnderscoresInIdentifiers: - while "__" in id: - id = id.replace("__", "_") - if id.len > 1: - id.removeSuffix {'_'} - id.removePrefix {'_'} - # if we need to lowercase the first letter, we'll lowercase - # until we hit a word boundary (_, digit, or lowercase char) - if not capsOkay and id[0].isUpperAscii: - for i in id.low..id.high: - if id[i] in ['_', id[i].toLowerAscii]: - break - id[i] = id[i].toLowerAscii - # ensure we're not, for example, starting with a digit - if id[0] notin IdentStartChars: - when nimvm: - warning "identifiers cannot start with `" & id[0] & "`" - else: - discard - # warn "identifiers cannot start with `" & id[0] & "`" - break sanitized - when elideUnderscoresInIdentifiers: - if id.len > 1: - while "_" in id: - id = id.replace("_", "") - if not id.isValidNimIdentifier: - when nimvm: - warning "bad identifier: " & id - else: - discard - # warn "bad identifier: " & id - break sanitized - result = id.some diff --git a/src/nimph/spec.nim b/src/nimph/spec.nim deleted file mode 100644 index 7227f41..0000000 --- a/src/nimph/spec.nim +++ /dev/null @@ -1,240 +0,0 @@ -import std/strformat -import std/options -import std/strutils -import std/hashes -import std/uri -import std/os -import std/times - -import compiler/pathutils - -import cutelog -export cutelog - -import nimph/sanitize - -# slash attack /////////////////////////////////////////////////// -when NimMajor >= 1 and NimMinor >= 1: - template `///`*(a: string): string = - # ensure a trailing DirSep - joinPath(a, $DirSep, "") - template `///`*(a: AbsoluteFile | AbsoluteDir): string = - # ensure a trailing DirSep - `///`(a.string) - template `//////`*(a: string | AbsoluteFile | AbsoluteDir): string = - # ensure a trailing DirSep and a leading DirSep - joinPath($DirSep, "", `///`(a), $DirSep, "") -else: - template `///`*(a: string): string = - # ensure a trailing DirSep - joinPath(a, "") - template `///`*(a: AbsoluteFile | AbsoluteDir): string = - # ensure a trailing DirSep - `///`(a.string) - template `//////`*(a: string | AbsoluteFile | AbsoluteDir): string = - # ensure a trailing DirSep and a leading DirSep - "" / "" / `///`(a) / "" - -type - Flag* {.pure.} = enum - Quiet - Strict - Force - Dry - Safe - Network - - RollGoal* = enum - Upgrade = "upgrade" - Downgrade = "downgrade" - Specific = "roll" - - ForkTargetResult* = object - ok*: bool - why*: string - owner*: string - repo*: string - url*: Uri - -const - dotNimble* {.strdefine.} = "".addFileExt("nimble") - dotNimbleLink* {.strdefine.} = "".addFileExt("nimble-link") - dotGit* {.strdefine.} = "".addFileExt("git") - dotHg* {.strdefine.} = "".addFileExt("hg") - DepDir* {.strdefine.} = //////"deps" - PkgDir* {.strdefine.} = //////"pkgs" - NimCfg* {.strdefine.} = "nim".addFileExt("cfg") - ghTokenFn* {.strdefine.} = "github_api_token" - ghTokenEnv* {.strdefine.} = "NIMPH_TOKEN" - hubTokenFn* {.strdefine.} = "".addFileExt("config") / "hub" - stalePackages* {.intdefine.} = 14 - configFile* {.strdefine.} = "nimph".addFileExt("json") - nimbleMeta* {.strdefine.} = "nimblemeta".addFileExt("json") - officialPackages* {.strdefine.} = "packages_official".addFileExt("json") - emptyRelease* {.strdefine.} = "#head" - defaultRemote* {.strdefine.} = "origin" - upstreamRemote* {.strdefine.} = "upstream" - excludeMissingSearchPaths* {.booldefine.} = false - excludeMissingLazyPaths* {.booldefine.} = true - writeNimbleDirPaths* {.booldefine.} = false - shortDate* = initTimeFormat "yyyy-MM-dd" - # add Safe to defaultFlags to, uh, default to Safe mode - defaultFlags*: set[Flag] = {Quiet, Strict} - - # when true, try to clamp analysis to project-local directories - WhatHappensInVegas* = false - -template withinDirectory*(path: string; body: untyped): untyped = - if not path.dirExists: - raise newException(ValueError, path & " is not a directory") - let cwd = getCurrentDir() - setCurrentDir(path) - defer: - setCurrentDir(cwd) - body - -template isValid*(url: Uri): bool = url.scheme.len != 0 - -proc hash*(url: Uri): Hash = - ## help hash URLs - var h: Hash = 0 - for field in url.fields: - when field is string: - h = h !& field.hash - elif field is bool: - h = h !& field.hash - result = !$h - -proc bare*(url: Uri): Uri = - result = url - result.anchor = "" - -proc bareUrlsAreEqual*(a, b: Uri): bool = - ## compare two urls without regard to their anchors - if a.isValid and b.isValid: - var - x = a.bare - y = b.bare - result = $x == $y - -proc pathToImport*(path: string): string = - ## calculate how a path will be imported by the compiler - assert path.len > 0 - result = path.lastPathPart.split("-")[0] - assert result.len > 0 - -proc normalizeUrl*(uri: Uri): Uri = - result = uri - if result.scheme == "" and result.path.contains("@"): - let - usersep = result.path.find("@") - pathsep = result.path.find(":") - result.path = uri.path[pathsep+1 .. ^1] - result.username = uri.path[0 ..< usersep] - result.hostname = uri.path[usersep+1 ..< pathsep] - result.scheme = "ssh" - else: - if result.scheme.startsWith("http"): - result.scheme = "git" - -proc convertToGit*(uri: Uri): Uri = - result = uri.normalizeUrl - if result.scheme == "" or result.scheme == "ssh": - result.scheme = "git" - if result.scheme == "git" and not result.path.endsWith(".git"): - result.path &= ".git" - result.username = "" - -proc convertToSsh*(uri: Uri): Uri = - result = uri.convertToGit - if not result.path[0].isAlphaNumeric: - result.path = result.path[1..^1] - if result.username == "": - result.username = "git" - result.path = result.username & "@" & result.hostname & ":" & result.path - result.username = "" - result.hostname = "" - result.scheme = "" - -proc packageName*(name: string): string = - ## return a string that is plausible as a package name - when true: - result = name - else: - const capsOkay = - when FilesystemCaseSensitive: - true - else: - false - let - sane = name.sanitizeIdentifier(capsOkay = capsOkay) - if sane.isSome: - result = sane.get - else: - raise newException(ValueError, "unable to sanitize `" & name & "`") - -proc packageName*(url: Uri): string = - ## guess the name of a package from a url - when defined(debug) or defined(debugPath): - assert url.isValid - var - # ensure the path doesn't end in a slash - path = url.path - removeSuffix(path, {'/'}) - result = packageName(path.extractFilename.changeFileExt("")) - -proc importName*(path: string): string = - ## a uniform name usable in code for imports - assert path.len > 0 - # strip any leading directories and extensions - result = splitFile(path).name - const capsOkay = - when FilesystemCaseSensitive: - true - else: - false - let - sane = path.sanitizeIdentifier(capsOkay = capsOkay) - # if it's a sane identifier, use it - if sane.isSome: - result = sane.get - elif not capsOkay: - # emit a lowercase name on case-insensitive filesystems - result = path.toLowerAscii - # else, we're just emitting the existing file's basename - -proc importName*(url: Uri): string = - let url = url.normalizeUrl - if not url.isValid: - raise newException(ValueError, "invalid url: " & $url) - elif url.scheme == "file": - result = url.path.importName - else: - result = url.packageName.importName - -proc forkTarget*(url: Uri): ForkTargetResult = - result.url = url.normalizeUrl - block success: - if not result.url.isValid: - result.why = &"url is invalid" - break - if result.url.hostname.toLowerAscii != "github.com": - result.why = &"url {result.url} does not point to github" - break - if result.url.path.len < 1: - result.why = &"unable to parse url {result.url}" - break - # split /foo/bar into (bar, foo) - let start = if result.url.path.startsWith("/"): 1 else: 0 - (result.owner, result.repo) = result.url.path[start..^1].splitPath - # strip .git - if result.repo.endsWith(".git"): - result.repo = result.repo[0..^len("git+2")] - result.ok = result.owner.len > 0 and result.repo.len > 0 - if not result.ok: - result.why = &"unable to parse url {result.url}" - -{.warning: "replace this with compiler code".} -proc destylize*(s: string): string = - ## this is how we create a uniformly comparable token - result = s.toLowerAscii.replace("_")