Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ jobs:
sudo apt-get install -y gcc g++ clang valgrind

- name: Build nob
run: cc -o nob nob.c
run: cc -o build/nob build/nob.c

- name: Run build and tests
run: ./nob
run: ./build/nob
env:
SNAKEPATH_SANITIZE: "1"

Expand All @@ -48,7 +48,7 @@ jobs:
uses: ilammy/msvc-dev-cmd@v1

- name: Build nob
run: cl.exe /Fe:nob.exe nob.c
run: cl.exe /Fe:build\nob.exe build\nob.c

- name: Run build and tests
run: .\nob.exe
run: .\build\nob.exe
26 changes: 13 additions & 13 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
nob
nob.old
build/nob
build/nob.old
*.o
*.dSYM/
api_demo
Expand All @@ -9,17 +9,17 @@ __pycache__/
*.pyc

# Tests
tests/python_harness/cpython_tests/
tests/python_harness/libsnakepath.so
tests/python_harness/snakepath.dll
tests/test_gcc
tests/test_clang
tests/test_gcc_san
tests/test_clang_san
tests/test_gpp
tests/test_clangpp
tests/test_fluent_*
tests/test_msvc*
build/python_harness/cpython_tests/
build/python_harness/libsnakepath.so
build/python_harness/snakepath.dll
build/test_gcc
build/test_clang
build/test_gcc_san
build/test_clang_san
build/test_gpp
build/test_clangpp
build/test_fluent_*
build/test_msvc*

# Stale test symlink artifacts
test_fluent_sym_*.tmp
Expand Down
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ Bad: shorten names, define convenience macros, remove whitespace/braces.
**Run all three before committing:**

```bash
gcc -std=c99 -I. -Wall -Wextra -Werror -o test_snakepath tests/test.c && ./test_snakepath
g++ -std=c++11 -x c++ -I. -Wall -Wextra -Werror -Wmissing-field-initializers -o test_cpp tests/test.c && ./test_cpp
cd tests/python_harness && gcc -shared -fPIC -o snakepath/libsnakepath.so snakepath_lib.c -I../.. && python run_cpython_tests.py
gcc -std=c99 -I. -Wall -Wextra -Werror -o test_snakepath build/test.c && ./test_snakepath
g++ -std=c++11 -x c++ -I. -Wall -Wextra -Werror -Wmissing-field-initializers -o test_cpp build/test.c && ./test_cpp
cd build/python_harness && gcc -shared -fPIC -o snakepath/libsnakepath.so snakepath_lib.c -I../.. && python run_cpython_tests.py
```

**g++ pitfalls:** `{0}` → `memset`, `void*` casts → `SP_PRIV_CAST`, C casts → `SP_PRIV_CAST`

**Termux:** `cc -DSNAKEPATH_QUIET -o nob nob.c && SNAKEPATH_SKIP_GCC=1 SNAKEPATH_NO_NRVO=1 ./nob`
**Termux:** `cc -DSNAKEPATH_QUIET -o build/nob build/nob.c && SNAKEPATH_SKIP_GCC=1 SNAKEPATH_NO_NRVO=1 ./build/nob`
Termux `/tmp` symlink failures are expected locally (Python `test_resolve_nonexist_relative_issue38671` and fluent `hardlink_to` test). CI is authoritative.

