From a53ebb4073346256a2dc0d1be7a1af84682737f5 Mon Sep 17 00:00:00 2001 From: mcmarius <23401453+mcmarius@users.noreply.github.com> Date: Mon, 5 Oct 2026 01:43:36 +0200 Subject: [PATCH 01/10] Generic docs fixes after branch rebases --- .internal/INFRA.md | 2 +- .internal/REPO.md | 4 ++-- .internal/mistakes.md | 2 +- AGENTS.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.internal/INFRA.md b/.internal/INFRA.md index 6911d0fb..063f6377 100644 --- a/.internal/INFRA.md +++ b/.internal/INFRA.md @@ -10,7 +10,7 @@ reading them. As a student you **use** them; you do **not** redesign them. step covering what you added (a second test executable, another run of your app) — is expected. Deleting or changing existing infrastructure is not: -* `CMakeLists.txt`, anything under `cmake/`, `scripts/`, `.github/` +* any `CMakeLists.txt`, anything under `cmake/`, `scripts/`, `.github/` * `.clang-tidy`, `.gitattributes`, `.gitignore`, `LICENSE.template` - but you can change `LICENSE` which is for _this_ code * `AGENTS.md` and anything under `.internal/` — the guidelines and course docs your AI assistant works diff --git a/.internal/REPO.md b/.internal/REPO.md index a335fbd3..5167276e 100644 --- a/.internal/REPO.md +++ b/.internal/REPO.md @@ -10,12 +10,12 @@ Checked against `main` and all template branches (`main`, `common-libs`, `descar | Path | What it is | |---|---| -| `CMakeLists.txt`, `cmake/` | CMake project: options, compiler flags, sanitizers, helpers. Infrastructure. | +| any `CMakeLists.txt`, `cmake/` | CMake project: options, compiler flags, sanitizers, helpers. Infrastructure. | | `scripts/` | `cmake.sh`, `run_cppcheck.sh`, `build_cppcheck.sh`, `run_tests.sh`, `run_valgrind.sh` (+ suppressions), `audit_ext_libs.sh`. Same checks CI runs. | | `.github/` | GitHub Actions: workflows, composite actions, Renovate configs. Infrastructure — and the CI your project requires. | | `.clang-tidy`, `.gitattributes`, `.gitignore`, `disable_modules.props` | Static analysis + linguist/VCS/MSBuild setup. Infrastructure. | | `include/` | Public headers of the library — this directory **is** your public interface. Starter example: `Example.h`. | -| `src/` | Library implementation, compiled once. Adding a file means adding it to `target_sources` in `CMakeLists.txt` (an addition, allowed). `src/internal/` is implementation-only: not shipped, not visible to `app/` or `tests/`. | +| `src/` | Library implementation, compiled once. Adding a file means adding it to `target_sources` in any `CMakeLists.txt` (an addition, allowed). `src/internal/` is implementation-only: not shipped, not visible to `app/` or `tests/`. | | `app/` | The executable: `main.cpp`, menus, wiring, I/O. Not reachable from `tests/` — by design. | | `tests/` | Test suites. `assert` / `static_assert` on `main`; framework examples on `tests/gtest` and `tests/Boost-ext-ut`. | | `assets/` | Your data/images/fonts (empty `.keep` on most branches). Document precisely where you got these from. | diff --git a/.internal/mistakes.md b/.internal/mistakes.md index d1ff54d8..bdc1edc8 100644 --- a/.internal/mistakes.md +++ b/.internal/mistakes.md @@ -142,4 +142,4 @@ or unclear in your particular project, ask — there is always a legitimate way Ask early, with specifics: what you tried, what you expected, what happened. A five-minute discussion at lab usually beats five hours (or five days) of guessing, and design problems do not get cheaper with age — fixed late, they become rewrites. Read [INFRA.md](INFRA.md) -before changing/deleting anything in `CMakeLists.txt`, `cmake/`, `scripts/` or `.github/`. +before changing/deleting anything in any `CMakeLists.txt`, `cmake/`, `scripts/` or `.github/`. diff --git a/AGENTS.md b/AGENTS.md index ada23a24..25b84048 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,7 +66,7 @@ explanation → guiding question → review of *their* code → a non-pasteable `tests/`, `README.md`, `LICENSE`, and your own data files (e.g. under `assets/` — not `tastatura.txt`). * **Infrastructure, never change:** `cmake/`, `scripts/`, `.github/`, `.clang-tidy`, `.gitattributes`, `.gitignore` — [why](.internal/INFRA.md). -* `CMakeLists.txt`: **adding is not editing** — your sources in a target, a new target, an `install()`, +* any `CMakeLists.txt`: **adding is not editing** — your sources in a target, a new target, an `install()`, a CI step covering what you added. Compiler flags, sanitizers, the stdlib logic and the job matrix stay frozen. * `.internal/` — read on demand: [mistakes](.internal/mistakes.md), [layout](.internal/REPO.md), From 23970e35652082dd43ea7abe76bb024ee76b2923 Mon Sep 17 00:00:00 2001 From: mcmarius <23401453+mcmarius@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:25:34 +0300 Subject: [PATCH 02/10] Add more common dirs and files --- branches.txt | 14 ++++++++++++++ ext/include/.keep | 0 ext/private/.keep | 0 3 files changed, 14 insertions(+) create mode 100644 branches.txt create mode 100644 ext/include/.keep create mode 100644 ext/private/.keep diff --git a/branches.txt b/branches.txt new file mode 100644 index 00000000..9b07624b --- /dev/null +++ b/branches.txt @@ -0,0 +1,14 @@ + baze-de-date/pqxx + baze-de-date/sqlite + common-libs + descarcare-date-api + ftxui + http-server + llms + raylib-cpp + sfml3 + sfml3-imagini-externe-api + sfml3-resurse-locale + tests/Boost-ext-ut + tests/gtest + wxwidgets diff --git a/ext/include/.keep b/ext/include/.keep new file mode 100644 index 00000000..e69de29b diff --git a/ext/private/.keep b/ext/private/.keep new file mode 100644 index 00000000..e69de29b From 12439218d40ef821d5ad88b59335daf7a5b0a96b Mon Sep 17 00:00:00 2001 From: mcmarius <23401453+mcmarius@users.noreply.github.com> Date: Tue, 20 Oct 2026 03:59:07 +0300 Subject: [PATCH 03/10] Check for unused ext libs --- scripts/audit_ext_libs.sh | 440 +++++++++++++++++++++++++++++++++----- 1 file changed, 385 insertions(+), 55 deletions(-) diff --git a/scripts/audit_ext_libs.sh b/scripts/audit_ext_libs.sh index 8afb2a29..db7447ec 100755 --- a/scripts/audit_ext_libs.sh +++ b/scripts/audit_ext_libs.sh @@ -1,61 +1,149 @@ #!/usr/bin/bash -# Include directories of dependencies must use -isystem. Otherwise, cppcheck abandons -# a file including from it at the first unparsed preprocessor construct, which turns -# checks into false positives and leaves the rest unchecked. The compilation database -# is audited, not CMakeLists.txt, since deps can be resolved from many places. - -# Fails when a directory below one of the dependency roots is given with -I. -# Directories of system packages given with -I are only reported, as they come -# from the interface of a dependency and are rarely ours to change. -find_dep_roots() { - local build_dir="$1" cached root - cached="$(sed -n 's/^FETCHCONTENT_BASE_DIR:[^=]*=//p; - s/^.*_\(SOURCE\|BINARY\)_DIR:[^=]*=//p' "${build_dir}/CMakeCache.txt" 2>/dev/null)" - { - printf '%s\n' "${EXT_DIR:-ext}" - printf '%s\n' "${cached}" - } | while IFS= read -r root; do - case "${root}" in - /*|[A-Za-z]:[\\/]*) printf '%s\n' "${root%/}" ;; - *) printf '%s\n' "${PWD%/}/${root%/}" ;; +# Audits build/compile_commands.json — the file scripts/cmake.sh enables, so CI and a local run read +# the same thing, and the cppcheck CI step runs this on every branch, on an unbuilt tree — for the +# two ways an external dependency is misconfigured: +# +# 1. a dependency include directory given with -I instead of -isystem -> error, exit 1. cppcheck +# abandons a file including from it. A system package directory is only reported, not ours to fix. +# 2. a dependency no student source includes -> warning only: it may be linked for its symbols, be a +# runtime, or generate its headers. Declare it beside the dependency, with CACHE so this reads it: +# set(AUDIT_EXT_LIBS_ALLOW "" CACHE STRING "linked, not included") +# Included only from tests/ is a note: for a test framework that is the design. +# +# bash ./scripts/audit_ext_libs.sh [-b build dir] [-v] (-v lists every dependency) +# env: BUILD_DIR, EXT_DIR, TESTS_DIR, AUDIT_EXT_LIBS_ALLOW (exceptions for this run only) +# No apostrophe below this line: the awk program is single-quoted. + +TESTS_DIR="${TESTS_DIR:-tests}" +VERBOSE_AUDIT="" +DEP_ROOTS=() # directories holding dependency code, umbrellas included +DEP_UNITS=() # "", one per dependency +ALLOW_FROM_CACHE="" + +abs_path() { + case "$1" in + /*|[A-Za-z]:[\\/]*) printf '%s' "${1%/}" ;; + *) printf '%s' "${PWD%/}/${1%/}" ;; + esac +} + +# CMakeCache.txt split into the two views above. Our own project is not a dependency, however +# inviting _SOURCE_DIR looks. ext/ names no dependency, so its include directories are +# reported per child, which points at the vendored library nobody includes; ext/lib is not a unit, +# no #include reaches it. +read_cmake_deps() { + local build_dir="$1" cache="${build_dir}/CMakeCache.txt" + local line key name value dir child base project="" + local entries=() + + [[ -f "${cache}" ]] || return 0 + while IFS= read -r line || [[ -n "${line}" ]]; do + key="${line%%:*}" + value="${line#*=}" + [[ "${key}" != "${line}" && "${value}" != "${line}" ]] || continue + case "${key}" in + CMAKE_PROJECT_NAME) project="${value}" ;; + AUDIT_EXT_LIBS_ALLOW) ALLOW_FROM_CACHE="${value}" ;; + FETCHCONTENT_BASE_DIR) DEP_ROOTS+=("$(abs_path "${value}")") ;; + *_SOURCE_DIR) entries+=("${key%_SOURCE_DIR}=${value}") ;; + *_BINARY_DIR) entries+=("${key%_BINARY_DIR}=${value}") ;; esac + done < "${cache}" + + DEP_ROOTS+=("$(abs_path "${EXT_DIR:-ext}")") + for line in "${entries[@]}"; do + name="${line%%=*}" + value="${line#*=}" + [[ "${name}" == "${project}" ]] && continue + DEP_ROOTS+=("$(abs_path "${value}")") + [[ -d "${value}" ]] || continue + DEP_UNITS+=("${name} ${value}") + done + + for dir in "${EXT_DIR:-ext}/include" "${EXT_DIR:-ext}/private"; do + [[ -d "${dir}" ]] || continue + while IFS= read -r child; do + child="$(abs_path "${child}")" + base="${child##*/}" + if [[ -d "${child}" ]]; then + find "${child}" -type f \ + \( -name '*.h' -o -name '*.hpp' -o -name '*.hxx' -o -name '*.hh' \) \ + -print -quit 2>/dev/null | grep -q . || continue + else + [[ "${base}" == *.h || "${base}" == *.hpp || "${base}" == *.hxx || "${base}" == *.hh ]] \ + || continue + fi + # name relative to the repository, directory as the compiler sees it + DEP_UNITS+=("${child#"${PWD}/"} ${child}") + done < <(find "${dir}" -mindepth 1 -maxdepth 1 2>/dev/null) done } -audit_compile_commands() { - local build_dir="$1" roots - roots="$(find_dep_roots "${build_dir}")" - AUDIT_REPO="${PWD}" AUDIT_ROOTS="${roots}" awk ' +audit_build_tree() { + local build_dir="$1" + + AUDIT_REPO="${PWD}" \ + AUDIT_TESTS_DIR="$(abs_path "${TESTS_DIR}")" \ + AUDIT_VERBOSE="${VERBOSE_AUDIT}" \ + AUDIT_GITHUB="${GITHUB_ACTIONS:-}" \ + AUDIT_EXT_ROOT="$(abs_path "${EXT_DIR:-ext}")" \ + AUDIT_ROOTS="$(printf '%s\n' "${DEP_ROOTS[@]}")" \ + AUDIT_UNITS="$(printf '%s\n' "${DEP_UNITS[@]}")" \ + AUDIT_ALLOW="${ALLOW_FROM_CACHE};${AUDIT_EXT_LIBS_ALLOW:-}" \ + awk ' + # norm() lowercases for comparison; never hand it to the filesystem, a real path is not lower + # case, so arrays keep the raw path to open and the normalised one to match function norm(path) { + return tolower(path_of(path)) + } + function path_of(path) { gsub(/\\/, "/", path) sub(/\/+$/, "", path) - return tolower(path) + return path + } + function display(path) { + path = path_of(path) + if (inside(norm(path), repo)) return substr(path, length(repo) + 2) + return path } function inside(path, root) { root = norm(root) return index(path, root) == 1 && \ (path == root || substr(path, length(root) + 1, 1) == "/") } - function is_dep_dir(path, i) { + function is_dep_dir(path_n, i) { for (i = 1; i <= n_roots; i++) - if (inside(path, roots[i])) + if (inside(path_n, roots[i])) return 1 return 0 } - function report(dir, file, kind) { - if (dir in reported) - return - reported[dir] = 1 - if (is_dep_dir(dir)) { - kind = "error: dependency include directory is not SYSTEM" - errors++ - } else { - kind = "warning: system include directory is not SYSTEM" - } - printf "%s\n %s\n used by %s\n", kind, dir, file + # inside the repository is not enough to be ours: a dependency can be fetched below it + function is_own_file(path_n) { + return inside(path_n, repo) && !is_dep_dir(path_n) } - function check_includes(command, file, rest, mode, dir) { + function dirname_of(path, dir) { + dir = path + if (!match(dir, /\/[^\/]*$/)) return "." + sub(/\/[^\/]*$/, "", dir) + return dir + } + # getline: 1 a line, 0 empty but present, -1 nothing to read; a directory reads as an error + function exists(path, found, probe) { + if (path in exists_memo) return exists_memo[path] + found = (getline probe < path) + close(path) + exists_memo[path] = (found >= 0) + return exists_memo[path] + } + # case, dashes and underscores never distinguish two dependency names + function name_key(name) { + gsub(/-/, "_", name) + return tolower(name) + } + + function parse_includes(command, rest, mode, dir) { + n_inc = 0 rest = command while (match(rest, /(-I|-isystem)/)) { mode = substr(rest, RSTART, RLENGTH) @@ -67,46 +155,275 @@ audit_compile_commands() { } sub(/^(=|\\?"| )/, "", dir) sub(/\\?"$/, "", dir) - dir = norm(dir) - if (dir == "" || mode == "-isystem" || inside(dir, repo)) + if (dir == "") continue + dir = path_of(dir) + if (dir !~ /^\// && dir !~ /^[a-zA-Z]:\//) dir = path_of(entry) "/" dir + n_inc++ + imode[n_inc] = mode + idir[n_inc] = dir + idir_n[n_inc] = norm(dir) + } + } + + # --- finding 1: a dependency directory must reach the compiler as SYSTEM --------------- + function audit_system(file, i) { + for (i = 1; i <= n_inc; i++) { + if (imode[i] == "-isystem") continue + if (is_dep_dir(idir_n[i])) report_system(idir[i], idir_n[i], file, 1) + else if (!inside(idir_n[i], repo)) report_system(idir[i], idir_n[i], file, 0) + } + } + function report_system(dir, dir_n, file, is_dep, kind) { + if (dir_n in reported) return + reported[dir_n] = 1 + if (is_dep) { + kind = "error: dependency include directory is not SYSTEM" + errors++ + } else { + kind = "warning: system include directory is not SYSTEM" + } + printf "%s\n %s\n used by %s\n", kind, dir, file + } + + # --- finding 2: dependencies nobody includes -------------------------------------------- + # Student code is ours (is_own_file) and not below tests/. Include directories of every student + # TU are pooled into one search path, and the closure over #include reaches the usual shape of the + # mistake: a source includes a header of yours, that header the dependency. Where resolution + # cannot be exact it is generous: it can hide an unused dependency, never invent one. + function record_student_tu(file, file_n, i) { + tu_raw[file_n] = path_of(file) + if (file_n in tu_kind) { + if (tu_kind[file_n] == 2 && !inside(file_n, tests_dir)) tu_kind[file_n] = 1 + } else { + tu_kind[file_n] = inside(file_n, tests_dir) ? 2 : 1 + } + for (i = 1; i <= n_inc; i++) { + if (idir_n[i] in inc_memo) continue + inc_memo[idir_n[i]] = 1 + n_inc_dirs++ + inc_dirs[n_inc_dirs] = idir[i] + } + } + function enqueue(path, product) { + if (scans + q_tail - q_head >= max_files) { truncated = 1; return } + q_tail++ + queue_file[q_tail] = path + queue_product[q_tail] = product + } + function scan(path, product, line, inc, quoted, base, resolved, i) { + if ((path in scanned) && (!product || (path in scanned_product))) return + scanned[path] = 1 + if (product) scanned_product[path] = 1 + scans++ + base = dirname_of(path) + while ((getline line < path) > 0) { + if (!match(line, /^[ \t]*#[ \t]*include[ \t]*[<"]/)) continue + quoted = substr(line, RSTART + RLENGTH - 1, 1) == "\"" + inc = substr(line, RSTART + RLENGTH) + if (quoted ? match(inc, /"/) : match(inc, />/)) inc = substr(inc, 1, RSTART - 1) + sub(/^[ \t]+/, "", inc) + sub(/[ \t]+$/, "", inc) + if (inc == "") continue + resolved = "" + if (quoted && exists(base "/" inc)) resolved = base "/" inc + else + for (i = 1; i <= n_inc_dirs; i++) + if (exists(inc_dirs[i] "/" inc)) { resolved = inc_dirs[i] "/" inc; break } + if (resolved == "") continue # a system header, or one the build still owes us + note_usage(resolved, product) + # open only our headers and dependency headers: the pooled path may hold system directories + if (is_own_file(norm(resolved)) || is_dep_dir(norm(resolved))) enqueue(resolved, product) + } + close(path) + } + function note_usage(resolved, product, unit) { + unit = unit_of(norm(resolved)) + if (unit == "") return + used[unit] = 1 + if (product) used_product[unit] = 1 + } + # deepest match wins over the umbrella above it + function unit_of(path_n, name, i, unit, depth) { + unit = "" + depth = -1 + for (name in unit_n) + for (i = 1; i <= unit_n[name]; i++) + if (inside(path_n, unit_dir[name, i]) && length(unit_dir[name, i]) > depth) { + unit = name + depth = length(unit_dir[name, i]) + } + return unit + } + # a dependency inside another one is that dependency to add or drop, never ours; reporting it + # would only teach students to silence the audit. Children of ext/ are ours, not parts of one. + function is_nested(unit, name, i, j) { + if (inside(unit_dir[unit, 1], ext_root)) return 0 + for (name in unit_n) { + if (name == unit) continue + for (i = 1; i <= unit_n[unit]; i++) + for (j = 1; j <= unit_n[name]; j++) + if (inside(unit_dir[unit, i], unit_dir[name, j]) && \ + unit_dir[unit, i] != unit_dir[name, j]) + return 1 + } + return 0 + } + function is_allowed(unit, i, j, entry) { + for (i = 1; i <= n_allow; i++) { + entry = allow[i] + if (entry == "") continue + if (entry == unit) return 1 + if (entry !~ /\//) continue + for (j = 1; j <= unit_n[unit]; j++) + if (inside(unit_dir[unit, j], entry) || inside(entry, unit_dir[unit, j])) return 1 + } + return 0 + } + function warn(text) { + warnings++ + if (github) print "::warning title=Unused dependency::" text + else print "warning: " text + } + function unit_dirs(unit, i, out) { + out = "" + for (i = 1; i <= unit_n[unit]; i++) + out = out (i > 1 ? "\n " : " ") display(unit_raw[unit, i]) + return out + } + function audit_unused( i, file, unit, status) { + if (n_units == 0) return # no dependency configured, nothing to say + for (file in tu_kind) enqueue(tu_raw[file], tu_kind[file] == 1) + while (q_head < q_tail) { + q_head++ + scan(queue_file[q_head], queue_product[q_head]) + } + + if (verbose) { + print "dependency audit:" + for (i = 1; i <= n_unit_names; i++) { + unit = unit_order[i] + if (unit in used) status = (unit in used_product ? "used" : "tests only") + else if (allowed_flag[unit]) status = "declared as used without including" + else if (nested_flag[unit]) status = "part of another dependency" + else status = "NOT INCLUDED" + printf " %-26s %s\n", unit, status + } + } + for (i = 1; i <= n_unit_names; i++) { + unit = unit_order[i] + if (unit in used) { + if (!(unit in used_product)) + printf "note: dependency %s is included only from %s/ — intended for a test " \ + "framework, worth a look otherwise\n", unit_label[unit], + relative_tests continue - if (dir !~ /^\// && dir !~ /^[a-z]:\//) # relative to the entry - dir = norm(entry "/" dir) - report(dir, file) + } + if (allowed_flag[unit] || nested_flag[unit]) continue + warn(sprintf("no student source includes anything from dependency %s", unit_label[unit])) + print unit_dirs(unit) + printf " downloaded and built on every CI run for nothing; drop the dependency, or\n" + printf " keep it and declare why, with:\n" + printf " set(AUDIT_EXT_LIBS_ALLOW \"%s\" CACHE STRING \"why it stays\")\n", \ + unit_label[unit] } + if (warnings) + printf "%d of %d configured dependencies are not included by any student source\n", \ + warnings, n_unit_names + if (truncated) + printf "note: the #include closure stopped after %d files; some dependencies may look\n" \ + " worse than they are\n", max_files } + BEGIN { - repo = ENVIRON["AUDIT_REPO"] + repo = norm(ENVIRON["AUDIT_REPO"]) + ext_root = norm(ENVIRON["AUDIT_EXT_ROOT"]) + relative_tests = display(ENVIRON["AUDIT_TESTS_DIR"]) + tests_dir = norm(ENVIRON["AUDIT_TESTS_DIR"]) + verbose = ENVIRON["AUDIT_VERBOSE"] != "" + github = ENVIRON["AUDIT_GITHUB"] != "" + max_files = 20000 + q_head = 0 + q_tail = 0 + n_roots = split(ENVIRON["AUDIT_ROOTS"], roots, "\n") + for (i = 1; i <= n_roots; i++) roots[i] = norm(roots[i]) + + n_units = split(ENVIRON["AUDIT_UNITS"], lines, "\n") + n_unit_names = 0 + for (i = 1; i <= n_units; i++) { + if (lines[i] !~ /\t/) continue + name = lines[i] + dir = lines[i] + sub(/\t.*$/, "", name) + sub(/^[^\t]*\t/, "", dir) + key = name_key(name) + if (!(key in unit_n)) { + n_unit_names++ + unit_order[n_unit_names] = key + unit_label[key] = name + } + unit_n[key]++ + unit_dir[key, unit_n[key]] = norm(dir) + unit_raw[key, unit_n[key]] = dir + } + + n_allow = split(ENVIRON["AUDIT_ALLOW"], raw_allow, /[;,\n] */) + for (i = 1; i <= n_allow; i++) { + entry = raw_allow[i] + gsub(/^[ \t]+|[ \t]+$|^"|"$/, "", entry) + allow[i] = (entry ~ /\//) ? norm(entry) : name_key(entry) + } + + # nested dependencies and declared exceptions, decided once, before any scanning + for (i = 1; i <= n_unit_names; i++) { + unit = unit_order[i] + nested_flag[unit] = is_nested(unit) + allowed_flag[unit] = is_allowed(unit) + } errors = 0 + warnings = 0 } + + # an entry: "directory" opens it, command and file arrive in either order, and the next entry + # (or the end of the file) closes it and runs both audits on it /"directory": "/ { + finish_entry() entry = $0 sub(/^ *"directory": *"/, "", entry) sub(/".*$/, "", entry) next } + /"command": "/ { + command = $0 + sub(/^ *"command": *"/, "", command) + sub(/",?$/, "", command) + next + } /"file": "/ { file = $0 sub(/^ *"file": *"/, "", file) sub(/".*$/, "", file) - # an entry lists its command before its file, so the check runs here. - # A dependency building itself is not our business, only our own files. - if (inside(norm(file), repo)) - check_includes(command, file) - command = "" next } - /"command": "/ { - command = $0 - sub(/^ *"command": *"/, "", command) - sub(/",?$/, "", command) - next + + function finish_entry( file_n) { + if (file == "") return + file_n = norm(file) + if (is_own_file(file_n)) { + parse_includes(command) + audit_system(file) + record_student_tu(file, file_n) + } + command = "" + file = "" } + END { + finish_entry() if (errors) printf "%d dependency include director%s not passed with -isystem\n", \ errors, (errors == 1 ? "y is" : "ies are") + audit_unused() exit(errors > 0) } ' "${build_dir}/compile_commands.json" @@ -114,7 +431,20 @@ audit_compile_commands() { main() { local build_dir="${BUILD_DIR:-build}" - audit_compile_commands "${build_dir}" + local opt + while getopts ":b:v" opt; do + case "${opt}" in + b) build_dir="${OPTARG}" ;; + v) VERBOSE_AUDIT=1 ;; + *) printf "Unknown option %s; available options: \n\ + -b (build dir)\n\ + -v (list every dependency and its status)\n" "${opt}" + exit 1 ;; + esac + done + + read_cmake_deps "${build_dir}" + audit_build_tree "${build_dir}" } main "$@" From 5b4eaf85bca324d3a4667a7683f7f2b889bac325 Mon Sep 17 00:00:00 2001 From: mcmarius <23401453+mcmarius@users.noreply.github.com> Date: Tue, 20 Oct 2026 04:19:32 +0300 Subject: [PATCH 04/10] Comment out broken rebase workflow --- .github/workflows/rebase-branches.yml | 114 +++++++++++++------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/.github/workflows/rebase-branches.yml b/.github/workflows/rebase-branches.yml index 2c34f41e..cb10ac50 100644 --- a/.github/workflows/rebase-branches.yml +++ b/.github/workflows/rebase-branches.yml @@ -28,63 +28,63 @@ jobs: with: fetch-depth: 0 - - name: Configure identity - run: | - git config user.email "${{ github.actor }}@users.noreply.github.com" - git config user.name "${{ github.actor }}" - - - name: Fetch branches - run: | - git fetch origin - - # Daca nu am nimic in input, atunci se face rebase din main - if [[ -z "${{ env.INPUT_BASE }}" ]]; then - REBASE_BRANCH="main" - else - REBASE_BRANCH="${{ env.INPUT_BASE }}" - fi - - git branch -r | # lista toate branch-urile remote - grep 'origin/' | # pastrez doar branch-urile care incep cu origin/ - grep -v "origin/HEAD" | # exclud HEAD - grep -v "origin/$REBASE_BRANCH" | # exclud branch-ul de rebase - sed 's#origin/##' > branches.txt # sterg origin/ si salveaza in fisier - - echo "Branches to rebase:" - cat branches.txt - - - name: Read custom base branches - run: | - # citesc config-ul cu base-uri custom din JSON - echo "BASE_BRANCHES=$(jq -c . .github/config/rebase-branches.json)" >> $GITHUB_ENV - - - name: Rebase branches - continue-on-error: true # in caz de conflict sa nu se opreasca - run: | - while read -r BRANCH; do - echo "Rebasing branch: $BRANCH" - - # Daca BRANCH e in rebase-branches.json, folosesc base-ul de acolo. Daca nu, folosesc default_base = 'main' - BASE=$(echo "$BASE_BRANCHES" | jq -r --arg BRANCH "$BRANCH" '.[$BRANCH] // "main"') - - # Daca a fost scris un base in input, suprascriu BASE cu ce a fost in input - if [[ -n "${{ env.INPUT_BASE }}" ]]; then - BASE="${{ env.INPUT_BASE }}" - fi - - echo "Using base: $BASE" - git checkout -B "$BRANCH" "origin/$BRANCH" - - if git rebase "origin/$BASE"; then - echo "Rebase succeeded" - # FIXME - # (git push origin "$BRANCH" --force-with-lease && echo "Rebased $BRANCH") || echo "Push failed" - else - echo "Conflict on $BRANCH" - git rebase --abort - fi - echo "-----------------------------------" - done <<< "$(cat branches.txt)" + # - name: Configure identity + # run: | + # git config user.email "${{ github.actor }}@users.noreply.github.com" + # git config user.name "${{ github.actor }}" + # + # - name: Fetch branches + # run: | + # git fetch origin + # + # # Daca nu am nimic in input, atunci se face rebase din main + # if [[ -z "${{ env.INPUT_BASE }}" ]]; then + # REBASE_BRANCH="main" + # else + # REBASE_BRANCH="${{ env.INPUT_BASE }}" + # fi + # + # git branch -r | # lista toate branch-urile remote + # grep 'origin/' | # pastrez doar branch-urile care incep cu origin/ + # grep -v "origin/HEAD" | # exclud HEAD + # grep -v "origin/$REBASE_BRANCH" | # exclud branch-ul de rebase + # sed 's#origin/##' > branches.txt # sterg origin/ si salveaza in fisier + # + # echo "Branches to rebase:" + # cat branches.txt + # + # - name: Read custom base branches + # run: | + # # citesc config-ul cu base-uri custom din JSON + # echo "BASE_BRANCHES=$(jq -c . .github/config/rebase-branches.json)" >> $GITHUB_ENV + # + # - name: Rebase branches + # continue-on-error: true # in caz de conflict sa nu se opreasca + # run: | + # while read -r BRANCH; do + # echo "Rebasing branch: $BRANCH" + # + # # Daca BRANCH e in rebase-branches.json, folosesc base-ul de acolo. Daca nu, folosesc default_base = 'main' + # BASE=$(echo "$BASE_BRANCHES" | jq -r --arg BRANCH "$BRANCH" '.[$BRANCH] // "main"') + # + # # Daca a fost scris un base in input, suprascriu BASE cu ce a fost in input + # if [[ -n "${{ env.INPUT_BASE }}" ]]; then + # BASE="${{ env.INPUT_BASE }}" + # fi + # + # echo "Using base: $BASE" + # git checkout -B "$BRANCH" "origin/$BRANCH" + # + # if git rebase "origin/$BASE"; then + # echo "Rebase succeeded" + # # FIXME + # # (git push origin "$BRANCH" --force-with-lease && echo "Rebased $BRANCH") || echo "Push failed" + # else + # echo "Conflict on $BRANCH" + # git rebase --abort + # fi + # echo "-----------------------------------" + # done <<< "$(cat branches.txt)" # https://github.com/orgs/community/discussions/25702 # - name: Trigger Publish Workflow From c9b51e20a203a6e349777cd29f36863df3e87764 Mon Sep 17 00:00:00 2001 From: mcmarius <23401453+mcmarius@users.noreply.github.com> Date: Tue, 20 Oct 2026 05:47:15 +0300 Subject: [PATCH 05/10] Unreviewed hopefully not (too much) slop --- .internal/BRANCHES.md | 99 +++++++++++++++++++++++++++++++++++++++++++ .internal/README.md | 5 ++- .internal/REPO.md | 43 +++---------------- .internal/mistakes.md | 37 ++++++++++++++-- AGENTS.md | 60 ++++++++++++++++++++++---- 5 files changed, 193 insertions(+), 51 deletions(-) create mode 100644 .internal/BRANCHES.md diff --git a/.internal/BRANCHES.md b/.internal/BRANCHES.md new file mode 100644 index 00000000..02093fd1 --- /dev/null +++ b/.internal/BRANCHES.md @@ -0,0 +1,99 @@ +# Template branches — layout and per-branch differences + +One row per branch in [`branches.txt`](../branches.txt). You work on **one** of them: find the row your +tree matches, read that row, treat the others as background. Branch differences are not yours to +reconcile. + +`Base` records where the branch's infrastructure came from. It is **not** an upstream to rebase, merge or +sync with: you start from a branch and stay on it, cherry-picking from another one when you want a feature +it has. That makes your tree a mix, so **match the tree, not the branch name** — a fork defaults to +`main` or `master`, and an SFML setup ported onto a `main` fork is legitimate work, not a mistake. + +* **Detect / ships** — evidence in the tree, strongest first, and the files the branch adds. `*` marks + starter code: adapt, rename or delete it freely; what must survive is the separation of `include/`, + `src/`, `src/internal/`, `app/`, `tests/`, not the example that illustrates it. +* **Extra `scripts/`** — on top of the set in [the layout below](#the-layout-every-branch-has). +* **CI** — the profiles are in [Reading CI](#reading-ci); the cell lists only what differs. + +## The layout every branch has + +Same on every branch — what yours adds is its row below. [INFRA.md](INFRA.md) says what is read-only for +you, [REPO.md](REPO.md) what is generated and never committed. + +| Path | What it is | +|---|---| +| any `CMakeLists.txt`, `cmake/` | CMake project: options, compiler flags, sanitizers, helpers. Infrastructure. | +| `scripts/` | `cmake.sh`, `run_cppcheck.sh`, `build_cppcheck.sh`, `run_tests.sh`, `run_valgrind.sh` + `valgrind-suppressions.supp`, `audit_ext_libs.sh` — the same checks CI runs. | +| `.github/` | GitHub Actions: workflows, composite actions, Renovate configs. Infrastructure — and the CI your project requires. | +| `.clang-tidy`, `.gitattributes`, `.gitignore`, `disable_modules.props` | Static analysis + linguist/VCS/MSBuild setup. Infrastructure. | +| `include/` | Public headers of the library — this directory **is** your public interface. | +| `src/` | Library implementation, compiled once; a new file goes into the target that declares it ([registering](#registering-a-source-a-test-or-a-target)). `src/internal/` is implementation-only: not shipped, not visible to `app/` or `tests/`. | +| `app/` | The executable: `main.cpp`, menus, wiring, I/O. Not reachable from `tests/` — by design. | +| `tests/` | Test suites, one executable registered with CTest; the frameworks themselves come from `ext/` or the build dir. | +| `assets/` | Your data/images/fonts, copied by the configured `install()`/`copy_files()` step. | +| `ext/` | Vendored third-party *lightweight* code, marked `linguist-vendored` in `.gitattributes`. `ext/include/` is on your public interface (`SYSTEM PUBLIC`), `ext/private/` stays inside `src/`, `ext/lib/` holds a vendored dep's compilable code. | +| `tastatura.txt` | Keyboard input only (`std::cin`); file data goes in your own files under `assets/`. | +| `README.md` | Project description + the homework checkboxes. Yours to edit. | +| `LICENSE`, `LICENSE.template` | AGPLv3 for your code / Unlicense for the template. You can edit the one for your code. | +| `launcher.command` | Double-click launcher template for the built `./oop_main` binary. Do not edit. Infrastructure. | + +## Branches + +| Branch | Base | Detect / ships | Extra `scripts/` | CI | +|---|---|---|---|---| +| `baze-de-date/pqxx` | `main` | `include/Database.h`\*, `src/database/Database.cpp`\*, `cmake/MinGWLibpq.cmake`, `.env.example`, `find_package(PostgreSQL)`, `PQXX_VERSION` | `configure_postgres.sh` — creates database, user and `pgcrypto`; CI runs it on every OS | A, but **no ASan row on Windows MSVC** | +| `baze-de-date/sqlite` | `main` | `FetchContent` `sqlite3`, `add_library(sqlite3 STATIC …/sqlite3.c)` | — | A | +| `common-libs` | `main` | `ext/include/{date,csv-parser}` (public), `ext/private/{digestpp,random,rlutil}`, `assets/date.csv`\* | — | A | +| `descarcare-date-api` | `main` | `FetchContent` `cpr`, `ext/include/json/` | — | A; macOS-GCC row commented | +| `ftxui` | `main` | `FetchContent` `ftxui`, links `ftxui::screen` / `::dom` / `::component` | — | A | +| `http-server` | `main` | `ext/include/httplib.h` + `ext/lib/httplib.cc` (library target `httplib`), `app/main-client.cpp`\*, target `${MAIN_EXECUTABLE_NAME}_client` | — | A; the runtime rows start the server in the background, and Valgrind analyses `oop_main_client` + `oop_test` | +| `llms` | `descarcare-date-api` | `FetchContent` `cpr numkong`, `NUMKONG_VERSION`, `ext/include/json/` | `run_llm.sh` — fetches and starts a llamafile; CI sources it before every runtime row | A; macOS-GCC row commented | +| `raylib-cpp` | `sfml3` | `FetchContent` `raylib raylib-cpp`, `target_link_libraries(oop PUBLIC raylib)` | GUI set (see `sfml3`) | B; macOS-GCC row commented | +| `sfml3` | `main` | `FetchContent` `SFML`, links `SFML::Graphics`, window title `"My Window"`\* | `install-linux-deps.sh` (dev packages per distro family), `run_sanitizers.sh`, `run_ui_test.sh` | B; macOS-GCC row commented | +| `sfml3-imagini-externe-api` | `sfml3` | `FetchContent` `SFML cpr` — images fetched over HTTP | GUI set (see `sfml3`) | B; macOS-GCC row commented | +| `sfml3-resurse-locale` | `sfml3` | `include/ResourceManager.hpp`\*, `src/ResourceManager.cpp`\*, `assets/fonts/`\*, `assets/images/`\*, `target_link_libraries(oop PUBLIC SFML::…)` | GUI set (see `sfml3`) | B; macOS-GCC row commented | +| `tests/Boost-ext-ut` | `main` | `ext/include/boost/ut.hpp`, `MAIN_LIBRARY_SOURCES`, second library target `oop_nslib` | — | A; the tests and `oop_nslib` are built **without** sanitizers | +| `tests/gtest` | `main` | `app/CMakeLists.txt` and `src/CMakeLists.txt` exist, `FetchContent` `gtest` inside `tests/CMakeLists.txt`, `include/BankAccount.h`\* | — | A; googletest is fetched into the build dir, never committed | +| `wxwidgets` | `sfml3` | `FetchContent` `wx`, `target_link_libraries(oop PUBLIC wx::core)` — GUI plumbing only, no SFML in the tree | GUI set (see `sfml3`) | B; macOS-GCC row commented | + +## Different on purpose + +Every branch rewrites the same two README regions for its own stack (the homework checkbox block and +`1. Pasul de configurare`), and `Resurse` belongs to the student. That is by design: never copy a README +region from another branch or from `main`, and never "align" two branches. + +## Registering a source, a test or a target + +Three shapes exist and all three are correct on their branch. Adding your own `CMakeLists.txt` is fine +too — the freeze is on deleting and changing, not on adding ([INFRA.md](INFRA.md)). + +* **Root `CMakeLists.txt`** (every branch but the two below): sources listed in + `add_library` / `add_executable` / `target_sources`, tests in `tests/CMakeLists.txt`. +* **Per directory** (`tests/gtest`): library sources in `src/CMakeLists.txt`, the executable in + `app/CMakeLists.txt`, tests in `tests/CMakeLists.txt`. +* **Shared source list** (`tests/Boost-ext-ut`): one `MAIN_LIBRARY_SOURCES` variable feeds the two library + targets, `oop` and the uninstrumented `oop_nslib`. + +Lists are registered directly even where a variable would read better because IDEs write into +`add_executable` regardless; pick the shape your branch has and do not switch it for aesthetics. + +## Reading CI + +* **A red row is the only certainty**: something is wrong, and the log of that step says what. A green + row says nothing about the checks that did not run — never tell a student a leak check or a test + "passed" because a row was green. +* **Profile A** (all non-GUI branches): every row blocks except the *Sanitizers (Windows MSVC)* step, + which is `continue-on-error` on every branch. +* **Profile B** (the 5 GUI branches): on Linux the ASan and Valgrind steps are `continue-on-error` and end + in `|| true`, and no step starts the app — a GUI hangs a headless runner. There, **no CI row can fail + because of your program**: the sanitizer and Valgrind output is in the step logs and you are expected to + read it, locally too (`./scripts/run_valgrind.sh`, an ASan configure). +* Commented matrix rows and advisory steps are **intentional**: the template keeps as many checks as it can + and drops only what is genuinely incompatible, with 3 OSes × 2 compilers as the floor. Re-enabling, + deleting or reordering a row is a forbidden edit — if a row looks broken, ask the teacher. +* Some external libraries do not play nice with sanitizers on some OSes; that is why + `tests/Boost-ext-ut` compiles an uninstrumented twin of the library for the test binary. +* `scripts/run_ui_test.sh` (sends keystrokes with `xdotool` to the window named by `APP_WINDOW`, which must + match the window title set in `app/`) and `scripts/run_sanitizers.sh` are **CI-internal**: the first + needs `xvfb`, the second expects the unpacked artifact bundle as its working directory. Neither is + related to `scripts/run_tests.sh`. diff --git a/.internal/README.md b/.internal/README.md index ec2c282d..ded611d3 100644 --- a/.internal/README.md +++ b/.internal/README.md @@ -2,7 +2,10 @@ Short, on-demand reference docs for this course. Read one when the task calls fo * [`mistakes.md`](mistakes.md) - recurring mistakes and why they cost points; use when reviewing code or before a commit/push. -* [`REPO.md`](REPO.md) - what is in the repository, what each branch adds, what is generated. +* [`BRANCHES.md`](BRANCHES.md) - the layout, then one row per template branch: how to tell which stack your + tree is, where you register a source/test/target there, what it ships, which CI rows block. Start here in + a new repo. +* [`REPO.md`](REPO.md) - what is generated and never committed, what is not in your repo at all. * [`INFRA.md`](INFRA.md) - the parts of the template that are course infrastructure: do not modify. * [`INTEGRITY.md`](INTEGRITY.md) - keeping these guidelines honest: pristine copy stored outside the repo on the first session, diffed every session, divergences recorded in the README (hidden, append-only); diff --git a/.internal/REPO.md b/.internal/REPO.md index 5167276e..6c296690 100644 --- a/.internal/REPO.md +++ b/.internal/REPO.md @@ -1,41 +1,8 @@ # What is in this repository -Checked against `main` and all template branches (`main`, `common-libs`, `descarcare-date-api`, -`baze-de-date/pqxx`, `baze-de-date/sqlite`, `http-server`, `llms`, `ftxui`, `sfml3`, -`sfml3-resurse-locale`, `sfml3-imagini-externe-api`, `raylib-cpp`, `wxwidgets`, `tests/gtest`, -`tests/Boost-ext-ut`) on 2026-10-03. Everything below is **read-only for you** unless the -[INFRA guide](INFRA.md) says otherwise. - -## Layout you get on every branch - -| Path | What it is | -|---|---| -| any `CMakeLists.txt`, `cmake/` | CMake project: options, compiler flags, sanitizers, helpers. Infrastructure. | -| `scripts/` | `cmake.sh`, `run_cppcheck.sh`, `build_cppcheck.sh`, `run_tests.sh`, `run_valgrind.sh` (+ suppressions), `audit_ext_libs.sh`. Same checks CI runs. | -| `.github/` | GitHub Actions: workflows, composite actions, Renovate configs. Infrastructure — and the CI your project requires. | -| `.clang-tidy`, `.gitattributes`, `.gitignore`, `disable_modules.props` | Static analysis + linguist/VCS/MSBuild setup. Infrastructure. | -| `include/` | Public headers of the library — this directory **is** your public interface. Starter example: `Example.h`. | -| `src/` | Library implementation, compiled once. Adding a file means adding it to `target_sources` in any `CMakeLists.txt` (an addition, allowed). `src/internal/` is implementation-only: not shipped, not visible to `app/` or `tests/`. | -| `app/` | The executable: `main.cpp`, menus, wiring, I/O. Not reachable from `tests/` — by design. | -| `tests/` | Test suites. `assert` / `static_assert` on `main`; framework examples on `tests/gtest` and `tests/Boost-ext-ut`. | -| `assets/` | Your data/images/fonts (empty `.keep` on most branches). Document precisely where you got these from. | -| `ext/` | Vendored third-party *lightweight* code (e.g. header-only libs), marked `linguist-vendored` in `.gitattributes`. `ext/include` is on your public interface (`SYSTEM PUBLIC`), `ext/private` stays inside `src/`; keep it private unless a header in `include/` names its type. On most branches it only contains `.keep`. | -| `tastatura.txt` | Keyboard input only (`std::cin`); file data goes in your own files under `assets/`. | -| `README.md` | Project description + the homework checkboxes. Yours to edit. | -| `LICENSE`, `LICENSE.template` | AGPLv3 for your code / Unlicense for the template. You can edit the one for your code. | -| `launcher.command` | Double-click launcher template for the built `./oop_main` binary. Do not edit. Infrastructure. | - -## Extras that exist only on some branches - -| Branch | Extra | -|---|---| -| `baze-de-date/pqxx` | `.env` (database name/user/password/host — edit the values, keep the file), `src/database/Database.{h,cpp}` | -| `http-server` | `main-client.cpp` (client for the server example), `ext/include/httplib.h`, `ext/lib/httplib.cc` | -| `common-libs` | header-only libs under `ext/include/` (`date`, `csv-parser`, `digestpp`, `random`, `rlutil`), `assets/date.csv` | -| `descarcare-date-api`, `llms` | `ext/include/json/` (nlohmann/json) | -| `sfml3-resurse-locale` | `assets/fonts/`, `assets/images/`, `include/ResourceManager.hpp`, `src/ResourceManager.cpp` | -| `tests/gtest` | googletest in `tests/`, `include/BankAccount.h`, `src/exemplu_test_oop/` | -| `tests/Boost-ext-ut` | `ext/include/boost/ut.hpp`, `include/BankAccount.h`, `src/exemplu_test_oop/` | +Layout — shared and per branch — lives in [BRANCHES.md](BRANCHES.md), which is also where you find out +which stack your tree is. This file is only what does not depend on the branch: what is generated, and what +is not in your repo at all. ## Generated — never commit @@ -46,8 +13,8 @@ output (models, exports, downloads), add it to `.gitignore` too — but **never* ## Not in your repo Maintainer-only material (the private maintainer guide, the grading/automation setup). What *is* in -your repo and must stay untouched: `.github/`, `cmake/`, `scripts/`, the config files above. See -[INFRA.md](INFRA.md). +your repo and must stay untouched: `.github/`, `cmake/`, `scripts/`, the config files listed in +[BRANCHES.md](BRANCHES.md). See [INFRA.md](INFRA.md) for why, and for what you may add. ## Misc diff --git a/.internal/mistakes.md b/.internal/mistakes.md index bdc1edc8..5a3a737c 100644 --- a/.internal/mistakes.md +++ b/.internal/mistakes.md @@ -27,7 +27,7 @@ Severity: 🔴 the requirement counts as **not met** · 🟠 **costs points** · | 🔴 No filler — **fără cod de umplutură/fără sens!** | Members that nothing calls (or that are "called" somewhere only to look used) are noise: they tell me nothing was learned. | Write a member when the scenario needs it. Generated code you did not contribute to earns at most bonus points, and only if it genuinely helps. | | 🔴 No copy-paste — **fără copy-paste!** | Duplicated blocks double every future bug and are the clearest sign the design was not thought through. | Pull the shared part into a function, a template, or a base class. | | 🔴 No data in the code — **fără date în cod!** | A project that hardcodes its dataset is not a program, it is a transcript with `if`s. | Read from your own data files as csv/json/other well-known format; do not hand-roll your own parser, use dedicated libraries (you have examples on other branches). And read from a library/DB where the topic requires it. `tastatura.txt` is for keyboard input only. | -| 🔴 `.gitattributes` set correctly (most likely no-op) | Linguist decides what counts as C++: ≥52–60% (T1), 75–78% (T2), 80–90% (T3). Wrong settings will put a bad project under even stricter checks. | Check the language bar on GitHub after your first real commit, not after the last one. Tests do not count towards this limit. | +| 🔴 `.gitattributes` set correctly | Linguist decides what counts as C++: ≥52–60% (T1), 75–78% (T2), 80–90% (T3). Wrong settings will put a bad project under even stricter checks. | Check the language bar on GitHub after your first real commit, not after the last one. You only touch this file when you add a directory of generated or vendored code; tests do not count towards this limit. | | 🔴 CI green, **all** checkboxes ticked, a git tag per homework | CI is the entry ticket; an unticked box or a missing tag is a purely administrative way to lose chances to enter the exam. | Budget time for CI round-trips; see [section 6](#6-checking-your-own-work). | | 🔴 No OS-specific headers (`conio.h`, `windows.h`, …) | CI builds on more than one OS; these headers break it and make the code non-portable. | Standard I/O only. Ask if you really think you need one. Portable alternatives are provided in branches. | | 🔴 Source files must be in the repo | An empty or partially committed repo has nothing to grade. | Check `git status --ignored` once in a while: `build/` and `install_dir/` belong in `.gitignore`, your sources do not. | @@ -103,13 +103,29 @@ This is the part no script can grade for you, and the part the oral is about. * 🟠 A tag per homework/stage (`v0.1`, `v0.2`, `v0.3`) with optional patch versions, placed on the commit where everything for that part is done. * 🟠 External libraries, datasets and resources not cited in the **Resurse** section of the README. +* 🟠 **A dependency you prepared but never use.** Presence of the setup is evidence of the **stack**, never + of usage. *Used* means all of: a header of it is `#include`d by your code, a symbol from it is called + somewhere in the project's story (not one call added to look used — that is filler, the mirror + mistake), it is linked into the target that needs it, `PUBLIC` only when a header in `include/` names its + type, and the **Resurse** entry cites version + licence. Until then it is downloaded and built on every + CI run for nothing, so deleting a dependency your branch ships is a legitimate move — do it before the + push. `scripts/audit_ext_libs.sh` prints a `warning` for a dependency no student `#include` reaches: + **advisory, it fails nothing**. A dependency reached without an include (linked for its symbols, a + runtime, a header your build generates) is declared beside the dependency, with + `set(AUDIT_EXT_LIBS_ALLOW "" CACHE STRING "why")` — it has to be `CACHE`, a plain + `set()` is invisible to the script. That line is an addition to your own `CMakeLists.txt`, so the freeze + in [INFRA.md](INFRA.md) does not forbid it. Included **only** from `tests/` is a note about design, not a + finding: test frameworks live there. * ⚪ Everything in one giant commit. Commit per feature, so a history exists to review and code review #1–#3 has something to discuss. * 🟠 **A test that hangs or reads `std::cin`.** CTest kills it after 60 s, but the ASan, Valgrind and smoke steps run the same test binary directly, with **no timeout**, and eat the whole job. Tests take data from fixtures, never from `std::cin`; at EOF a "read until the input is valid" loop spins forever. * 🟠 **A test with no assertion.** It runs code, it verifies nothing. * 🟠 **Making a member public so it can be tested.** Widen nothing for a check — test through the public interface. * 🔴 **`tests/` written to raise the C++ share.** The percentage is measured without `tests/`, and filler is penalised anyway. -* ⚪ **Library code whose only effect is `std::cout`.** Catalog C2, visible now that the library is separate from `app/`. -* 🟠 **A vendored type in a public header that could have stayed private.** Parse in the `.cpp`, hand out std types, keep that dep `SYSTEM PRIVATE`. +* ⚪ **Library code whose only effect is `std::cout`.** Visible now that the library is separate from `app/`. +* 🟠 **A vendored type in a public header.** Ask "could this have stayed private *without* duplicating + the code?", not "never": on the GUI branches the public vendored type is deliberate, the alternative is a + copy of the resource handling. Where it is not: parse in the `.cpp`, hand out std types, keep that dep + `SYSTEM PRIVATE`. ## 6. Checking your own work @@ -119,16 +135,29 @@ The same things CI runs, you can run locally — see the [README](../README.md) ./scripts/cmake.sh configure # add -e "-DUSE_ASAN=ON" for AddressSanitizer ./scripts/cmake.sh build ./scripts/run_cppcheck.sh +./scripts/run_tests.sh -r # or: ./scripts/cmake.sh test ./scripts/run_valgrind.sh clang-tidy -p build src/*.cpp app/*.cpp tests/*.cpp # uses the repo's .clang-tidy ``` +Those exist on every branch; what yours adds is in [BRANCHES.md](BRANCHES.md). `run_ui_test.sh` and +`run_sanitizers.sh` are called by CI, not by you (the first needs `xvfb`/`xdotool`, the second expects the +unpacked artifact bundle as its working directory); on the GUI branches `run_valgrind.sh` drives the window +with `xdotool`, so run it where a display exists. + Practical notes: * Fix **all** the independent CI failures in one push; each round-trip is minutes of waiting. * Keep a recent green commit on your default branch and re-run a workflow from time to time — the CI dependency cache expires, and pushing on a cold cache after weeks of silence is slow. -* A green CI means your code compiles and passes the mechanical checks. It is the **minimum**, not a proof of design. +* **Red is the only certainty.** A red row means something is wrong; a green row only means the steps that + ran did not fail. On the GUI branches no row can fail because of your program, so run ASan and Valgrind + yourself and read the logs of the steps that ran. CI is the **minimum**, never proof of memory safety or + of design. + +Some limitations of the template are known and left as they are for lack of time — the advisory runtime +rows on the GUI branches, and external libraries that do not cooperate with sanitizers on some OSes. Ask +before working around one; contributions through the template repo are welcome. ## 7. Beyond CI diff --git a/AGENTS.md b/AGENTS.md index 25b84048..b74d0dda 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,14 +16,45 @@ You are working inside a **student's project** of this course (*Programare Orien --- +## First action in a repo: work out which template stack this tree is + +Then read that row in [`.internal/BRANCHES.md`](.internal/BRANCHES.md). The **branch name is a hint, +not evidence**: a fork defaults to `main` or `master`, and a student may port a feature from +another branch — it is legitimate. Detect from the tree, best match wins, strongest signal first: + +1. files in `scripts/`, 2. what is vendored in `ext/`, +3. `FetchContent_Declare` names and `target_link_libraries` **in the `CMakeLists.txt` that declares the +target** (not in a comment in some other file), +4. the framework in `tests/CMakeLists.txt`, and whether `app/CMakeLists.txt` / `src/CMakeLists.txt` exist +at all, 5. marker files (`.env.example`, a starter header the branch ships). + +A partial match is the normal case: starter code deleted, a dependency added. Say which signals matched +and why that row. On a tie, ask one question ("ai mutat sau adăugat ceva peste ce venea din branch-ul +`X`?") instead of assuming, and never tell a student their branch "does not exist". Reconciling branch +differences is not yours to do. A declared `FetchContent`/linked dependency identifies the **stack**, never +proves the student **uses** it ([mistakes.md](.internal/mistakes.md)). The point is to help the student +with using the proper setup, which has been validated. + +--- + ## Always in force (quoted from the [README](README.md)) * **Fără cod de umplutură!** · **Fără copy-paste!** · **Fără variabile globale!** · **Fără atribute publice!** -* **Fără date în cod** (T2, T3) — data is read from files; `tastatura.txt` holds keyboard input only. -* `.gitattributes` set so the required C++ share is counted; CI green with **all** checkboxes ticked; - a git tag per theme. +* **Fără date în cod** — data is read from files; `tastatura.txt` holds comprehensive keyboard input only. +* `.gitattributes` set so the required C++ share is counted, and student code should be more than infra code: + infra code is intentionally not excluded by `.gitattributes`; CI green with **all** checkboxes ticked; + a git tag per homework. * A requirement is **not met** if it is satisfied only by code the student did not meaningfully write - or cannot explain. Untested AI filler earns at most token bonus points, and only if it truly helps. + or cannot explain. Untested AI filler does not earn any points. + +## CI: red is the only certainty + +A red row means something is definitely wrong. **A green row says nothing about the checks that did not +run** — on the GUI branches the ASan/Valgrind rows are advisory and no row launches the app, so "CI e +verde ⇒ nu ai leaks / e testat" is always a false claim. Read the logs of the steps that did run, filtering +noise from external libs in valgrind output. Disabled +matrix rows are intentional (3 OSes × 2 compilers is the floor); asking the teacher about one is fine, +commenting it is not. Details per branch: [BRANCHES.md](.internal/BRANCHES.md). ## `.internal/mistakes.md` — load on demand, do not keep it in context @@ -64,12 +95,25 @@ explanation → guiding question → review of *their* code → a non-pasteable * **Yours:** `include/` (the library's public interface), `src/` (its implementation, `src/internal/` is not public), `app/` (the executable: `main.cpp`, menus, wiring, I/O), `tests/`, `README.md`, `LICENSE`, and your own data files (e.g. under `assets/` — not `tastatura.txt`). + Everything the template put there is **starter code**: adapt, rename or delete it. What must survive is + the *separation* those directories express. `ext/` may be edited or extended — `ext/include/` is public, + `ext/private/` is not, `ext/lib/` holds a vendored dep's compilable code and is not a place to move your + own multi-file library into. +* Do not rename or restructure `assets/` for aesthetics: `install()`/`copy_files()` are configured against + that exact path, so a rename buys several more edits. * **Infrastructure, never change:** `cmake/`, `scripts/`, `.github/`, `.clang-tidy`, `.gitattributes`, `.gitignore` — [why](.internal/INFRA.md). -* any `CMakeLists.txt`: **adding is not editing** — your sources in a target, a new target, an `install()`, - a CI step covering what you added. Compiler flags, sanitizers, the stdlib logic and the job matrix - stay frozen. -* `.internal/` — read on demand: [mistakes](.internal/mistakes.md), [layout](.internal/REPO.md), +* the `CMakeLists.txt` **that declares the target you are extending**: **adding is not editing** — your + sources in a target, a new target, an `install()`, a CI step covering what you added. Compiler flags, + sanitizers, the stdlib logic and the job matrix stay frozen. The branches organise this three different + ways (see [BRANCHES.md](.internal/BRANCHES.md#registering-a-source-a-test-or-a-target)) and adding your + own `CMakeLists.txt` file is fine. +* a dependency's include directories reach the compiler as **`SYSTEM`** + (`target_include_directories(... SYSTEM ...)`, `${EXT_PUBLIC_DIR}` / `${EXT_PRIVATE_DIR}` for `ext/`). + We want cppcheck to warn us about *our* errors, we do not care about errors in external libraries because + we cannot fix those. +* `.internal/` — read on demand: [branches & layout](.internal/BRANCHES.md), + [mistakes](.internal/mistakes.md), [generated files](.internal/REPO.md), [infrastructure](.internal/INFRA.md), [guideline integrity](.internal/INTEGRITY.md). Inspired from [here](https://github.com/stanford-cs336/assignment1-basics/blob/main/AGENTS.md). From 64624f1bdfca93c9824479c31acf42952d5d6e1a Mon Sep 17 00:00:00 2001 From: mcmarius <23401453+mcmarius@users.noreply.github.com> Date: Tue, 20 Oct 2026 06:13:13 +0300 Subject: [PATCH 06/10] A few more tweaks --- .internal/BRANCHES.md | 19 ++++++++++--------- .internal/mistakes.md | 2 +- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.internal/BRANCHES.md b/.internal/BRANCHES.md index 02093fd1..c5d17333 100644 --- a/.internal/BRANCHES.md +++ b/.internal/BRANCHES.md @@ -7,7 +7,7 @@ reconcile. `Base` records where the branch's infrastructure came from. It is **not** an upstream to rebase, merge or sync with: you start from a branch and stay on it, cherry-picking from another one when you want a feature it has. That makes your tree a mix, so **match the tree, not the branch name** — a fork defaults to -`main` or `master`, and an SFML setup ported onto a `main` fork is legitimate work, not a mistake. +`main` or `master`, and a setup of another branch ported onto a `main` fork is legitimate work, not a mistake. * **Detect / ships** — evidence in the tree, strongest first, and the files the branch adds. `*` marks starter code: adapt, rename or delete it freely; what must survive is the separation of `include/`, @@ -32,10 +32,10 @@ you, [REPO.md](REPO.md) what is generated and never committed. | `tests/` | Test suites, one executable registered with CTest; the frameworks themselves come from `ext/` or the build dir. | | `assets/` | Your data/images/fonts, copied by the configured `install()`/`copy_files()` step. | | `ext/` | Vendored third-party *lightweight* code, marked `linguist-vendored` in `.gitattributes`. `ext/include/` is on your public interface (`SYSTEM PUBLIC`), `ext/private/` stays inside `src/`, `ext/lib/` holds a vendored dep's compilable code. | -| `tastatura.txt` | Keyboard input only (`std::cin`); file data goes in your own files under `assets/`. | +| `tastatura.txt` | Comprehensive keyboard input only (`std::cin`); file data goes in your own files under `assets/`. | | `README.md` | Project description + the homework checkboxes. Yours to edit. | | `LICENSE`, `LICENSE.template` | AGPLv3 for your code / Unlicense for the template. You can edit the one for your code. | -| `launcher.command` | Double-click launcher template for the built `./oop_main` binary. Do not edit. Infrastructure. | +| `launcher.command` | Double-click launcher template for the built `./oop_main` binary for macOS. Do not edit. Infrastructure. | ## Branches @@ -58,14 +58,15 @@ you, [REPO.md](REPO.md) what is generated and never committed. ## Different on purpose -Every branch rewrites the same two README regions for its own stack (the homework checkbox block and -`1. Pasul de configurare`), and `Resurse` belongs to the student. That is by design: never copy a README +Every branch rewrites the README for its own stack (`1. Pasul de configurare`), +and `Resurse` belongs to the student. That is by design: never copy a README region from another branch or from `main`, and never "align" two branches. ## Registering a source, a test or a target Three shapes exist and all three are correct on their branch. Adding your own `CMakeLists.txt` is fine -too — the freeze is on deleting and changing, not on adding ([INFRA.md](INFRA.md)). +too — the freeze is on deleting and changing, not on adding ([INFRA.md](INFRA.md)). By addition, +we mean net genuine additions, not "additions" that modify or delete in disguise. * **Root `CMakeLists.txt`** (every branch but the two below): sources listed in `add_library` / `add_executable` / `target_sources`, tests in `tests/CMakeLists.txt`. @@ -75,7 +76,7 @@ too — the freeze is on deleting and changing, not on adding ([INFRA.md](INFRA. targets, `oop` and the uninstrumented `oop_nslib`. Lists are registered directly even where a variable would read better because IDEs write into -`add_executable` regardless; pick the shape your branch has and do not switch it for aesthetics. +`add_executable` regardless; pick the shape your branch has and do not switch it without a strong reason. ## Reading CI @@ -89,8 +90,8 @@ Lists are registered directly even where a variable would read better because ID because of your program**: the sanitizer and Valgrind output is in the step logs and you are expected to read it, locally too (`./scripts/run_valgrind.sh`, an ASan configure). * Commented matrix rows and advisory steps are **intentional**: the template keeps as many checks as it can - and drops only what is genuinely incompatible, with 3 OSes × 2 compilers as the floor. Re-enabling, - deleting or reordering a row is a forbidden edit — if a row looks broken, ask the teacher. + and drops only what is genuinely incompatible, with 3 OSes × 2 compilers as the floor. + Deleting or changing a row is a forbidden edit — if a row looks broken, ask the teacher. * Some external libraries do not play nice with sanitizers on some OSes; that is why `tests/Boost-ext-ut` compiles an uninstrumented twin of the library for the test binary. * `scripts/run_ui_test.sh` (sends keystrokes with `xdotool` to the window named by `APP_WINDOW`, which must diff --git a/.internal/mistakes.md b/.internal/mistakes.md index 5a3a737c..c879d25b 100644 --- a/.internal/mistakes.md +++ b/.internal/mistakes.md @@ -27,7 +27,7 @@ Severity: 🔴 the requirement counts as **not met** · 🟠 **costs points** · | 🔴 No filler — **fără cod de umplutură/fără sens!** | Members that nothing calls (or that are "called" somewhere only to look used) are noise: they tell me nothing was learned. | Write a member when the scenario needs it. Generated code you did not contribute to earns at most bonus points, and only if it genuinely helps. | | 🔴 No copy-paste — **fără copy-paste!** | Duplicated blocks double every future bug and are the clearest sign the design was not thought through. | Pull the shared part into a function, a template, or a base class. | | 🔴 No data in the code — **fără date în cod!** | A project that hardcodes its dataset is not a program, it is a transcript with `if`s. | Read from your own data files as csv/json/other well-known format; do not hand-roll your own parser, use dedicated libraries (you have examples on other branches). And read from a library/DB where the topic requires it. `tastatura.txt` is for keyboard input only. | -| 🔴 `.gitattributes` set correctly | Linguist decides what counts as C++: ≥52–60% (T1), 75–78% (T2), 80–90% (T3). Wrong settings will put a bad project under even stricter checks. | Check the language bar on GitHub after your first real commit, not after the last one. You only touch this file when you add a directory of generated or vendored code; tests do not count towards this limit. | +| 🔴 `.gitattributes` set correctly | Linguist decides what counts as C++: ≥52–60% (T1), 75–78% (T2), 80–90% (T3). Wrong settings will put a bad project under even stricter checks. | Check the language bar on GitHub after your first real commit, not after the last one. Ask before you intend to edit this, you most likely do not need to; tests do not count towards this limit. | | 🔴 CI green, **all** checkboxes ticked, a git tag per homework | CI is the entry ticket; an unticked box or a missing tag is a purely administrative way to lose chances to enter the exam. | Budget time for CI round-trips; see [section 6](#6-checking-your-own-work). | | 🔴 No OS-specific headers (`conio.h`, `windows.h`, …) | CI builds on more than one OS; these headers break it and make the code non-portable. | Standard I/O only. Ask if you really think you need one. Portable alternatives are provided in branches. | | 🔴 Source files must be in the repo | An empty or partially committed repo has nothing to grade. | Check `git status --ignored` once in a while: `build/` and `install_dir/` belong in `.gitignore`, your sources do not. | From 4b73ad73bd9dbb223dcd3a58b8409c052dde3d29 Mon Sep 17 00:00:00 2001 From: mcmarius <23401453+mcmarius@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:37:44 +0300 Subject: [PATCH 07/10] More consistency checks --- .internal/BRANCHES.md | 11 ++++++----- .internal/mistakes.md | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.internal/BRANCHES.md b/.internal/BRANCHES.md index c5d17333..92c58394 100644 --- a/.internal/BRANCHES.md +++ b/.internal/BRANCHES.md @@ -41,6 +41,7 @@ you, [REPO.md](REPO.md) what is generated and never committed. | Branch | Base | Detect / ships | Extra `scripts/` | CI | |---|---|---|---|---| +| `main` | N/A | starter branch with no dependencies | N/A | A | | `baze-de-date/pqxx` | `main` | `include/Database.h`\*, `src/database/Database.cpp`\*, `cmake/MinGWLibpq.cmake`, `.env.example`, `find_package(PostgreSQL)`, `PQXX_VERSION` | `configure_postgres.sh` — creates database, user and `pgcrypto`; CI runs it on every OS | A, but **no ASan row on Windows MSVC** | | `baze-de-date/sqlite` | `main` | `FetchContent` `sqlite3`, `add_library(sqlite3 STATIC …/sqlite3.c)` | — | A | | `common-libs` | `main` | `ext/include/{date,csv-parser}` (public), `ext/private/{digestpp,random,rlutil}`, `assets/date.csv`\* | — | A | @@ -48,13 +49,13 @@ you, [REPO.md](REPO.md) what is generated and never committed. | `ftxui` | `main` | `FetchContent` `ftxui`, links `ftxui::screen` / `::dom` / `::component` | — | A | | `http-server` | `main` | `ext/include/httplib.h` + `ext/lib/httplib.cc` (library target `httplib`), `app/main-client.cpp`\*, target `${MAIN_EXECUTABLE_NAME}_client` | — | A; the runtime rows start the server in the background, and Valgrind analyses `oop_main_client` + `oop_test` | | `llms` | `descarcare-date-api` | `FetchContent` `cpr numkong`, `NUMKONG_VERSION`, `ext/include/json/` | `run_llm.sh` — fetches and starts a llamafile; CI sources it before every runtime row | A; macOS-GCC row commented | -| `raylib-cpp` | `sfml3` | `FetchContent` `raylib raylib-cpp`, `target_link_libraries(oop PUBLIC raylib)` | GUI set (see `sfml3`) | B; macOS-GCC row commented | -| `sfml3` | `main` | `FetchContent` `SFML`, links `SFML::Graphics`, window title `"My Window"`\* | `install-linux-deps.sh` (dev packages per distro family), `run_sanitizers.sh`, `run_ui_test.sh` | B; macOS-GCC row commented | -| `sfml3-imagini-externe-api` | `sfml3` | `FetchContent` `SFML cpr` — images fetched over HTTP | GUI set (see `sfml3`) | B; macOS-GCC row commented | -| `sfml3-resurse-locale` | `sfml3` | `include/ResourceManager.hpp`\*, `src/ResourceManager.cpp`\*, `assets/fonts/`\*, `assets/images/`\*, `target_link_libraries(oop PUBLIC SFML::…)` | GUI set (see `sfml3`) | B; macOS-GCC row commented | +| `raylib-cpp` | `sfml3` | `FetchContent` `raylib raylib-cpp`, `target_link_libraries(oop PUBLIC raylib)` | GUI set (see `sfml3`) | B; see `sfml3` | +| `sfml3` | `main` | `FetchContent` `SFML`, links `SFML::Graphics`, window title `"My Window"`\* | `install-linux-deps.sh` (dev packages per distro family), `run_sanitizers.sh`, `run_ui_test.sh` | B; macOS-GCC row commented, macOS 15 instead of 26, Clang 17 instead of 21 | +| `sfml3-imagini-externe-api` | `sfml3` | `FetchContent` `SFML cpr` — images fetched over HTTP | GUI set (see `sfml3`) | B; see `sfml3` | +| `sfml3-resurse-locale` | `sfml3` | `include/ResourceManager.hpp`\*, `src/ResourceManager.cpp`\*, `assets/fonts/`\*, `assets/images/`\*, `target_link_libraries(oop PUBLIC SFML::…)` | GUI set (see `sfml3`) | B; see `sfml3` | | `tests/Boost-ext-ut` | `main` | `ext/include/boost/ut.hpp`, `MAIN_LIBRARY_SOURCES`, second library target `oop_nslib` | — | A; the tests and `oop_nslib` are built **without** sanitizers | | `tests/gtest` | `main` | `app/CMakeLists.txt` and `src/CMakeLists.txt` exist, `FetchContent` `gtest` inside `tests/CMakeLists.txt`, `include/BankAccount.h`\* | — | A; googletest is fetched into the build dir, never committed | -| `wxwidgets` | `sfml3` | `FetchContent` `wx`, `target_link_libraries(oop PUBLIC wx::core)` — GUI plumbing only, no SFML in the tree | GUI set (see `sfml3`) | B; macOS-GCC row commented | +| `wxwidgets` | `sfml3` | `FetchContent` `wx`, `target_link_libraries(oop PUBLIC wx::core)` — GUI plumbing only, no SFML in the tree | GUI set (see `sfml3`) | B; see `sfml3` | ## Different on purpose diff --git a/.internal/mistakes.md b/.internal/mistakes.md index c879d25b..520eeabc 100644 --- a/.internal/mistakes.md +++ b/.internal/mistakes.md @@ -24,7 +24,7 @@ Severity: 🔴 the requirement counts as **not met** · 🟠 **costs points** · |---|---|---| | 🔴 No public data members — **fără atribute publice!** | The object's invariants can be broken from anywhere, so the class has no behaviour to speak of. | Private members, public *operations*. A getter is not an operation; `private:` is the default in `class`. | | 🔴 No global / file-scope variables — **fără variabile globale!** | Hidden coupling; state that no constructor, test or `main` controls. | Pass what you need; make it a member or a parameter. `static const`/`constexpr` constants are fine. | -| 🔴 No filler — **fără cod de umplutură/fără sens!** | Members that nothing calls (or that are "called" somewhere only to look used) are noise: they tell me nothing was learned. | Write a member when the scenario needs it. Generated code you did not contribute to earns at most bonus points, and only if it genuinely helps. | +| 🔴 No filler — **fără cod de umplutură/fără sens!** | Members that nothing calls (or that are "called" somewhere only to look used) are noise: they tell me nothing was learned. | Write a member when the scenario needs it. Forced scenarios do not count. Generated code you did not contribute to earns no points. | | 🔴 No copy-paste — **fără copy-paste!** | Duplicated blocks double every future bug and are the clearest sign the design was not thought through. | Pull the shared part into a function, a template, or a base class. | | 🔴 No data in the code — **fără date în cod!** | A project that hardcodes its dataset is not a program, it is a transcript with `if`s. | Read from your own data files as csv/json/other well-known format; do not hand-roll your own parser, use dedicated libraries (you have examples on other branches). And read from a library/DB where the topic requires it. `tastatura.txt` is for keyboard input only. | | 🔴 `.gitattributes` set correctly | Linguist decides what counts as C++: ≥52–60% (T1), 75–78% (T2), 80–90% (T3). Wrong settings will put a bad project under even stricter checks. | Check the language bar on GitHub after your first real commit, not after the last one. Ask before you intend to edit this, you most likely do not need to; tests do not count towards this limit. | @@ -109,13 +109,17 @@ This is the part no script can grade for you, and the part the oral is about. mistake), it is linked into the target that needs it, `PUBLIC` only when a header in `include/` names its type, and the **Resurse** entry cites version + licence. Until then it is downloaded and built on every CI run for nothing, so deleting a dependency your branch ships is a legitimate move — do it before the - push. `scripts/audit_ext_libs.sh` prints a `warning` for a dependency no student `#include` reaches: - **advisory, it fails nothing**. A dependency reached without an include (linked for its symbols, a - runtime, a header your build generates) is declared beside the dependency, with + push. `scripts/audit_ext_libs.sh` prints a `warning` for a dependency no student `#include` reaches. + A dependency reached without an include (linked for its symbols, a runtime, a header your build generates) + is declared beside the dependency, with `set(AUDIT_EXT_LIBS_ALLOW "" CACHE STRING "why")` — it has to be `CACHE`, a plain `set()` is invisible to the script. That line is an addition to your own `CMakeLists.txt`, so the freeze in [INFRA.md](INFRA.md) does not forbid it. Included **only** from `tests/` is a note about design, not a finding: test frameworks live there. + This step fails if you include an ext lib as non-SYSTEM dep. Why? Mis-configuration: we want to know about + errors in our own code, but we need to skip external code (so external needs to be SYSTEM). The reverse is + also a mistake: marking our own code as SYSTEM puts errors under the rug and then we find issues at runtime + instead of compile time. * ⚪ Everything in one giant commit. Commit per feature, so a history exists to review and code review #1–#3 has something to discuss. * 🟠 **A test that hangs or reads `std::cin`.** CTest kills it after 60 s, but the ASan, Valgrind and smoke steps run the same test binary directly, with **no timeout**, and eat the whole job. Tests take data from fixtures, never from `std::cin`; at EOF a "read until the input is valid" loop spins forever. * 🟠 **A test with no assertion.** It runs code, it verifies nothing. @@ -140,9 +144,10 @@ The same things CI runs, you can run locally — see the [README](../README.md) clang-tidy -p build src/*.cpp app/*.cpp tests/*.cpp # uses the repo's .clang-tidy ``` -Those exist on every branch; what yours adds is in [BRANCHES.md](BRANCHES.md). `run_ui_test.sh` and -`run_sanitizers.sh` are called by CI, not by you (the first needs `xvfb`/`xdotool`, the second expects the -unpacked artifact bundle as its working directory); on the GUI branches `run_valgrind.sh` drives the window +Those exist on every branch; what yours adds is in [BRANCHES.md](BRANCHES.md). On GUI branches, +there are also `run_ui_test.sh` and `run_sanitizers.sh` which are called by CI; to use them locally you need +more setup and the sanitizers work only on Linux (the first needs `xvfb`/`xdotool`, the second expects the +unpacked artifact bundle as its working directory); on GUI branches `run_valgrind.sh` drives the window with `xdotool`, so run it where a display exists. Practical notes: From 2c46e697bfeed445d1cd863627af271b62c1d92b Mon Sep 17 00:00:00 2001 From: mcmarius <23401453+mcmarius@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:37:55 +0300 Subject: [PATCH 08/10] More fixes in audit libs script --- scripts/audit_ext_libs.sh | 206 ++++++++++++++++++++++++++++++-------- 1 file changed, 167 insertions(+), 39 deletions(-) diff --git a/scripts/audit_ext_libs.sh b/scripts/audit_ext_libs.sh index db7447ec..bcdd8157 100755 --- a/scripts/audit_ext_libs.sh +++ b/scripts/audit_ext_libs.sh @@ -9,7 +9,14 @@ # 2. a dependency no student source includes -> warning only: it may be linked for its symbols, be a # runtime, or generate its headers. Declare it beside the dependency, with CACHE so this reads it: # set(AUDIT_EXT_LIBS_ALLOW "" CACHE STRING "linked, not included") -# Included only from tests/ is a note: for a test framework that is the design. +# Included only from tests/ is a note: for a test framework that is the design. Reported only for a +# dependency this project names in its own build files: one that another dependency fetched (zlib +# below curl, the googletest umbrella above gtest) is not the student to drop, and warning about it +# teaches students to ignore the audit. Vendored code below ext/ is theirs, whoever copied it in. +# +# Dependencies are discovered from CMakeCache.txt plus the FetchContent trees: the base directory, or +# its default /_deps. The cache alone is not enough, since FetchContent_Populate followed by a +# hand-written target (baze-de-date/sqlite) leaves no _SOURCE_DIR entry behind. # # bash ./scripts/audit_ext_libs.sh [-b build dir] [-v] (-v lists every dependency) # env: BUILD_DIR, EXT_DIR, TESTS_DIR, AUDIT_EXT_LIBS_ALLOW (exceptions for this run only) @@ -19,37 +26,109 @@ TESTS_DIR="${TESTS_DIR:-tests}" VERBOSE_AUDIT="" DEP_ROOTS=() # directories holding dependency code, umbrellas included DEP_UNITS=() # "", one per dependency +FETCH_ROOTS=() # directories FetchContent unpacked into, parents of -src +OWN_CMAKE=() # CMake files of this repository, the ones naming the dependencies it chose ALLOW_FROM_CACHE="" +TAB=$'\t' # unit separator; a literal tab here would be one editor away from silence +# Absolute, without a trailing slash and without a . component: a path kept as ./build/_deps/x fails +# every prefix comparison below it, and the build directory then passes for repository code. abs_path() { - case "$1" in - /*|[A-Za-z]:[\\/]*) printf '%s' "${1%/}" ;; - *) printf '%s' "${PWD%/}/${1%/}" ;; + local path="$1" + while [[ "${path}" == ./* ]]; do path="${path#./}"; done + case "${path}" in + /*|[A-Za-z]:[\\/]*) printf '%s' "${path%/}" ;; + *) printf '%s' "${PWD%/}/${path%/}" ;; + esac +} + +add_fetch_root() { # FetchContent unpacks here; the cached base is often the default already + local root existing + root="$(abs_path "$1")" + for existing in "${FETCH_ROOTS[@]}"; do + [[ "${existing}" == "${root}" ]] && return 0 + done + FETCH_ROOTS+=("${root}") +} + +under_dep_root() { # below one of the dependency roots + local root + for root in "${DEP_ROOTS[@]}"; do + [[ "${1}" == "${root%/}"/* ]] && return 0 + done + return 1 +} + +has_header() { # a header here or below it, so this is dependency code and not a note directory + local path="$1" + if [[ -d "${path}" ]]; then + find "${path}" -type f \ + \( -name '*.h' -o -name '*.hpp' -o -name '*.hxx' -o -name '*.hh' \) \ + -print -quit 2>/dev/null | grep -q . + return + fi + case "${path}" in + *.h|*.hpp|*.hxx|*.hh) return 0 ;; + *) return 1 ;; esac } +# FetchContent unpacks every dependency as -src next to -build, below the base directory. +# The cache names the ones pulled in with add_subdirectory; this adds the ones a hand-written target +# built from a bare FetchContent_Populate, which leave no name in the cache at all. +fetch_units_under() { # a FetchContent base directory + local dir="$1" entry name + [[ -d "${dir}" ]] || return 0 + while IFS= read -r entry; do + name="${entry##*/}" + name="${name%-src}" + [[ -n "${name}" ]] || continue + has_header "${entry}" || continue + DEP_UNITS+=("${name}${TAB}$(abs_path "${entry}")") + done < <(find "${dir}" -mindepth 1 -maxdepth 1 -type d -name '*-src' 2>/dev/null) +} + +# The build files naming a dependency are what makes it the student choice. Dependency trees are not +# build files: curl declaring zlib is not this project declaring zlib. +read_own_cmake() { # + local build_dir_abs file + build_dir_abs="$(abs_path "$1")" + while IFS= read -r file; do + file="$(abs_path "${file}")" + [[ "${file}" != "${build_dir_abs}"/* ]] || continue + under_dep_root "${file}" && continue + OWN_CMAKE+=("${file}") + done < <(find "${PWD}" \( -name .git -o -name _deps \) -prune -o \ + \( -name 'CMakeLists.txt' -o -name '*.cmake' \) -print 2>/dev/null) +} + # CMakeCache.txt split into the two views above. Our own project is not a dependency, however -# inviting _SOURCE_DIR looks. ext/ names no dependency, so its include directories are -# reported per child, which points at the vendored library nobody includes; ext/lib is not a unit, -# no #include reaches it. +# inviting _SOURCE_DIR looks. An empty value means unset: never let it become the repository. +# ext/ names no dependency, so its include directories are reported per child, which points at the +# vendored library nobody includes; ext/lib is not a unit, no #include reaches it. read_cmake_deps() { local build_dir="$1" cache="${build_dir}/CMakeCache.txt" - local line key name value dir child base project="" + local line key name value dir child project="" local entries=() - [[ -f "${cache}" ]] || return 0 - while IFS= read -r line || [[ -n "${line}" ]]; do - key="${line%%:*}" - value="${line#*=}" - [[ "${key}" != "${line}" && "${value}" != "${line}" ]] || continue - case "${key}" in - CMAKE_PROJECT_NAME) project="${value}" ;; - AUDIT_EXT_LIBS_ALLOW) ALLOW_FROM_CACHE="${value}" ;; - FETCHCONTENT_BASE_DIR) DEP_ROOTS+=("$(abs_path "${value}")") ;; - *_SOURCE_DIR) entries+=("${key%_SOURCE_DIR}=${value}") ;; - *_BINARY_DIR) entries+=("${key%_BINARY_DIR}=${value}") ;; - esac - done < "${cache}" + add_fetch_root "${build_dir}/_deps" + + if [[ -f "${cache}" ]]; then + while IFS= read -r line || [[ -n "${line}" ]]; do + key="${line%%:*}" + value="${line#*=}" + [[ "${key}" != "${line}" && "${value}" != "${line}" ]] || continue + case "${key}" in + CMAKE_PROJECT_NAME) project="${value}" ;; + AUDIT_EXT_LIBS_ALLOW) ALLOW_FROM_CACHE="${value}" ;; + FETCHCONTENT_BASE_DIR) + [[ -n "${value}" ]] || value="${build_dir}/_deps" + add_fetch_root "${value}" ;; + *_SOURCE_DIR) [[ -n "${value}" ]] && entries+=("${key%_SOURCE_DIR}=${value}") ;; + *_BINARY_DIR) [[ -n "${value}" ]] && entries+=("${key%_BINARY_DIR}=${value}") ;; + esac + done < "${cache}" + fi DEP_ROOTS+=("$(abs_path "${EXT_DIR:-ext}")") for line in "${entries[@]}"; do @@ -58,26 +137,25 @@ read_cmake_deps() { [[ "${name}" == "${project}" ]] && continue DEP_ROOTS+=("$(abs_path "${value}")") [[ -d "${value}" ]] || continue - DEP_UNITS+=("${name} ${value}") + DEP_UNITS+=("${name}${TAB}${value}") + done + + for dir in "${FETCH_ROOTS[@]}"; do + DEP_ROOTS+=("${dir}") + fetch_units_under "${dir}" done for dir in "${EXT_DIR:-ext}/include" "${EXT_DIR:-ext}/private"; do [[ -d "${dir}" ]] || continue while IFS= read -r child; do child="$(abs_path "${child}")" - base="${child##*/}" - if [[ -d "${child}" ]]; then - find "${child}" -type f \ - \( -name '*.h' -o -name '*.hpp' -o -name '*.hxx' -o -name '*.hh' \) \ - -print -quit 2>/dev/null | grep -q . || continue - else - [[ "${base}" == *.h || "${base}" == *.hpp || "${base}" == *.hxx || "${base}" == *.hh ]] \ - || continue - fi + has_header "${child}" || continue # name relative to the repository, directory as the compiler sees it - DEP_UNITS+=("${child#"${PWD}/"} ${child}") + DEP_UNITS+=("${child#"${PWD}/"}${TAB}${child}") done < <(find "${dir}" -mindepth 1 -maxdepth 1 2>/dev/null) done + + read_own_cmake "${build_dir}" } audit_build_tree() { @@ -90,6 +168,7 @@ audit_build_tree() { AUDIT_EXT_ROOT="$(abs_path "${EXT_DIR:-ext}")" \ AUDIT_ROOTS="$(printf '%s\n' "${DEP_ROOTS[@]}")" \ AUDIT_UNITS="$(printf '%s\n' "${DEP_UNITS[@]}")" \ + AUDIT_OWN_CMAKE="$(printf '%s\n' "${OWN_CMAKE[@]}")" \ AUDIT_ALLOW="${ALLOW_FROM_CACHE};${AUDIT_EXT_LIBS_ALLOW:-}" \ awk ' # norm() lowercases for comparison; never hand it to the filesystem, a real path is not lower @@ -128,9 +207,12 @@ audit_build_tree() { sub(/\/[^\/]*$/, "", dir) return dir } - # getline: 1 a line, 0 empty but present, -1 nothing to read; a directory reads as an error + # getline: 1 a line, 0 empty but present, -1 nothing to read; a directory reads as an error. + # A path scan() is reading must never be closed here: closing restarts it at the first line, so a + # header naming itself would be scanned forever and the audit would never reach the report. function exists(path, found, probe) { if (path in exists_memo) return exists_memo[path] + if (path in open_path) return 1 found = (getline probe < path) close(path) exists_memo[path] = (found >= 0) @@ -216,6 +298,7 @@ audit_build_tree() { if (product) scanned_product[path] = 1 scans++ base = dirname_of(path) + open_path[path] = 1 while ((getline line < path) > 0) { if (!match(line, /^[ \t]*#[ \t]*include[ \t]*[<"]/)) continue quoted = substr(line, RSTART + RLENGTH - 1, 1) == "\"" @@ -235,6 +318,7 @@ audit_build_tree() { if (is_own_file(norm(resolved)) || is_dep_dir(norm(resolved))) enqueue(resolved, product) } close(path) + delete open_path[path] } function note_usage(resolved, product, unit) { unit = unit_of(norm(resolved)) @@ -254,10 +338,28 @@ audit_build_tree() { } return unit } + function is_vendored(unit, i) { + for (i = 1; i <= unit_n[unit]; i++) + if (inside(unit_dir[unit, i], ext_root)) + return 1 + return 0 + } + # Whose dependency is it: a name absent from the build files of this repository was fetched by + # another dependency (zlib below curl, the googletest umbrella above gtest) or unpacked into a + # shared FETCHCONTENT_BASE_DIR by a different project. Nobody here can drop it, so reporting it + # would only teach students to silence the audit. ext/ is different: the student tree holds it. + # With no build file readable, fall back to reporting: quiet is worse than noisy here. + function is_ours(unit) { + if (is_vendored(unit)) return 1 + if (n_own_words == 0) return 1 + # unit is already a name_key (unit_order holds keys, never labels) and own_word is folded the + # same way; keying the label again would only invite a mismatch + return (unit in own_word) + } # a dependency inside another one is that dependency to add or drop, never ours; reporting it # would only teach students to silence the audit. Children of ext/ are ours, not parts of one. function is_nested(unit, name, i, j) { - if (inside(unit_dir[unit, 1], ext_root)) return 0 + if (is_vendored(unit)) return 0 for (name in unit_n) { if (name == unit) continue for (i = 1; i <= unit_n[unit]; i++) @@ -290,6 +392,18 @@ audit_build_tree() { out = out (i > 1 ? "\n " : " ") display(unit_raw[unit, i]) return out } + # words of the build files of this repository, folded like dependency names: the names in them are + # the dependencies this project chose, and the only ones it is asked about + function own_words(line, n, parts, i, word) { + n = split(line, parts, /[^A-Za-z0-9_]+/) + for (i = 1; i <= n; i++) { + word = name_key(parts[i]) + if (word != "" && !(word in own_word)) { + own_word[word] = 1 + n_own_words++ + } + } + } function audit_unused( i, file, unit, status) { if (n_units == 0) return # no dependency configured, nothing to say for (file in tu_kind) enqueue(tu_raw[file], tu_kind[file] == 1) @@ -305,8 +419,9 @@ audit_build_tree() { if (unit in used) status = (unit in used_product ? "used" : "tests only") else if (allowed_flag[unit]) status = "declared as used without including" else if (nested_flag[unit]) status = "part of another dependency" + else if (!ours_flag[unit]) status = "not named by this project" else status = "NOT INCLUDED" - printf " %-26s %s\n", unit, status + printf " %-26s %s\n", unit_label[unit], status } } for (i = 1; i <= n_unit_names; i++) { @@ -318,7 +433,7 @@ audit_build_tree() { relative_tests continue } - if (allowed_flag[unit] || nested_flag[unit]) continue + if (allowed_flag[unit] || nested_flag[unit] || !ours_flag[unit]) continue warn(sprintf("no student source includes anything from dependency %s", unit_label[unit])) print unit_dirs(unit) printf " downloaded and built on every CI run for nothing; drop the dependency, or\n" @@ -327,8 +442,8 @@ audit_build_tree() { unit_label[unit] } if (warnings) - printf "%d of %d configured dependencies are not included by any student source\n", \ - warnings, n_unit_names + printf "%d of the %d dependencies named by this project are not included by any student source\n", \ + warnings, ours_checked if (truncated) printf "note: the #include closure stopped after %d files; some dependencies may look\n" \ " worse than they are\n", max_files @@ -354,6 +469,7 @@ audit_build_tree() { if (lines[i] !~ /\t/) continue name = lines[i] dir = lines[i] + key_dir = "" sub(/\t.*$/, "", name) sub(/^[^\t]*\t/, "", dir) key = name_key(name) @@ -362,6 +478,9 @@ audit_build_tree() { unit_order[n_unit_names] = key unit_label[key] = name } + key_dir = key SUBSEP norm(dir) # cache and fetch tree name it twice + if (key_dir in seen_dir) continue + seen_dir[key_dir] = 1 unit_n[key]++ unit_dir[key, unit_n[key]] = norm(dir) unit_raw[key, unit_n[key]] = dir @@ -374,11 +493,20 @@ audit_build_tree() { allow[i] = (entry ~ /\//) ? norm(entry) : name_key(entry) } - # nested dependencies and declared exceptions, decided once, before any scanning + n_own_files = split(ENVIRON["AUDIT_OWN_CMAKE"], own_files, "\n") + for (i = 1; i <= n_own_files; i++) { + if (own_files[i] == "") continue + while ((getline own_line < own_files[i]) > 0) own_words(own_line) + close(own_files[i]) + } + + # nested dependencies, declared exceptions and whose dependency it is, decided before scanning for (i = 1; i <= n_unit_names; i++) { unit = unit_order[i] nested_flag[unit] = is_nested(unit) allowed_flag[unit] = is_allowed(unit) + ours_flag[unit] = is_ours(unit) + if (!nested_flag[unit] && !allowed_flag[unit] && ours_flag[unit]) ours_checked++ } errors = 0 warnings = 0 From 0f6978b53758d554035e5691db6e98b26efa77e7 Mon Sep 17 00:00:00 2001 From: mcmarius <23401453+mcmarius@users.noreply.github.com> Date: Tue, 20 Oct 2026 13:45:23 +0300 Subject: [PATCH 09/10] Even more inconsistency fixes --- .github/workflows/rebase-branches.yml | 1 + .internal/BRANCHES.md | 25 ++++++++++++++----------- .internal/mistakes.md | 21 ++++++++++++--------- AGENTS.md | 8 ++++---- README.md | 6 +++--- cmake/Options.cmake | 1 + scripts/audit_ext_libs.sh | 5 +++-- 7 files changed, 38 insertions(+), 29 deletions(-) diff --git a/.github/workflows/rebase-branches.yml b/.github/workflows/rebase-branches.yml index cb10ac50..cb596ad9 100644 --- a/.github/workflows/rebase-branches.yml +++ b/.github/workflows/rebase-branches.yml @@ -1,3 +1,4 @@ +# IGNORE THIS FILE, IT IS NOT USED OR MAINTAINED, NOBODY RUNS IT name: Automatic rebase branches on: diff --git a/.internal/BRANCHES.md b/.internal/BRANCHES.md index 92c58394..f46577ab 100644 --- a/.internal/BRANCHES.md +++ b/.internal/BRANCHES.md @@ -1,8 +1,8 @@ # Template branches — layout and per-branch differences -One row per branch in [`branches.txt`](../branches.txt). You work on **one** of them: find the row your -tree matches, read that row, treat the others as background. Branch differences are not yours to -reconcile. +One row per branch in [`branches.txt`](../branches.txt) + `main` (implied unless stated otherwise). +You work on **one** of them: find the row your tree matches, read that row, treat the others as +background. Branch differences are not yours to reconcile. `Base` records where the branch's infrastructure came from. It is **not** an upstream to rebase, merge or sync with: you start from a branch and stay on it, cherry-picking from another one when you want a feature @@ -31,7 +31,7 @@ you, [REPO.md](REPO.md) what is generated and never committed. | `app/` | The executable: `main.cpp`, menus, wiring, I/O. Not reachable from `tests/` — by design. | | `tests/` | Test suites, one executable registered with CTest; the frameworks themselves come from `ext/` or the build dir. | | `assets/` | Your data/images/fonts, copied by the configured `install()`/`copy_files()` step. | -| `ext/` | Vendored third-party *lightweight* code, marked `linguist-vendored` in `.gitattributes`. `ext/include/` is on your public interface (`SYSTEM PUBLIC`), `ext/private/` stays inside `src/`, `ext/lib/` holds a vendored dep's compilable code. | +| `ext/` | Vendored third-party *lightweight* code, marked `linguist-vendored` in `.gitattributes`. `ext/include/` is on your public interface (`SYSTEM PUBLIC`), `ext/private/` stays inside `src/`; where present, `ext/lib/` holds a vendored dep's compilable code. | | `tastatura.txt` | Comprehensive keyboard input only (`std::cin`); file data goes in your own files under `assets/`. | | `README.md` | Project description + the homework checkboxes. Yours to edit. | | `LICENSE`, `LICENSE.template` | AGPLv3 for your code / Unlicense for the template. You can edit the one for your code. | @@ -42,7 +42,7 @@ you, [REPO.md](REPO.md) what is generated and never committed. | Branch | Base | Detect / ships | Extra `scripts/` | CI | |---|---|---|---|---| | `main` | N/A | starter branch with no dependencies | N/A | A | -| `baze-de-date/pqxx` | `main` | `include/Database.h`\*, `src/database/Database.cpp`\*, `cmake/MinGWLibpq.cmake`, `.env.example`, `find_package(PostgreSQL)`, `PQXX_VERSION` | `configure_postgres.sh` — creates database, user and `pgcrypto`; CI runs it on every OS | A, but **no ASan row on Windows MSVC** | +| `baze-de-date/pqxx` | `main` | `include/Database.h`\*, `src/database/Database.cpp`\*, `cmake/MinGWLibpq.cmake`, `.env.example`, `find_package(PostgreSQL)`, `PQXX_VERSION` | `configure_postgres.sh` — creates database and `pgcrypto` (for creating a user, see `README.md`); CI runs it on every OS | A, but **no ASan row on Windows MSVC** | | `baze-de-date/sqlite` | `main` | `FetchContent` `sqlite3`, `add_library(sqlite3 STATIC …/sqlite3.c)` | — | A | | `common-libs` | `main` | `ext/include/{date,csv-parser}` (public), `ext/private/{digestpp,random,rlutil}`, `assets/date.csv`\* | — | A | | `descarcare-date-api` | `main` | `FetchContent` `cpr`, `ext/include/json/` | — | A; macOS-GCC row commented | @@ -85,17 +85,20 @@ Lists are registered directly even where a variable would read better because ID row says nothing about the checks that did not run — never tell a student a leak check or a test "passed" because a row was green. * **Profile A** (all non-GUI branches): every row blocks except the *Sanitizers (Windows MSVC)* step, - which is `continue-on-error` on every branch. + which is `continue-on-error` on every branch, and Clang-tidy, which was buggy in the past and can + fail only if the project is mis-configured. You should still try to have a clean clang-tidy. * **Profile B** (the 5 GUI branches): on Linux the ASan and Valgrind steps are `continue-on-error` and end - in `|| true`, and no step starts the app — a GUI hangs a headless runner. There, **no CI row can fail - because of your program**: the sanitizer and Valgrind output is in the step logs and you are expected to - read it, locally too (`./scripts/run_valgrind.sh`, an ASan configure). + in `|| true`; no ASan/Valgrind step that starts the app can fail; GUi smoke tests are skipped because they + hang a headless runner. The sanitizer and Valgrind outputs are in the step logs and you are expected to + read it, locally too (`./scripts/run_valgrind.sh`, an ASan configure). Note that valgrind may need + additional suppressions for ext libs: those are fine to add (and please contribute them) as long as + you do not exclude your own code. * Commented matrix rows and advisory steps are **intentional**: the template keeps as many checks as it can and drops only what is genuinely incompatible, with 3 OSes × 2 compilers as the floor. Deleting or changing a row is a forbidden edit — if a row looks broken, ask the teacher. * Some external libraries do not play nice with sanitizers on some OSes; that is why `tests/Boost-ext-ut` compiles an uninstrumented twin of the library for the test binary. * `scripts/run_ui_test.sh` (sends keystrokes with `xdotool` to the window named by `APP_WINDOW`, which must - match the window title set in `app/`) and `scripts/run_sanitizers.sh` are **CI-internal**: the first - needs `xvfb`, the second expects the unpacked artifact bundle as its working directory. Neither is + match the window title set in `app/`) and `scripts/run_sanitizers.sh` are **CI-internal**: they + need `xvfb`/`xdotool`; the second expects the unpacked artifact bundle as its working directory. Neither is related to `scripts/run_tests.sh`. diff --git a/.internal/mistakes.md b/.internal/mistakes.md index 520eeabc..a5d37732 100644 --- a/.internal/mistakes.md +++ b/.internal/mistakes.md @@ -27,7 +27,7 @@ Severity: 🔴 the requirement counts as **not met** · 🟠 **costs points** · | 🔴 No filler — **fără cod de umplutură/fără sens!** | Members that nothing calls (or that are "called" somewhere only to look used) are noise: they tell me nothing was learned. | Write a member when the scenario needs it. Forced scenarios do not count. Generated code you did not contribute to earns no points. | | 🔴 No copy-paste — **fără copy-paste!** | Duplicated blocks double every future bug and are the clearest sign the design was not thought through. | Pull the shared part into a function, a template, or a base class. | | 🔴 No data in the code — **fără date în cod!** | A project that hardcodes its dataset is not a program, it is a transcript with `if`s. | Read from your own data files as csv/json/other well-known format; do not hand-roll your own parser, use dedicated libraries (you have examples on other branches). And read from a library/DB where the topic requires it. `tastatura.txt` is for keyboard input only. | -| 🔴 `.gitattributes` set correctly | Linguist decides what counts as C++: ≥52–60% (T1), 75–78% (T2), 80–90% (T3). Wrong settings will put a bad project under even stricter checks. | Check the language bar on GitHub after your first real commit, not after the last one. Ask before you intend to edit this, you most likely do not need to; tests do not count towards this limit. | +| 🔴 `.gitattributes` set correctly | Linguist decides what counts as C++: ≥52–60% (T1), 75–78% (T2), 80–90% (T3) **of your own code** — meaning more code than the template ships. Wrong settings will put a bad project under even stricter checks. | The GitHub language bar is a hint, not the number: it counts `tests/` and the rest of the repo, while the graded share is measured without them. Ask before you intend to edit this, you most likely do not need to. | | 🔴 CI green, **all** checkboxes ticked, a git tag per homework | CI is the entry ticket; an unticked box or a missing tag is a purely administrative way to lose chances to enter the exam. | Budget time for CI round-trips; see [section 6](#6-checking-your-own-work). | | 🔴 No OS-specific headers (`conio.h`, `windows.h`, …) | CI builds on more than one OS; these headers break it and make the code non-portable. | Standard I/O only. Ask if you really think you need one. Portable alternatives are provided in branches. | | 🔴 Source files must be in the repo | An empty or partially committed repo has nothing to grade. | Check `git status --ignored` once in a while: `build/` and `install_dir/` belong in `.gitignore`, your sources do not. | @@ -138,17 +138,20 @@ The same things CI runs, you can run locally — see the [README](../README.md) ```sh ./scripts/cmake.sh configure # add -e "-DUSE_ASAN=ON" for AddressSanitizer ./scripts/cmake.sh build +./scripts/audit_ext_libs.sh # needs the configure above; CI runs it inside the cppcheck step ./scripts/run_cppcheck.sh ./scripts/run_tests.sh -r # or: ./scripts/cmake.sh test ./scripts/run_valgrind.sh clang-tidy -p build src/*.cpp app/*.cpp tests/*.cpp # uses the repo's .clang-tidy ``` -Those exist on every branch; what yours adds is in [BRANCHES.md](BRANCHES.md). On GUI branches, -there are also `run_ui_test.sh` and `run_sanitizers.sh` which are called by CI; to use them locally you need -more setup and the sanitizers work only on Linux (the first needs `xvfb`/`xdotool`, the second expects the -unpacked artifact bundle as its working directory); on GUI branches `run_valgrind.sh` drives the window -with `xdotool`, so run it where a display exists. +`audit_ext_libs.sh` reads `build/compile_commands.json`, so without a configured build dir all it can +report is that it cannot open that file. Those scripts exist on every branch; what yours adds is in +[BRANCHES.md](BRANCHES.md). On GUI branches, there are also `run_ui_test.sh` and `run_sanitizers.sh` which +are called by CI; to use them locally you need more setup and the sanitizers with GUI work only on Linux +(they need `xvfb`/`xdotool`; the second expects the unpacked artifact bundle as its working directory); +on GUI branches `run_valgrind.sh` also needs `xvfb`/`xdotool`. If you can make these work more broadly, +tell the teacher. Practical notes: @@ -156,9 +159,9 @@ Practical notes: * Keep a recent green commit on your default branch and re-run a workflow from time to time — the CI dependency cache expires, and pushing on a cold cache after weeks of silence is slow. * **Red is the only certainty.** A red row means something is wrong; a green row only means the steps that - ran did not fail. On the GUI branches no row can fail because of your program, so run ASan and Valgrind - yourself and read the logs of the steps that ran. CI is the **minimum**, never proof of memory safety or - of design. + ran did not fail. On the GUI branches no ASan/Valgrind row can fail because of your program, so run ASan + and Valgrind yourself and read the logs of the steps that ran. CI is the **minimum**, never proof of + memory safety or of design. Some limitations of the template are known and left as they are for lack of time — the advisory runtime rows on the GUI branches, and external libraries that do not cooperate with sanitizers on some OSes. Ask diff --git a/AGENTS.md b/AGENTS.md index b74d0dda..fe747ac4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,10 +50,10 @@ with using the proper setup, which has been validated. ## CI: red is the only certainty A red row means something is definitely wrong. **A green row says nothing about the checks that did not -run** — on the GUI branches the ASan/Valgrind rows are advisory and no row launches the app, so "CI e -verde ⇒ nu ai leaks / e testat" is always a false claim. Read the logs of the steps that did run, filtering -noise from external libs in valgrind output. Disabled -matrix rows are intentional (3 OSes × 2 compilers is the floor); asking the teacher about one is fine, +run** — on the GUI branches the ASan/Valgrind rows are advisory and no row turns red when the app misbehaves +(leak, crash), so "CI e verde ⇒ nu ai leaks / e testat" is always a false claim. Read the logs of the steps +that did run, filtering noise from external libs in valgrind output. Disabled +matrix rows are intentional (3 OSes, 2 compilers is the floor); asking the teacher about one is fine, commenting it is not. Details per branch: [BRANCHES.md](.internal/BRANCHES.md). ## `.internal/mistakes.md` — load on demand, do not keep it in context diff --git a/README.md b/README.md index c2901bff..3269d0d7 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ O cerință nu se consideră îndeplinită dacă este realizată doar prin cod g - [ ] scenariu de utilizare **cu sens** a claselor definite: - crearea de obiecte și apelarea tuturor funcțiilor membru publice în main - vor fi adăugate în fișierul `tastatura.txt` DOAR exemple de date de intrare de la tastatură (dacă există); dacă aveți nevoie de date din fișiere, creați alte fișiere separat -- [ ] minim 52-60% din codul propriu să fie C++; testele nu se pun +- [ ] minim 52-60% din codul propriu să fie C++, adică să aveți mai mult cod decât ce e inclus în template; testele nu se pun, nu vă luați după procentajul din interfața web; detalii la ore - [x] `.gitattributes` configurat corect (nu trebuie făcute modificări aici) - [ ] tag de `git`: de exemplu `v0.1` - [ ] serviciu de integrare continuă (CI) cu **toate bifele**; exemplu: GitHub Actions @@ -82,7 +82,7 @@ O cerință nu se consideră îndeplinită dacă este realizată doar prin cod g - [ ] STL - [ ] cât mai multe `const` - [ ] funcții *de nivel înalt*, de eliminat cât mai mulți getters/setters/funcții low-level -- [ ] minim 75-78% din codul propriu să fie C++; testele nu se pun +- [ ] minim 75-78% din codul propriu să fie C++; testele nu se pun, nu vă luați după procentajul din interfața web - [ ] la sfârșit: commit separat cu adăugarea unei noi clase derivate fără a modifica restul codului, **pe lângă cele 3 derivate deja adăugate** din aceeași ierarhie - noua derivată nu poate fi una existentă care a fost ștearsă și adăugată din nou - noua derivată va fi integrată în codul existent (adică va fi folosită, nu adăugată doar ca să fie) @@ -95,7 +95,7 @@ O cerință nu se consideră îndeplinită dacă este realizată doar prin cod g - [ ] 2 șabloane de proiectare (design patterns) - [ ] o clasă șablon cu sens; minim **2 instanțieri** - [ ] preferabil și o funcție șablon (template) cu sens; minim 2 instanțieri -- [ ] minim 80-90% din codul propriu să fie C++; testele nu se pun +- [ ] minim 80-90% din codul propriu să fie C++; testele nu se pun, nu vă luați după procentajul din interfața web - [ ] tag de `git` pe commit cu **toate bifele**: de exemplu `v0.3` sau `v1.0` - [ ] code review #3 2 proiecte diff --git a/cmake/Options.cmake b/cmake/Options.cmake index f9f77209..c323c01c 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -1,5 +1,6 @@ option(CMAKE_COLOR_DIAGNOSTICS "Enable color diagnostics" ON) option(CMAKE_TLS_VERIFY "Verify TLS certificates" OFF) +option(CMAKE_EXPORT_COMPILE_COMMANDS "Export compilation database" ON) option(WARNINGS_AS_ERRORS "Treat warnings as errors" OFF) option(USE_ASAN "Use Address Sanitizer" OFF) diff --git a/scripts/audit_ext_libs.sh b/scripts/audit_ext_libs.sh index bcdd8157..6e780ee5 100755 --- a/scripts/audit_ext_libs.sh +++ b/scripts/audit_ext_libs.sh @@ -1,7 +1,8 @@ #!/usr/bin/bash -# Audits build/compile_commands.json — the file scripts/cmake.sh enables, so CI and a local run read -# the same thing, and the cppcheck CI step runs this on every branch, on an unbuilt tree — for the +# Audits build/compile_commands.json — ON by default in cmake/Options.cmake and passed explicitly by +# scripts/cmake.sh, so CI and a local run read the same thing, and the cppcheck CI step runs this on every +# branch, on an unbuilt tree — for the # two ways an external dependency is misconfigured: # # 1. a dependency include directory given with -I instead of -isystem -> error, exit 1. cppcheck From 67b3c89c2efe5eea54a015b6395f0c2dde590cdf Mon Sep 17 00:00:00 2001 From: mcmarius <23401453+mcmarius@users.noreply.github.com> Date: Tue, 20 Oct 2026 15:27:54 +0300 Subject: [PATCH 10/10] Final tweaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Să fie bine să nu fie rău --- .internal/BRANCHES.md | 15 +++++---- .internal/mistakes.md | 12 +++---- CMakeLists.txt | 2 ++ README.md | 2 +- cmake/Options.cmake | 1 - scripts/audit_ext_libs.sh | 71 ++++++++++++++++++++++++++++++--------- 6 files changed, 73 insertions(+), 30 deletions(-) diff --git a/.internal/BRANCHES.md b/.internal/BRANCHES.md index f46577ab..69b7c712 100644 --- a/.internal/BRANCHES.md +++ b/.internal/BRANCHES.md @@ -1,6 +1,6 @@ # Template branches — layout and per-branch differences -One row per branch in [`branches.txt`](../branches.txt) + `main` (implied unless stated otherwise). +One row per branch in [`branches.txt`](../branches.txt) + `main` (the default branch). You work on **one** of them: find the row your tree matches, read that row, treat the others as background. Branch differences are not yours to reconcile. @@ -41,7 +41,7 @@ you, [REPO.md](REPO.md) what is generated and never committed. | Branch | Base | Detect / ships | Extra `scripts/` | CI | |---|---|---|---|---| -| `main` | N/A | starter branch with no dependencies | N/A | A | +| `main` | N/A | starter branch with no dependencies | — | A | | `baze-de-date/pqxx` | `main` | `include/Database.h`\*, `src/database/Database.cpp`\*, `cmake/MinGWLibpq.cmake`, `.env.example`, `find_package(PostgreSQL)`, `PQXX_VERSION` | `configure_postgres.sh` — creates database and `pgcrypto` (for creating a user, see `README.md`); CI runs it on every OS | A, but **no ASan row on Windows MSVC** | | `baze-de-date/sqlite` | `main` | `FetchContent` `sqlite3`, `add_library(sqlite3 STATIC …/sqlite3.c)` | — | A | | `common-libs` | `main` | `ext/include/{date,csv-parser}` (public), `ext/private/{digestpp,random,rlutil}`, `assets/date.csv`\* | — | A | @@ -54,7 +54,7 @@ you, [REPO.md](REPO.md) what is generated and never committed. | `sfml3-imagini-externe-api` | `sfml3` | `FetchContent` `SFML cpr` — images fetched over HTTP | GUI set (see `sfml3`) | B; see `sfml3` | | `sfml3-resurse-locale` | `sfml3` | `include/ResourceManager.hpp`\*, `src/ResourceManager.cpp`\*, `assets/fonts/`\*, `assets/images/`\*, `target_link_libraries(oop PUBLIC SFML::…)` | GUI set (see `sfml3`) | B; see `sfml3` | | `tests/Boost-ext-ut` | `main` | `ext/include/boost/ut.hpp`, `MAIN_LIBRARY_SOURCES`, second library target `oop_nslib` | — | A; the tests and `oop_nslib` are built **without** sanitizers | -| `tests/gtest` | `main` | `app/CMakeLists.txt` and `src/CMakeLists.txt` exist, `FetchContent` `gtest` inside `tests/CMakeLists.txt`, `include/BankAccount.h`\* | — | A; googletest is fetched into the build dir, never committed | +| `tests/gtest` | `main` | `app/CMakeLists.txt` and `src/CMakeLists.txt` exist, `FetchContent` `gtest` inside `tests/CMakeLists.txt`, `include/BankAccount.h`\* | — | A | | `wxwidgets` | `sfml3` | `FetchContent` `wx`, `target_link_libraries(oop PUBLIC wx::core)` — GUI plumbing only, no SFML in the tree | GUI set (see `sfml3`) | B; see `sfml3` | ## Different on purpose @@ -85,11 +85,12 @@ Lists are registered directly even where a variable would read better because ID row says nothing about the checks that did not run — never tell a student a leak check or a test "passed" because a row was green. * **Profile A** (all non-GUI branches): every row blocks except the *Sanitizers (Windows MSVC)* step, - which is `continue-on-error` on every branch, and Clang-tidy, which was buggy in the past and can - fail only if the project is mis-configured. You should still try to have a clean clang-tidy. + which is `continue-on-error` on every branch, and Clang-tidy, which is advisory. + You should still have a clean clang-tidy as best-practice. * **Profile B** (the 5 GUI branches): on Linux the ASan and Valgrind steps are `continue-on-error` and end - in `|| true`; no ASan/Valgrind step that starts the app can fail; GUi smoke tests are skipped because they - hang a headless runner. The sanitizer and Valgrind outputs are in the step logs and you are expected to + in `|| true`; no ASan/Valgrind step that starts the app can fail; GUI smoke tests are skipped because they + hang a headless runner; only tests are re-ran again in the context of the delivered artifacts. + The sanitizer and Valgrind outputs are in the step logs and you are expected to read it, locally too (`./scripts/run_valgrind.sh`, an ASan configure). Note that valgrind may need additional suppressions for ext libs: those are fine to add (and please contribute them) as long as you do not exclude your own code. diff --git a/.internal/mistakes.md b/.internal/mistakes.md index a5d37732..fcb47a25 100644 --- a/.internal/mistakes.md +++ b/.internal/mistakes.md @@ -31,7 +31,7 @@ Severity: 🔴 the requirement counts as **not met** · 🟠 **costs points** · | 🔴 CI green, **all** checkboxes ticked, a git tag per homework | CI is the entry ticket; an unticked box or a missing tag is a purely administrative way to lose chances to enter the exam. | Budget time for CI round-trips; see [section 6](#6-checking-your-own-work). | | 🔴 No OS-specific headers (`conio.h`, `windows.h`, …) | CI builds on more than one OS; these headers break it and make the code non-portable. | Standard I/O only. Ask if you really think you need one. Portable alternatives are provided in branches. | | 🔴 Source files must be in the repo | An empty or partially committed repo has nothing to grade. | Check `git status --ignored` once in a while: `build/` and `install_dir/` belong in `.gitignore`, your sources do not. | -| 🔴 No memory errors | Leaks, out-of-bounds, UB. These are already checked automatically. | Run Valgrind and ASan yourself, see below. | +| 🔴 No memory errors | Leaks, out-of-bounds, UB. These are already checked automatically where possible. | Run Valgrind and ASan yourself, see below. | ## 2. Theme requirements — how students usually misread them @@ -147,11 +147,11 @@ clang-tidy -p build src/*.cpp app/*.cpp tests/*.cpp # uses the repo's .clang-t `audit_ext_libs.sh` reads `build/compile_commands.json`, so without a configured build dir all it can report is that it cannot open that file. Those scripts exist on every branch; what yours adds is in -[BRANCHES.md](BRANCHES.md). On GUI branches, there are also `run_ui_test.sh` and `run_sanitizers.sh` which -are called by CI; to use them locally you need more setup and the sanitizers with GUI work only on Linux -(they need `xvfb`/`xdotool`; the second expects the unpacked artifact bundle as its working directory); -on GUI branches `run_valgrind.sh` also needs `xvfb`/`xdotool`. If you can make these work more broadly, -tell the teacher. +[BRANCHES.md](BRANCHES.md). On GUI branches, there are also `run_valgrind.sh` and `run_sanitizers.sh` which +are called by CI and call in turn `run_ui_test.sh`; to use them locally you need more setup and the +sanitizers with GUI work only on Linux (they need `xvfb`/`xdotool`; the second expects the unpacked +artifact bundle as its working directory); on GUI branches `run_valgrind.sh` also needs `xvfb`/`xdotool`. +If you can make these work more broadly, tell the teacher. Practical notes: diff --git a/CMakeLists.txt b/CMakeLists.txt index 57be606a..ca783379 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,8 @@ set(MAIN_EXECUTABLE_NAME "${MAIN_PROJECT_NAME}_main") set(MAIN_LIBRARY_NAME "${MAIN_PROJECT_NAME}") set(TESTS_EXECUTABLE_NAME "${MAIN_PROJECT_NAME}_test") +# needs to be before `project()` to take effect +option(CMAKE_EXPORT_COMPILE_COMMANDS "Export compilation database" ON) project(${MAIN_PROJECT_NAME}) # set(CMAKE_PROJECT_VERSION_MAJOR 0) diff --git a/README.md b/README.md index 3269d0d7..0175cbd6 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ O cerință nu se consideră îndeplinită dacă este realizată doar prin cod g - nu doar citiri/afișări sau adăugat/șters elemente într-un/dintr-un vector - [ ] scenariu de utilizare **cu sens** a claselor definite: - crearea de obiecte și apelarea tuturor funcțiilor membru publice în main - - vor fi adăugate în fișierul `tastatura.txt` DOAR exemple de date de intrare de la tastatură (dacă există); dacă aveți nevoie de date din fișiere, creați alte fișiere separat + - vor fi adăugate în fișierul `tastatura.txt` DOAR exemple de date de intrare de la tastatură (dacă există) pentru a acoperi cât mai multe cazuri; dacă aveți nevoie de date din fișiere, creați alte fișiere separat - [ ] minim 52-60% din codul propriu să fie C++, adică să aveți mai mult cod decât ce e inclus în template; testele nu se pun, nu vă luați după procentajul din interfața web; detalii la ore - [x] `.gitattributes` configurat corect (nu trebuie făcute modificări aici) - [ ] tag de `git`: de exemplu `v0.1` diff --git a/cmake/Options.cmake b/cmake/Options.cmake index c323c01c..f9f77209 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -1,6 +1,5 @@ option(CMAKE_COLOR_DIAGNOSTICS "Enable color diagnostics" ON) option(CMAKE_TLS_VERIFY "Verify TLS certificates" OFF) -option(CMAKE_EXPORT_COMPILE_COMMANDS "Export compilation database" ON) option(WARNINGS_AS_ERRORS "Treat warnings as errors" OFF) option(USE_ASAN "Use Address Sanitizer" OFF) diff --git a/scripts/audit_ext_libs.sh b/scripts/audit_ext_libs.sh index 6e780ee5..b207e299 100755 --- a/scripts/audit_ext_libs.sh +++ b/scripts/audit_ext_libs.sh @@ -1,6 +1,6 @@ #!/usr/bin/bash -# Audits build/compile_commands.json — ON by default in cmake/Options.cmake and passed explicitly by +# Audits build/compile_commands.json — ON by default and passed explicitly by # scripts/cmake.sh, so CI and a local run read the same thing, and the cppcheck CI step runs this on every # branch, on an unbuilt tree — for the # two ways an external dependency is misconfigured: @@ -11,13 +11,16 @@ # runtime, or generate its headers. Declare it beside the dependency, with CACHE so this reads it: # set(AUDIT_EXT_LIBS_ALLOW "" CACHE STRING "linked, not included") # Included only from tests/ is a note: for a test framework that is the design. Reported only for a -# dependency this project names in its own build files: one that another dependency fetched (zlib -# below curl, the googletest umbrella above gtest) is not the student to drop, and warning about it -# teaches students to ignore the audit. Vendored code below ext/ is theirs, whoever copied it in. +# dependency this project names in code in its own build files (a name inside a comment is prose, +# not a choice): one that another dependency fetched (zlib below curl, the googletest umbrella above +# gtest) is not the student to drop, and warning about it teaches students to ignore the audit. +# Vendored code below ext/ is theirs, whoever copied it in. # # Dependencies are discovered from CMakeCache.txt plus the FetchContent trees: the base directory, or # its default /_deps. The cache alone is not enough, since FetchContent_Populate followed by a -# hand-written target (baze-de-date/sqlite) leaves no _SOURCE_DIR entry behind. +# hand-written target (baze-de-date/sqlite) leaves no _SOURCE_DIR entry behind. Only the build +# directory given (-b, BUILD_DIR, default build) is read: a second build tree in the repository is +# generated output, marked by the CMakeCache.txt in it, and it says nothing about this project. # # bash ./scripts/audit_ext_libs.sh [-b build dir] [-v] (-v lists every dependency) # env: BUILD_DIR, EXT_DIR, TESTS_DIR, AUDIT_EXT_LIBS_ALLOW (exceptions for this run only) @@ -29,6 +32,7 @@ DEP_ROOTS=() # directories holding dependency code, umbrellas included DEP_UNITS=() # "", one per dependency FETCH_ROOTS=() # directories FetchContent unpacked into, parents of -src OWN_CMAKE=() # CMake files of this repository, the ones naming the dependencies it chose +BUILD_TREES=() # generated build trees below the repository, each marked by its CMakeCache.txt ALLOW_FROM_CACHE="" TAB=$'\t' # unit separator; a literal tab here would be one editor away from silence @@ -60,6 +64,14 @@ under_dep_root() { # below one of the dependency roots return 1 } +under_build_tree() { # below a generated build tree, whichever name the IDE gave it + local tree + for tree in "${BUILD_TREES[@]}"; do + [[ "${1}" == "${tree%/}"/* ]] && return 0 + done + return 1 +} + has_header() { # a header here or below it, so this is dependency code and not a note directory local path="$1" if [[ -d "${path}" ]]; then @@ -90,17 +102,31 @@ fetch_units_under() { # a FetchContent base directory } # The build files naming a dependency are what makes it the student choice. Dependency trees are not -# build files: curl declaring zlib is not this project declaring zlib. +# build files: curl declaring zlib is not this project declaring zlib. Neither is a build directory, +# whichever name the IDE gave it (build, build-asan, cmake-build-debug, out): CMakeCache.txt marks +# one, only the tree given to this script is ever read, and a generated cmake_install.cmake names the +# dependencies of the build that wrote it, not the ones this repository chose. read_own_cmake() { # - local build_dir_abs file + local build_dir_abs entry + local -a found=() build_dir_abs="$(abs_path "$1")" - while IFS= read -r file; do - file="$(abs_path "${file}")" - [[ "${file}" != "${build_dir_abs}"/* ]] || continue - under_dep_root "${file}" && continue - OWN_CMAKE+=("${file}") - done < <(find "${PWD}" \( -name .git -o -name _deps \) -prune -o \ - \( -name 'CMakeLists.txt' -o -name '*.cmake' \) -print 2>/dev/null) + # one walk: the caches mark the trees to drop, so they are collected with the build files; + # CMakeFiles is every build tree root, cutting the deep part of each one out of the walk + while IFS= read -r entry; do found+=("${entry}"); done < <( + find "${PWD}" \( -name .git -o -name _deps -o -name CMakeFiles \) -prune -o \ + \( -name CMakeCache.txt -o -name 'CMakeLists.txt' -o -name '*.cmake' \) -print 2>/dev/null) + for entry in "${found[@]}"; do + [[ "${entry##*/}" == CMakeCache.txt ]] || continue + BUILD_TREES+=("$(abs_path "${entry%/*}")") + done + for entry in "${found[@]}"; do + [[ "${entry##*/}" == CMakeCache.txt ]] && continue + entry="$(abs_path "${entry}")" + [[ "${entry}" != "${build_dir_abs}"/* ]] || continue + under_dep_root "${entry}" && continue + under_build_tree "${entry}" && continue + OWN_CMAKE+=("${entry}") + done } # CMakeCache.txt split into the two views above. Our own project is not a dependency, however @@ -393,10 +419,25 @@ audit_build_tree() { out = out (i > 1 ? "\n " : " ") display(unit_raw[unit, i]) return out } + # a # that is not inside a quoted argument starts a CMake comment: prose about a dependency does + # not pick it, so the words there are not the project naming one (a disabled FetchContent_Declare + # in a comment must not make its library the student answer for) + function strip_comment(line, out, i, len, ch, quoted) { + out = "" + quoted = 0 + len = length(line) + for (i = 1; i <= len; i++) { + ch = substr(line, i, 1) + if (ch == "\"") quoted = !quoted + else if (ch == "#" && !quoted) break + out = out ch + } + return out + } # words of the build files of this repository, folded like dependency names: the names in them are # the dependencies this project chose, and the only ones it is asked about function own_words(line, n, parts, i, word) { - n = split(line, parts, /[^A-Za-z0-9_]+/) + n = split(strip_comment(line), parts, /[^A-Za-z0-9_]+/) for (i = 1; i <= n; i++) { word = name_key(parts[i]) if (word != "" && !(word in own_word)) {