From 07a016863d4e09a08b60b2a8fba5ecf25e5820a0 Mon Sep 17 00:00:00 2001 From: ee7 <45465154+ee7@users.noreply.github.com> Date: Fri, 20 May 2022 14:14:44 +0200 Subject: [PATCH] info: use prob-specs cache (#605) This stops baking the prob-specs data into the binary, and makes `configlet info` use the cached prob-specs directory as `configlet sync` does. Advantages: - The prob-specs data is updated at run time - we no longer need commits like b54fe48f7290 or 146d41c184b5, and can remove `prob_specs_exercises.json` - We reduce the size of the configlet executable by about 12 KiB (2.5%) - Fewer lines of code: we can remove `bin/write_probspecs_info.nim` Disadvantages: - The user must now pass `-o` or `---offline` when running `configlet info` with no/limited network connectivity - `configlet info` is a little slower, even when using `-o`, because we run a few `git` commands to validate the cache repo Closes: #483 --- README.md | 3 + bin/write_probspecs_info.nim | 79 --------------- src/cli.nim | 25 +++-- src/info/info.nim | 33 ++++--- src/info/prob_specs_exercises.json | 149 ----------------------------- src/sync/probspecs.nim | 8 +- 6 files changed, 45 insertions(+), 252 deletions(-) delete mode 100755 bin/write_probspecs_info.nim delete mode 100644 src/info/prob_specs_exercises.json diff --git a/README.md b/README.md index f12e135c..53ac30e6 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,9 @@ Options for fmt: -u, --update Prompt to write formatted files -y, --yes Auto-confirm the prompt from --update +Options for info: + -o, --offline Do not update the cached 'problem-specifications' data + Options for sync: -e, --exercise Only operate on this exercise -o, --offline Do not update the cached 'problem-specifications' data diff --git a/bin/write_probspecs_info.nim b/bin/write_probspecs_info.nim deleted file mode 100755 index b813a8af..00000000 --- a/bin/write_probspecs_info.nim +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env -S nim r --verbosity:0 --skipParentCfg:on -import std/[algorithm, json, os, osproc, strformat, strutils, times] -import pkg/jsony - -# Silence `styleCheck` hints for underscores. -{.push hint[Name]: off.} - -type - ProbSpecsExercises = object - with_canonical_data: seq[string] - without_canonical_data: seq[string] - deprecated: seq[string] - - ProbSpecsState = object - last_updated: string - problem_specifications_commit_ref: string - exercises: ProbSpecsExercises - -{.pop.} - -proc execAndCheck(cmd: string): string = - var exitCode = -1 - (result, exitCode) = execCmdEx(cmd) - if exitCode == 0: - result.stripLineEnd() - else: - stderr.writeLine(result) - stderr.writeLine &"Command exited non-zero: {cmd}" - quit 1 - -proc getCommitTimestamp(probSpecsDir: string): string = - let cmd = &"git -C {probSpecsDir} log -n1 --pretty=%ct" - execAndCheck(cmd).parseInt().fromUnix().utc().`$` - -proc getCommitRef(probSpecsDir: string): string = - execAndCheck(&"git -C {probSpecsDir} rev-parse HEAD") - -proc getExercises(probSpecsDir: string): ProbSpecsExercises = - result = ProbSpecsExercises() - for kind, path in walkDir(probSpecsDir / "exercises"): - if kind == pcDir: - let track = path.lastPathPart() - if fileExists(path / ".deprecated"): - result.deprecated.add track - elif fileExists(path / "canonical-data.json"): - result.with_canonical_data.add track - else: - result.without_canonical_data.add track - sort result.with_canonical_data - sort result.without_canonical_data - sort result.deprecated - -proc init(T: typedesc[ProbSpecsState], probSpecsDir: string): T = - T( - last_updated: getCommitTimestamp(probSpecsDir), - problem_specifications_commit_ref: getCommitRef(probSpecsDir), - exercises: getExercises(probSpecsDir) - ) - -proc main = - const repoRootDir = currentSourcePath().parentDir().parentDir() - const probSpecsDir = getCacheDir() / "exercism" / "configlet" / "problem-specifications" - if dirExists(probSpecsDir): - let jsonContents = ProbSpecsState.init(probSpecsDir).toJson().parseJson().pretty() - const jsonOutputPath = repoRootDir / "src" / "info" / "prob_specs_exercises.json" - writeFile(jsonOutputPath, jsonContents & "\n") - echo &"Wrote updated data to '{jsonOutputPath}'" - else: - let msg = fmt""" - Missing problem-specifications directory at this location: - {probSpecsDir} - - Please run `configlet sync` to set up the problem-specifications cache. - """.unindent() - stderr.writeLine msg - quit 1 - -when isMainModule: - main() diff --git a/src/cli.nim b/src/cli.nim index a2480aa6..54c37623 100644 --- a/src/cli.nim +++ b/src/cli.nim @@ -24,7 +24,7 @@ type Action* = object case kind*: ActionKind - of actNil, actGenerate, actInfo, actLint: + of actNil, actGenerate, actLint: discard of actFmt: # We can't name these fields `exercise`, `update`, and `yes` because we @@ -33,6 +33,8 @@ type exerciseFmt*: string updateFmt*: bool yesFmt*: bool + of actInfo: + offlineInfo*: bool of actSync: exercise*: string offline*: bool @@ -65,8 +67,9 @@ type optFmtSyncUpdate = "update" optFmtSyncYes = "yes" - # Options for `sync` - optSyncOffline = "offline" + # Options for both `info` and `sync` + optInfoSyncOffline = "offline" + # Scope to sync optSyncDocs = "docs" optSyncFilepaths = "filepaths" @@ -90,7 +93,7 @@ const configletVersion = staticRead(repoRootDir / "configlet.version").strip() short = genShortKeys() optsNoVal = {optHelp, optVersion, optFmtSyncUpdate, optFmtSyncYes, - optSyncOffline, optSyncDocs, optSyncFilepaths, optSyncMetadata} + optInfoSyncOffline, optSyncDocs, optSyncFilepaths, optSyncMetadata} func generateNoVals: tuple[shortNoVal: set[char], longNoVal: seq[string]] = ## Returns the short and long keys for the options in `optsNoVal`. @@ -199,7 +202,7 @@ func genHelpText: string = optFmtSyncExercise: "Only operate on this exercise", optFmtSyncUpdate: "Prompt to update the unsynced track data", optFmtSyncYes: &"Auto-confirm prompts from --{$optFmtSyncUpdate} for updating docs, filepaths, and metadata", - optSyncOffline: "Do not update the cached 'problem-specifications' data", + optInfoSyncOffline: "Do not update the cached 'problem-specifications' data", optSyncDocs: "Sync Practice Exercise '.docs/introduction.md' and '.docs/instructions.md' files", optSyncFilepaths: "Populate empty 'files' values in Concept/Practice exercise '.meta/config.json' files", optSyncMetadata: "Sync Practice Exercise '.meta/config.json' metadata values", @@ -252,6 +255,8 @@ func genHelpText: string = optFmtSyncUpdate of "yesFmt": optFmtSyncYes + of "offlineInfo": + optInfoSyncOffline else: parseEnum[Opt](key) # Set the description for `fmt` options. @@ -456,7 +461,7 @@ proc handleOption(conf: var Conf; kind: CmdLineKind; key, val: string) = # Process action-specific options if not isGlobalOpt: case conf.action.kind - of actNil, actGenerate, actInfo, actLint: + of actNil, actGenerate, actLint: discard of actFmt: case opt @@ -468,6 +473,12 @@ proc handleOption(conf: var Conf; kind: CmdLineKind; key, val: string) = setActionOpt(yesFmt, true) else: discard + of actInfo: + case opt + of optInfoSyncOffline: + setActionOpt(offlineInfo, true) + else: + discard of actSync: case opt of optFmtSyncExercise: @@ -479,7 +490,7 @@ proc handleOption(conf: var Conf; kind: CmdLineKind; key, val: string) = of optSyncTests: setActionOpt(tests, parseVal[TestsMode](kind, key, val)) conf.action.scope.incl skTests - of optSyncOffline: + of optInfoSyncOffline: setActionOpt(offline, true) of optSyncDocs, optSyncMetadata, optSyncFilepaths: conf.action.scope.incl parseEnum[SyncKind]($opt) diff --git a/src/info/info.nim b/src/info/info.nim index f0da0136..90fe8c0f 100644 --- a/src/info/info.nim +++ b/src/info/info.nim @@ -1,6 +1,6 @@ import std/[algorithm, os, sequtils, sets, strformat, strutils, sugar, terminal] import pkg/jsony -import ".."/[cli, types_track_config] +import ".."/[cli, sync/probspecs, types_track_config] proc header(s: string): string = if colorStdout: @@ -60,22 +60,23 @@ type withoutCanonicalData: HashSet[string] deprecated: HashSet[string] - ProbSpecsState = object - lastUpdated: string - problemSpecificationsCommitRef: string - exercises: ProbSpecsExercises - -proc init(T: typedesc[ProbSpecsExercises]): T = - ## Reads the prob-specs data at compile-time, and returns an object containing - ## every exercise in `exercism/problem-specifications`, grouped by kind. - const slugsPath = currentSourcePath().parentDir() / "prob_specs_exercises.json" - let contents = staticRead(slugsPath) - contents.fromJson(ProbSpecsState).exercises +proc init(T: typedesc[ProbSpecsExercises], probSpecsDir: ProbSpecsDir): T = + result = T() + for kind, path in walkDir(probSpecsDir / "exercises"): + if kind == pcDir: + let exerciseSlug = path.lastPathPart() + if fileExists(path / ".deprecated"): + result.deprecated.incl exerciseSlug + elif fileExists(path / "canonical-data.json"): + result.withCanonicalData.incl exerciseSlug + else: + result.withoutCanonicalData.incl exerciseSlug proc unimplementedProbSpecsExercises(practiceExercises: seq[PracticeExercise], - foregone: HashSet[string]): string = - const probSpecsExercises = ProbSpecsExercises.init() + foregone: HashSet[string], + probSpecsDir: ProbSpecsDir): string = let + probSpecsExercises = ProbSpecsExercises.init(probSpecsDir) practiceExerciseSlugs = collect: for p in practiceExercises: {p.slug.`$`} @@ -129,8 +130,10 @@ proc info*(conf: Conf) = if fileExists(trackConfigPath): let t = TrackConfig.init trackConfigPath.readFile() + let probSpecsDir = ProbSpecsDir.init(conf) echo conceptsInfo(t.exercises.practice, t.concepts) - echo unimplementedProbSpecsExercises(t.exercises.practice, t.exercises.foregone) + echo unimplementedProbSpecsExercises(t.exercises.practice, t.exercises.foregone, + probSpecsDir) echo trackSummary(t.exercises.`concept`, t.exercises.practice, t.concepts) else: var msg = &"file does not exist: {trackConfigPath}" diff --git a/src/info/prob_specs_exercises.json b/src/info/prob_specs_exercises.json deleted file mode 100644 index 0840037e..00000000 --- a/src/info/prob_specs_exercises.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "last_updated": "2022-04-15T10:59:32Z", - "problem_specifications_commit_ref": "7cd0ab13d2f1158633c5d7f551fe2160ba7280c1", - "exercises": { - "with_canonical_data": [ - "acronym", - "affine-cipher", - "all-your-base", - "allergies", - "alphametics", - "anagram", - "armstrong-numbers", - "atbash-cipher", - "beer-song", - "binary-search", - "binary-search-tree", - "bob", - "book-store", - "bowling", - "change", - "circular-buffer", - "clock", - "collatz-conjecture", - "complex-numbers", - "connect", - "crypto-square", - "custom-set", - "darts", - "diamond", - "difference-of-squares", - "diffie-hellman", - "dnd-character", - "dominoes", - "etl", - "flatten-array", - "food-chain", - "forth", - "gigasecond", - "go-counting", - "grade-school", - "grains", - "grep", - "hamming", - "hello-world", - "high-scores", - "house", - "isbn-verifier", - "isogram", - "killer-sudoku-helper", - "kindergarten-garden", - "knapsack", - "largest-series-product", - "leap", - "linked-list", - "list-ops", - "luhn", - "markdown", - "matching-brackets", - "matrix", - "meetup", - "micro-blog", - "minesweeper", - "nth-prime", - "nucleotide-count", - "ocr-numbers", - "palindrome-products", - "pangram", - "pascals-triangle", - "perfect-numbers", - "phone-number", - "pig-latin", - "poker", - "pov", - "prime-factors", - "protein-translation", - "proverb", - "pythagorean-triplet", - "queen-attack", - "rail-fence-cipher", - "raindrops", - "rational-numbers", - "react", - "rectangles", - "resistor-color", - "resistor-color-duo", - "resistor-color-trio", - "rest-api", - "reverse-string", - "rna-transcription", - "robot-simulator", - "roman-numerals", - "rotational-cipher", - "run-length-encoding", - "saddle-points", - "satellite", - "say", - "scale-generator", - "scrabble-score", - "secret-handshake", - "series", - "sgf-parsing", - "sieve", - "simple-cipher", - "space-age", - "spiral-matrix", - "square-root", - "state-of-tic-tac-toe", - "sublist", - "sum-of-multiples", - "tournament", - "transpose", - "triangle", - "twelve-days", - "two-bucket", - "two-fer", - "variable-length-quantity", - "word-count", - "word-search", - "wordy", - "yacht", - "zebra-puzzle", - "zipper" - ], - "without_canonical_data": [ - "bank-account", - "dot-dsl", - "error-handling", - "hangman", - "ledger", - "lens-person", - "paasio", - "parallel-letter-frequency", - "robot-name", - "simple-linked-list", - "strain", - "tree-building" - ], - "deprecated": [ - "accumulate", - "binary", - "counter", - "hexadecimal", - "nucleotide-codons", - "octal", - "point-mutations", - "trinary" - ] - } -} diff --git a/src/sync/probspecs.nim b/src/sync/probspecs.nim index 34967685..e37b3f40 100644 --- a/src/sync/probspecs.nim +++ b/src/sync/probspecs.nim @@ -104,6 +104,10 @@ proc getNameOfRemote*(probSpecsDir: ProbSpecsDir; showError(&"there is no remote that points to '{location}' at '{host}' in " & &"the cached problem-specifications directory: '{probSpecsDir}'") +func isOffline(conf: Conf): bool = + (conf.action.kind == actSync and conf.action.offline) or + (conf.action.kind == actInfo and conf.action.offlineInfo) + proc validate(probSpecsDir: ProbSpecsDir, conf: Conf) = ## Raises an error if the given `probSpecsDir` is not a valid ## `problem-specifications` repo that is up-to-date with upstream. @@ -128,7 +132,7 @@ proc validate(probSpecsDir: ProbSpecsDir, conf: Conf) = "problem-specifications working directory is not clean: " & &"'{probSpecsDir}'") - if conf.action.offline: + if isOffline(conf): # Don't checkout `main` when `--offline` was passed, because: # 1. The current tests of the configlet executable require that # `configlet sync --offline` does not alter the state of the cached @@ -166,7 +170,7 @@ proc init*(T: typedesc[ProbSpecsDir], conf: Conf): T = result = T(getCacheDir() / "exercism" / "configlet" / "problem-specifications") if dirExists(result): validate(result, conf) - elif conf.action.offline: + elif isOffline(conf): let msg = fmt""" Error: --offline was passed, but there is no cached 'problem-specifications' repo at: '{result}'