## EXPECTED_FAILURES
Expand All @@ -50,7 +50,7 @@ Dict mapping error substrings → `(class_name, test_name)` tuples. Runner verif
- Use `_decode(..., errors="surrogatepass")` and copy `SpPath` structs in `_from_sp` to preserve embedded nulls.
- Windows builds should not compile `sp_owner_wrap`/`sp_group_wrap`; gate the wrappers in C.
- `sp_with_segments` now takes a `parts_count` (no NULL-terminated arrays); use `SP_ARRAY_LEN`.
- New functionality goes in `snakepath.h` first; then mirror wrappers in `tests/python_harness/snakepath_lib.c` and `tests/python_harness/snakepath/__init__.py`, plus tests in `tests/test.c` (and `tests/test_fluent_api.c` for fluent parity).
- New functionality goes in `snakepath.h` first; then mirror wrappers in `build/python_harness/snakepath_lib.c` and `build/python_harness/snakepath/__init__.py`, plus tests in `build/test.c` (and `build/test_fluent_api.c` for fluent parity).
- When API examples change, update `api_demo.c` first, then sync `README.md` and `index.html`, and record any new learnings here.
- Public API call depth is now enforced by `tests/test_call_depth.py` (limit = 3 public frames); keep wrapper chains flat and favor `sp_priv_*` delegation.
- Public API call depth is now enforced by `build/test_call_depth.py` (limit = 3 public frames); keep wrapper chains flat and favor `sp_priv_*` delegation.
- For `"."` behavior, keep `SpPath` canonical as empty (`len == 0`) and let string conversion render `"."`; storing literal `"."` breaks equality/parents semantics.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
Snakepath:
C99 STB-style header-only port of [Python's pathlib](https://docs.python.org/3/library/pathlib.html). Passes [CPython 3.12's own test suite](tests/python_harness/).
C99 STB-style header-only port of [Python's pathlib](https://docs.python.org/3/library/pathlib.html). Passes [CPython 3.12's own test suite](build/python_harness/).
POSIX + Windows. No malloc (OS functions like `opendir`/`stat` may allocate internally).
Vibe-coded with Claude Code + Cursor.

Expand Down Expand Up @@ -31,7 +31,7 @@ rm -f demo snakepath.h
## Build & Test

```bash
cc -o nob nob.c && ./nob
cc -o build/nob build/nob.c && ./build/nob
```


Expand Down
109 changes: 56 additions & 53 deletions nob.c → build/nob.c
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* nob.c - Build script for snakepath using nob.h
* Usage: cc -o nob nob.c && ./nob
* Usage: cc -o build/nob build/nob.c && ./build/nob
*
* Builds are parallelized across available CPU cores.
*
* For quiet mode (no command echo):
* cc -DSNAKEPATH_QUIET -o nob nob.c && ./nob
* cc -DSNAKEPATH_QUIET -o build/nob build/nob.c && ./build/nob
*/

#ifdef SNAKEPATH_QUIET
Expand Down Expand Up @@ -123,14 +123,14 @@ static bool build_source_async(BuildConfig cfg, const char *source, const char *
#endif
}

/* Include root directory for header files */
/* Include project root for snakepath.h */
#ifdef _WIN32
if (cfg.compiler == COMPILER_MSVC || cfg.compiler == COMPILER_MSVC_CPP) {
nob_cmd_append(&cmd, "/I.");
nob_cmd_append(&cmd, "/I..");
} else
#endif
{
nob_cmd_append(&cmd, "-I.");
nob_cmd_append(&cmd, "-I..");
}

append_warnings(&cmd, cfg.compiler);
Expand Down Expand Up @@ -199,18 +199,18 @@ static bool run_valgrind(const char *exe) {

static const char *all_artifacts[] = {
#ifdef _WIN32
"tests/test_msvc.exe", "tests/test_msvc_cpp.exe", "tests/test_fluent_msvc.exe", "demo.exe",
/* PDB and obj files from MSVC (now in tests/ directory to avoid conflicts) */
"tests/test_msvc.pdb", "tests/test_msvc_cpp.pdb", "tests/test_fluent_msvc.pdb", "demo.pdb",
"tests/test_msvc.obj", "tests/test_msvc_cpp.obj", "tests/test_fluent_msvc.obj", "api_demo.obj",
"test_msvc.exe", "test_msvc_cpp.exe", "test_fluent_msvc.exe", "../demo.exe",
/* PDB and obj files from MSVC */
"test_msvc.pdb", "test_msvc_cpp.pdb", "test_fluent_msvc.pdb", "../demo.pdb",
"test_msvc.obj", "test_msvc_cpp.obj", "test_fluent_msvc.obj", "../api_demo.obj",
/* Legacy obj files in root (clean these up too) */
"test.obj", "test_fluent_api.obj",
"tests/python_harness/snakepath.dll",
"../test.obj", "../test_fluent_api.obj",
"python_harness/snakepath.dll",
#else
"tests/test_gcc", "tests/test_clang", "tests/test_gcc_san", "tests/test_clang_san",
"tests/test_gpp", "tests/test_clangpp", "tests/test_fluent_gcc", "tests/test_fluent_clang",
"api_demo",
"tests/python_harness/libsnakepath.so",
"test_gcc", "test_clang", "test_gcc_san", "test_clang_san",
"test_gpp", "test_clangpp", "test_fluent_gcc", "test_fluent_clang",
"../api_demo",
"python_harness/libsnakepath.so",
#endif
NULL
};
Expand All @@ -222,23 +222,23 @@ static bool build_python_lib(Compiler compiler, Nob_Procs *procs) {
#ifdef _WIN32
if (compiler == COMPILER_MSVC) {
nob_cmd_append(&cmd, "cl.exe", "/std:c11", "/LD", "/O2");
nob_cmd_append(&cmd, "/W4", "/I.");
nob_cmd_append(&cmd, "/Fe:tests/python_harness/snakepath.dll");
nob_cmd_append(&cmd, "tests/python_harness/snakepath_lib.c");
nob_cmd_append(&cmd, "/W4", "/I..");
nob_cmd_append(&cmd, "/Fe:python_harness/snakepath.dll");
nob_cmd_append(&cmd, "python_harness/snakepath_lib.c");
} else {
nob_log(NOB_WARNING, "Python lib: Using clang on Windows");
nob_cmd_append(&cmd, "clang", "-shared", "-fPIC", "-O2", "-I.");
nob_cmd_append(&cmd, "clang", "-shared", "-fPIC", "-O2", "-I..");
nob_cmd_append(&cmd, "-fvisibility=hidden");
nob_cmd_append(&cmd, "-o", "tests/python_harness/snakepath.dll");
nob_cmd_append(&cmd, "tests/python_harness/snakepath_lib.c");
nob_cmd_append(&cmd, "-o", "python_harness/snakepath.dll");
nob_cmd_append(&cmd, "python_harness/snakepath_lib.c");
}
#else
const char *cc = (compiler == COMPILER_CLANG || compiler == COMPILER_CLANGPP) ? "clang" : "gcc";
nob_cmd_append(&cmd, cc, "-shared", "-fPIC", "-O2", "-I.");
nob_cmd_append(&cmd, cc, "-shared", "-fPIC", "-O2", "-I..");
nob_cmd_append(&cmd, "-Wall", "-Wextra");
nob_cmd_append(&cmd, "-fvisibility=hidden");
nob_cmd_append(&cmd, "-o", "tests/python_harness/libsnakepath.so");
nob_cmd_append(&cmd, "tests/python_harness/snakepath_lib.c");
nob_cmd_append(&cmd, "-o", "python_harness/libsnakepath.so");
nob_cmd_append(&cmd, "python_harness/snakepath_lib.c");
#endif

bool result;
Expand Down Expand Up @@ -270,14 +270,14 @@ static const char *find_python(void) {
static bool run_call_depth_tests(void) {
Nob_Cmd cmd = {0};
const char *python = find_python();
nob_cmd_append(&cmd, python, "tests/test_call_depth.py", "snakepath.h", "3");
nob_cmd_append(&cmd, python, "test_call_depth.py", "../snakepath.h", "3");
return nob_cmd_run(&cmd);
}

static bool run_python_tests(void) {
Nob_Cmd cmd = {0};
const char *python = find_python();
nob_cmd_append(&cmd, python, "tests/python_harness/run_cpython_tests.py");
nob_cmd_append(&cmd, python, "python_harness/run_cpython_tests.py");
return nob_cmd_run(&cmd);
}

Expand All @@ -297,6 +297,9 @@ static bool clean_artifacts(void) {
int main(int argc, char **argv) {
NOB_GO_REBUILD_URSELF(argc, argv);

/* nob.c lives in build/ alongside test sources — chdir so paths are simple */
nob_set_current_dir("build");

/* Check VERBOSE env var - default true, set to 0 to suppress INFO logs */
const char *verbose_env = getenv("VERBOSE");
if (verbose_env && (strcmp(verbose_env, "0") == 0 || strcmp(verbose_env, "false") == 0)) {
Expand Down Expand Up @@ -344,39 +347,39 @@ int main(int argc, char **argv) {

#ifdef _WIN32
BuildConfig test_configs[] = {
{COMPILER_MSVC, false, "MSVC (C)", "tests/test_msvc.exe"},
{COMPILER_MSVC_CPP, false, "MSVC (C++)", "tests/test_msvc_cpp.exe"},
{COMPILER_MSVC, false, "MSVC (C)", "test_msvc.exe"},
{COMPILER_MSVC_CPP, false, "MSVC (C++)", "test_msvc_cpp.exe"},
};
size_t test_count = sizeof(test_configs) / sizeof(test_configs[0]);
BuildConfig fluent_configs[] = {
{COMPILER_MSVC, false, "MSVC Fluent", "tests/test_fluent_msvc.exe"},
{COMPILER_MSVC, false, "MSVC Fluent", "test_fluent_msvc.exe"},
};
BuildConfig demo_config = {COMPILER_MSVC, false, "Demo", "api_demo.exe"};
const char *demo_output = "api_demo.exe";
BuildConfig demo_config = {COMPILER_MSVC, false, "Demo", "../api_demo.exe"};
const char *demo_output = "../api_demo.exe";
#else
bool use_sanitizers = getenv("SNAKEPATH_SANITIZE") != NULL;
bool skip_gcc = getenv("SNAKEPATH_SKIP_GCC") != NULL; /* For Termux where gcc is clang */

/* Full configs with sanitizers */
BuildConfig test_configs_full[] = {
{COMPILER_GCC, false, "GCC", "./tests/test_gcc"},
{COMPILER_CLANG, false, "Clang", "./tests/test_clang"},
{COMPILER_GCC, true, "GCC + sanitizers", "./tests/test_gcc_san"},
{COMPILER_CLANG, true, "Clang + sanitizers", "./tests/test_clang_san"},
{COMPILER_GPP, false, "G++ (C++)", "./tests/test_gpp"},
{COMPILER_CLANGPP, false, "Clang++ (C++)", "./tests/test_clangpp"},
{COMPILER_GCC, false, "GCC", "./test_gcc"},
{COMPILER_CLANG, false, "Clang", "./test_clang"},
{COMPILER_GCC, true, "GCC + sanitizers", "./test_gcc_san"},
{COMPILER_CLANG, true, "Clang + sanitizers", "./test_clang_san"},
{COMPILER_GPP, false, "G++ (C++)", "./test_gpp"},
{COMPILER_CLANGPP, false, "Clang++ (C++)", "./test_clangpp"},
};
/* No sanitizers */
BuildConfig test_configs_no_san[] = {
{COMPILER_GCC, false, "GCC", "./tests/test_gcc"},
{COMPILER_CLANG, false, "Clang", "./tests/test_clang"},
{COMPILER_GPP, false, "G++ (C++)", "./tests/test_gpp"},
{COMPILER_CLANGPP, false, "Clang++ (C++)", "./tests/test_clangpp"},
{COMPILER_GCC, false, "GCC", "./test_gcc"},
{COMPILER_CLANG, false, "Clang", "./test_clang"},
{COMPILER_GPP, false, "G++ (C++)", "./test_gpp"},
{COMPILER_CLANGPP, false, "Clang++ (C++)", "./test_clangpp"},
};
/* Clang-only (for Termux where gcc is clang) */
BuildConfig test_configs_clang_only[] = {
{COMPILER_CLANG, false, "Clang", "./tests/test_clang"},
{COMPILER_CLANGPP, false, "Clang++ (C++)", "./tests/test_clangpp"},
{COMPILER_CLANG, false, "Clang", "./test_clang"},
{COMPILER_CLANGPP, false, "Clang++ (C++)", "./test_clangpp"},
};

BuildConfig *test_configs;
Expand All @@ -395,17 +398,17 @@ int main(int argc, char **argv) {
}

BuildConfig fluent_configs_full[] = {
{COMPILER_GCC, false, "GCC Fluent", "./tests/test_fluent_gcc"},
{COMPILER_CLANG, false, "Clang Fluent", "./tests/test_fluent_clang"},
{COMPILER_GCC, false, "GCC Fluent", "./test_fluent_gcc"},
{COMPILER_CLANG, false, "Clang Fluent", "./test_fluent_clang"},
};
BuildConfig fluent_configs_clang[] = {
{COMPILER_CLANG, false, "Clang Fluent", "./tests/test_fluent_clang"},
{COMPILER_CLANG, false, "Clang Fluent", "./test_fluent_clang"},
};
BuildConfig *fluent_configs = skip_gcc ? fluent_configs_clang : fluent_configs_full;
BuildConfig demo_config = skip_gcc
? (BuildConfig){COMPILER_CLANG, false, "Demo", "./api_demo"}
: (BuildConfig){COMPILER_GCC, false, "Demo", "./api_demo"};
const char *demo_output = "./api_demo";
? (BuildConfig){COMPILER_CLANG, false, "Demo", "../api_demo"}
: (BuildConfig){COMPILER_GCC, false, "Demo", "../api_demo"};
const char *demo_output = "../api_demo";
#endif

#ifdef _WIN32
Expand All @@ -419,16 +422,16 @@ int main(int argc, char **argv) {

for (size_t i = 0; i < test_count; i++) {
LOG_INFO( " Starting build: %s", test_configs[i].name);
build_source_async(test_configs[i], "tests/test.c", NULL, &procs);
build_source_async(test_configs[i], "test.c", NULL, &procs);
}

for (size_t i = 0; i < fluent_count; i++) {
LOG_INFO( " Starting build: %s", fluent_configs[i].name);
build_source_async(fluent_configs[i], "tests/test_fluent_api.c", NULL, &procs);
build_source_async(fluent_configs[i], "test_fluent_api.c", NULL, &procs);
}

LOG_INFO( " Starting build: %s", demo_config.name);
build_source_async(demo_config, "api_demo.c", NULL, &procs);
build_source_async(demo_config, "../api_demo.c", NULL, &procs);

/* Build Python shared library */
LOG_INFO( " Starting build: Python bindings");
Expand Down Expand Up @@ -484,7 +487,7 @@ int main(int argc, char **argv) {
/* Phase 5: Valgrind (must be sequential, slow) */
if (all_ok) {
LOG_INFO( "=== Running valgrind ===");
if (!run_valgrind("./tests/test_gcc")) {
if (!run_valgrind("./test_gcc")) {
nob_log(NOB_ERROR, "Valgrind check failed");
all_ok = false;
}
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ <h2 id="build--test">Build &amp; Test</h2>
<div class="language-bash highlighter-rouge">
<div class="highlight">
<pre
class="highlight"><code>cc <span class="nt">-o</span> nob nob.c <span class="o">&amp;&amp;</span> ./nob</code></pre>
class="highlight"><code>cc <span class="nt">-o</span> build/nob build/nob.c <span class="o">&amp;&amp;</span> ./build/nob</code></pre>
</div>
</div>
<details>
Expand Down