diff --git a/README.md b/README.md index f27399e4..6f1a7772 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,13 @@ # esp-ice +> **Experimental PoC** -- this is a proof of concept, tested +> primarily on Linux. Windows may not work as expected, or at +> all. Not intended for production use. + *Ice ice baby. Too cold -- slice like a ninja, cut like a razor blade.* +![ice walkthrough](demo/demo.gif) + ## What is ice? `ice` is a single-binary frontend for ESP-IDF projects. It replaces @@ -22,9 +28,6 @@ Highlights: - Managed ESP-IDF reference under `~/.ice/` with cheap named checkouts that share git objects across versions. -> **Experimental PoC** -- this project is a proof of concept and is -> not intended for production use. - ## Install ### Prebuilt binaries diff --git a/cmd/init/init.c b/cmd/init/init.c index 7b51981b..12ef4461 100644 --- a/cmd/init/init.c +++ b/cmd/init/init.c @@ -230,12 +230,21 @@ static int in_list(const char *target, const char *const *list) * * A bare name (no separator, no leading @b{.} / @b{~} / @b{/}) maps to * @b{~/.ice/checkouts//}, mirroring @b{ice repo checkout}'s - * shorthand. Anything else is taken verbatim. Returns a malloc'd - * string the caller owns. + * shorthand. Anything else is taken verbatim. The result is then + * canonicalized to an absolute path via @c path_realpath: this value + * is exported as @c IDF_PATH and embedded in @c .ice/config, and + * esp-idf's @c project.cmake runs @c try_compile from a temp dir + * under @c build/, where a relative @c IDF_PATH resolves against the + * wrong cwd and the generated @c build/toolchain/toolchain-.cmake's + * @c include($ENV{IDF_PATH}/...) fails. If the path doesn't exist + * yet, fall through with the verbatim string -- the tools/tools.json + * check below will report it. Returns a malloc'd string the caller + * owns. */ static char *resolve_idf_arg(const char *arg) { struct sbuf p = SBUF_INIT; + char *real; int bare; bare = *arg && arg[0] != '/' && arg[0] != '.' && arg[0] != '~' && @@ -245,6 +254,12 @@ static char *resolve_idf_arg(const char *arg) sbuf_addf(&p, "%s/checkouts/%s", ice_home(), arg); else sbuf_addstr(&p, arg); + + real = path_realpath(p.buf); + if (real) { + sbuf_release(&p); + return real; + } return sbuf_detach(&p); } diff --git a/cmd/target/openocd/openocd.c b/cmd/target/openocd/openocd.c index 31cabfd0..80629eb9 100644 --- a/cmd/target/openocd/openocd.c +++ b/cmd/target/openocd/openocd.c @@ -355,7 +355,8 @@ static char *resolve_gdb(const struct debug_chip *dc) * openocd terminal. */ static int spawn_openocd(struct process *proc, const char *openocd_bin, - const char *openocd_version, char *openocd_cmd_buf) + const char *openocd_version, char *openocd_cmd_buf, + struct sbuf *prelog) { struct svec argv = SVEC_INIT; const char *env_kv[2] = {NULL, NULL}; @@ -405,8 +406,11 @@ static int spawn_openocd(struct process *proc, const char *openocd_bin, * the gdb stub up. Stream what we read to stderr in real time so * the user sees the banner / probe progress before the alt screen * takes over (and so failure diagnostics are visible without - * digging in scrollback). */ - struct sbuf prelog = SBUF_INIT; + * digging in scrollback). Bytes are also accumulated in @p prelog + * for the caller -- on success @ref run_debug seeds the gdb pane's + * scrollback with this so the banner is reachable from inside the + * alt screen via Ctrl-T inspect; on failure the @c{see banner + * above} diagnostic in the caller's stderr is enough. */ int ready = 0; int daemon_exited = 0; @@ -420,7 +424,7 @@ static int spawn_openocd(struct process *proc, const char *openocd_bin, if (n > 0) { fwrite(buf, 1, (size_t)n, stderr); fflush(stderr); - sbuf_add(&prelog, buf, (size_t)n); + sbuf_add(prelog, buf, (size_t)n); /* sbuf is NUL-terminated and OpenOCD's output is * text, so strstr is safe and portable (memmem is * a GNU extension we don't depend on elsewhere). @@ -437,7 +441,7 @@ static int spawn_openocd(struct process *proc, const char *openocd_bin, * OpenOCD finishes init). The "for gdb connections" * suffix is uniquely emitted by gdb_server.c's * listener, so the match holds across versions. */ - if (strstr(prelog.buf, "for gdb connections")) { + if (strstr(prelog->buf, "for gdb connections")) { ready = 1; break; } @@ -456,12 +460,10 @@ static int spawn_openocd(struct process *proc, const char *openocd_bin, kill(proc->pid, SIGTERM); process_finish(proc); svec_clear(&argv); - sbuf_release(&prelog); sbuf_release(&scripts_env); return -1; } - sbuf_release(&prelog); /* argv strings are owned by svec; the process struct stores * proc->argv as a borrow. Clearing svec here is safe: the child * has already exec'd, so its argv copy is independent. */ @@ -642,7 +644,7 @@ static const char DEBUG_HELP_TEXT[] = static int run_debug(struct process *oocd_proc, const char *gdb_bin, const char *elf, struct serial *uart_s, const char *port_label, unsigned baud, - const char *chip_label) + const char *chip_label, const struct sbuf *prelog) { int rc = term_raw_enter(TERM_RAW_MOUSE | TERM_RAW_BRACKETED_PASTE); if (rc < 0) @@ -666,13 +668,72 @@ static int run_debug(struct process *oocd_proc, const char *gdb_bin, struct tui_rect status_r, divider_r; debug_layout(rows, cols, &gdb_p, &uart_p, &status_r, ÷r_r); - /* gdb in a pty pre-loaded with target remote : + ELF. */ + /* Seed the gdb pane's vt100 with the openocd startup banner + * captured during @ref spawn_openocd. The banner already streamed + * to the user's stderr before the alt screen took over (so failure + * diagnostics stay visible after exit), but inside the alt screen + * it's otherwise lost. Feeding it to the live grid (rather than + * the scrollback ring via @ref tui_log_append) means the banner + * occupies the top of the pane on entry and is naturally pushed + * into the ring as gdb's output fills the grid -- you don't have + * to PgUp to see it. Drained explicitly so any rows that scroll + * off the grid land in the ring before the first @ref pump_pipe + * gets a chance to. */ + if (prelog && prelog->len) { + vt100_input(gdb_p.V, prelog->buf, prelog->len); + if (!tui_log_is_frozen(&gdb_p.L)) + tui_log_pull_from_vt100(&gdb_p.L, gdb_p.V); + } + + /* gdb in a pty pre-loaded with the standard ESP-IDF startup + * sequence, mirroring tools/cmake/gdbinit.cmake's "connect" + * fragment: + * + * target remote : attach + * monitor reset halt reset chip + halt at reset vector + * maintenance flush register-cache + * drop gdb's stale cached regs + * thbreak app_main temp HW breakpoint at user entry + * continue run from reset vector to app_main + * + * Without this preamble openocd's own @c{init} resets the chip + * (so the "preserve chip state for post-mortem" framing this + * cmd used to claim was already untrue -- the standard + * board files do @c{reset halt} regardless of what we type), + * but the chip then runs free for a few hundred ms before gdb + * dials in. By the time gdb's connect-halt lands, PC happens + * to be sitting right on @c{app_main}'s prologue -- and if the + * user then types @c{b app_main; c}, the HW breakpoint they + * just armed and the resume PC are the same address, so the + * very first fetch on resume re-trips the match register + * before the instruction commits. openocd-esp32's recovery + * for "halted at PC == HW-breakpoint address right after a + * resume" is to software-reset CPU0, which puts the chip back + * at the reset vector, the chip runs through boot, hits + * @c{app_main} again, breakpoint fires for real this time -- + * and the user @c{c}s, and we loop. Loops at ~10 Hz, no + * @c{printf} ever flushes to UART. + * + * The preamble side-steps the trap entirely: we issue our own + * @c{reset halt} so the chip is at the reset vector, install a + * *temporary* HW breakpoint at @c{app_main}, and let the chip + * run through boot. The temp breakpoint fires once when the + * boot path reaches @c{app_main}, gdb auto-removes it, the + * chip is parked at @c{app_main} with no breakpoint at PC, and + * subsequent user @c{b app_main; c} works because the + * breakpoint isn't sitting on the resume PC any longer (the + * one-instruction step over the freshly-installed breakpoint + * is the standard auto-skip path that does work). */ struct sbuf gdb_remote = SBUF_INIT; sbuf_addf(&gdb_remote, "target remote :%d", opt_gdb_port); const char *gdb_argv[] = {gdb_bin, "-q", "-ex", "set pagination off", "-ex", "set confirm off", "-ex", gdb_remote.buf, + "-ex", "monitor reset halt", + "-ex", "maintenance flush register-cache", + "-ex", "thbreak app_main", + "-ex", "continue", elf, NULL}; struct process gdb_proc = PROCESS_INIT; gdb_proc.argv = gdb_argv; @@ -695,16 +756,17 @@ static int run_debug(struct process *oocd_proc, const char *gdb_bin, return 1; } - /* One-line hint at the top of the UART pane: ice debug attaches - * without resetting (preserves chip state for post-mortem -- the - * key reason to use JTAG-attach in the first place), so a - * long-running app's UART pane will look "empty" until the user - * either interacts with the chip or restarts it. Tell them how. */ - static const char hint[] = - "\x1b[2m" - "ice debug: attached, chip state preserved. " - "Ctrl-T r resets and shows boot logs.\n" - "\x1b[0m"; + /* One-line hint at the top of the UART pane: gdb's `target + * remote` halts the chip on attach, so the UART pane stays + * silent until the inferior runs again -- tell the user how to + * resume. Yellow so it stands out on the first glance; CRLF + * because vt100's bare LF only moves the cursor down (no column + * reset), which would leave subsequent UART output starting at + * the column past the hint instead of column 1. */ + static const char hint[] = "\x1b[33m" + "ice debug: chip halted on attach -- " + "run 'continue' in gdb to resume.\r\n" + "\x1b[0m"; vt100_input(uart_p.V, hint, sizeof hint - 1); int focus = 0; @@ -1020,12 +1082,16 @@ int cmd_target_openocd(int argc, const char **argv) * @c opt_port has already absorbed @c --port, @c $ESPPORT, and the * configured @c serial.port via @c OPT_STRING_CFG. If still unset, * pick a port passively (no @c open(), no DTR/RTS toggle, no ROM - * handshake). ice debug attaches to a *running* chip, so the - * destructive @ref esf_find_esp_port probe used by ice flash is the - * wrong tool here -- it would reset the chip into ROM mode and back - * just to read the chip id, and on a chip whose USB-Serial/JTAG is - * the JTAG transport that renumeration races OpenOCD's libusb scan - * and reliably breaks the attach. + * handshake). The destructive @ref esf_find_esp_port probe used + * by ice flash is the wrong tool here -- it would toggle the + * UART's DTR/RTS to bounce the chip into ROM bootloader and back + * just to read the chip id, and on a chip whose USB-Serial/JTAG + * is the JTAG transport that renumeration races OpenOCD's libusb + * scan and reliably breaks the attach. (Note: openocd's own + * @c{init} does a JTAG-side @c{reset halt} on the chip regardless, + * so this isn't about preserving chip state -- it's about not + * adding a *second* reset path through DTR/RTS that would + * interfere with the JTAG one.) */ char *autoport = NULL; if (!opt_port) { @@ -1073,10 +1139,15 @@ int cmd_target_openocd(int argc, const char **argv) * we don't trip the const-qualifier on opt_openocd_cmd. */ char *oocd_cmd_owned = sbuf_strdup(opt_openocd_cmd); struct process oocd_proc = PROCESS_INIT; + /* Owned here, filled by spawn_openocd, consumed by run_debug to + * seed the gdb pane's scrollback so the openocd startup banner + * stays reachable inside the alt screen. */ + struct sbuf prelog = SBUF_INIT; fprintf(stderr, "Starting openocd ...\n"); if (spawn_openocd(&oocd_proc, openocd_bin, openocd_version, - oocd_cmd_owned) < 0) { + oocd_cmd_owned, &prelog) < 0) { + sbuf_release(&prelog); free(oocd_cmd_owned); serial_close(s); free(autoport); @@ -1089,8 +1160,9 @@ int cmd_target_openocd(int argc, const char **argv) /* ---- run dual-pane ---- */ const char *chip_label = opt_chip ? opt_chip : "?"; int run_rc = run_debug(&oocd_proc, gdb_bin, opt_elf, s, opt_port, - (unsigned)opt_baud, chip_label); + (unsigned)opt_baud, chip_label, &prelog); + sbuf_release(&prelog); free(oocd_cmd_owned); serial_close(s); free(autoport); diff --git a/demo/demo.gif b/demo/demo.gif new file mode 100644 index 00000000..8ff118d2 Binary files /dev/null and b/demo/demo.gif differ diff --git a/demo/demo.tape b/demo/demo.tape new file mode 100644 index 00000000..1f131863 --- /dev/null +++ b/demo/demo.tape @@ -0,0 +1,247 @@ +# SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD +# +# SPDX-License-Identifier: Apache-2.0 +# +# demo/demo.tape -- end-to-end walkthrough for the README. +# +# Drives ice from a fresh checkout of esp-idf master through the +# hello_world example: completion -> repo checkout -> init -> +# menuconfig -> build -> flash -> monitor -> qemu -> qemu --debug -> +# ice debug. Real chip + real QEMU + real OpenOCD; render this on +# the host the chip is wired to. +# +# Render with vhs (https://github.com/charmbracelet/vhs): +# cd ~/work/esp-ice && vhs demo/demo.tape +# +# Requires: +# - vhs, ttyd, ffmpeg +# - ice on PATH +# - ESP32-S3 attached on /dev/ttyUSB0 (UART) and /dev/ttyACM0 (USB-JTAG) +# - ~/.ice/esp-idf reference already populated; do at least one +# `ice repo checkout master /tmp/warm` before recording so the +# submodule packs are cached -- otherwise step 2 will record several +# minutes of cloning into the GIF. +# +# All steps run in real time -- nothing is hidden. USB-JTAG is used +# for flashing because it's faster than UART. +# +# Progress-driven steps (repo checkout, init, build, flash) sync via +# `Wait+Screen / done/` so the next Type fires only after ice has +# fully released stdin -- vhs's wall-clock Sleep otherwise outruns the +# operation, and bytes typed while ice is still in raw mode (polling +# stdin for the Ctrl-V verbose toggle) get eaten before bash regains +# control. +Screen (vs the default +Line) is needed because the bash +# prompt overwrites the last line right after ice's "✓ done." +# success line, racing the matcher. + +Output demo/demo.gif + +Set Shell bash +Set FontFamily "Adwaita Mono" +Set FontSize 13 +Set Width 1100 +Set Height 720 +Set Padding 14 +Set Theme { "name": "ice-demo", "background": "#000000", "foreground": "#E5E5E5", "cursor": "#FFFFFF", "selection": "#444444", "black": "#000000", "red": "#CC0000", "green": "#4E9A06", "yellow": "#C4A000", "blue": "#3465A4", "magenta": "#75507B", "cyan": "#06989A", "white": "#D3D7CF", "brightBlack": "#555753", "brightRed": "#EF2929", "brightGreen": "#8AE234", "brightYellow": "#FCE94F", "brightBlue": "#729FCF", "brightMagenta": "#AD7FA8", "brightCyan": "#34E2E2", "brightWhite": "#EEEEEC" } +Set TypingSpeed 50ms + +# Start in demo/, throw away any previous esp-idf checkout, clean prompt. +Hide +Type "cd demo && rm -rf esp-idf" +Enter +Type "export PS1='$ '" +Enter +Type "clear" +Enter +Sleep 1000ms +Show + +# --------------------------------------------------------------------------- +# 1. Enable tab completion. +# --------------------------------------------------------------------------- +Type "# enable tab completion" +Sleep 1000ms +Enter +Type `eval "$(ice completion bash)"` +Sleep 1000ms +Enter +Sleep 800ms + +# --------------------------------------------------------------------------- +# 2. Local checkout of esp-idf master (clones from ~/.ice/esp-idf). +# --------------------------------------------------------------------------- +Type "# check out esp-idf master into ./esp-idf" +Sleep 1000ms +Enter +Type "ice repo checkout master ./esp-idf" +Sleep 1000ms +Enter +Wait+Screen@60s /Checking out done/ +Sleep 1000ms + +# --------------------------------------------------------------------------- +# 3. Hop into the hello_world example. +# --------------------------------------------------------------------------- +Type "# move into the hello_world example" +Sleep 1000ms +Enter +Type "cd esp-idf/examples/get-started/hello_world/" +Sleep 1000ms +Enter +Sleep 800ms + +# --------------------------------------------------------------------------- +# 4. Bind the project to esp32s3 + the local esp-idf checkout (../../.. +# from hello_world/). +# --------------------------------------------------------------------------- +Type "# bind the project to esp32-s3 and the local esp-idf checkout" +Sleep 1000ms +Enter +Type "ice init esp32s3 ../../.." +Sleep 1000ms +Enter +Wait+Screen@60s /Configuring done/ +Sleep 1000ms + +# --------------------------------------------------------------------------- +# 5. menuconfig -- ice's native Kconfig TUI. Navigate, peek at help, +# quit without saving so the example's sdkconfig stays as init left it. +# --------------------------------------------------------------------------- +Type "# launch menuconfig (ice's native Kconfig TUI)" +Sleep 1000ms +Enter +Type "ice menuconfig" +Sleep 1000ms +Enter +Sleep 2500ms +Down +Sleep 250ms +Down +Sleep 250ms +Down +Sleep 250ms +Down +Sleep 350ms +Enter +Sleep 1500ms +Type "?" +Sleep 2500ms +Escape +Sleep 500ms +Escape +Sleep 500ms +Type "q" +Sleep 1500ms + +# --------------------------------------------------------------------------- +# 6. ice build. Spinner-only -- Ctrl-V verbose toggle works interactively +# but tends to overwhelm vhs's screen tracking when ninja prints +# hundreds of lines through it. +# --------------------------------------------------------------------------- +Type "# build the firmware" +Sleep 1000ms +Enter +Type "ice build" +Sleep 1000ms +Enter +# Wait for the build to finish. +Wait+Screen@5m /Building done/ +# Linger on the ✓ Building done. (...) line. +Sleep 2000ms + +# --------------------------------------------------------------------------- +# 7. ice flash via USB-JTAG -- much faster than UART on this image. +# --------------------------------------------------------------------------- +Type "# flash via USB-JTAG (faster than UART on this image)" +Sleep 1000ms +Enter +Type "ice flash --port /dev/ttyACM0" +Sleep 1000ms +Enter +# Wait for flash to finish, then linger on ✓ Flashing done. +Wait+Screen@60s /Flashing done/ +Sleep 2000ms + +# --------------------------------------------------------------------------- +# 8. Monitor -- live UART through ice's vt100 pipeline. Ctrl-] exits. +# --------------------------------------------------------------------------- +Type "# attach the serial monitor (Ctrl-] exits)" +Sleep 1000ms +Enter +Type "ice monitor" +Sleep 1000ms +Enter +Sleep 5000ms +Ctrl+] +Sleep 1000ms + +# --------------------------------------------------------------------------- +# 9. QEMU -- run the same firmware in qemu-system-xtensa. Ctrl-T x exits. +# --------------------------------------------------------------------------- +Type "# run the firmware under QEMU (Ctrl-T x exits)" +Sleep 1000ms +Enter +Type "ice qemu" +Sleep 1000ms +Enter +Sleep 5000ms +Ctrl+T +Sleep 100ms +Type "x" +Sleep 1500ms + +# --------------------------------------------------------------------------- +# 10. QEMU --debug -- gdb on top, UART on the bottom. QEMU starts paused; +# `c` in the gdb pane releases the chip and the UART pane fills with the +# boot log. +# --------------------------------------------------------------------------- +Type "# QEMU + gdb dual-pane (c continues, Ctrl-T x exits)" +Sleep 1000ms +Enter +Type "ice qemu --debug" +Sleep 1000ms +Enter +Sleep 3000ms +Type "c" +Sleep 1000ms +Enter +Sleep 5000ms +Ctrl+T +Sleep 100ms +Type "x" +Sleep 1500ms + +# --------------------------------------------------------------------------- +# 11. Real debug -- OpenOCD + xtensa-esp-elf-gdb against the chip. +# Reset, set a breakpoint at app_main, continue, backtrace at the hit. +# --------------------------------------------------------------------------- +Type "# debug the real chip via OpenOCD + gdb" +Sleep 1000ms +Enter +Type "ice debug" +Sleep 1000ms +Enter +# OpenOCD spawns + gdb attaches and halts the chip. +Sleep 3000ms + +# Break on app_main. +Type "b app_main" +Sleep 1000ms +Enter + +# Continue -- bootloader runs, app starts, breakpoint hits at app_main entry. +Type "c" +Sleep 1000ms +Enter +Sleep 11000ms + +# Show the backtrace at the hit. +Type "bt" +Sleep 1000ms +Enter +Sleep 2000ms + +# Exit the dual-pane TUI. +Ctrl+T +Sleep 100ms +Type "x" diff --git a/platform.h b/platform.h index 786536fd..8cf0c43e 100644 --- a/platform.h +++ b/platform.h @@ -312,6 +312,28 @@ int self_pid(void); */ const char *temp_dir(void); +/** + * @brief Canonicalize @p path to an absolute, normalized form. + * + * POSIX: @c realpath() with the @c NULL second-argument extension -- + * resolves symlinks and collapses @c "." / @c ".." components. All + * components must exist; for a non-existent path returns NULL with + * @c errno set. + * + * Windows: @c GetFullPathNameW() round-tripped through UTF-8. Makes + * the path absolute and normalizes separators / @c "." / @c "..", but + * does @b{not} resolve symlinks (rare on Windows; callers needing + * full resolution should use @c GetFinalPathNameByHandle on a handle + * directly). Does not require the path to exist. + * + * Both platforms return a malloc'd string the caller must @c free(). + * + * @param path Path to canonicalize. + * @return Malloc'd absolute path on success, NULL on failure (errno + * set on POSIX; @c GetLastError() on Windows). + */ +char *path_realpath(const char *path); + /* ------------------------------------------------------------------ */ /* Child process API */ /* ------------------------------------------------------------------ */ diff --git a/platform/posix/io.c b/platform/posix/io.c index 499fb090..1098943b 100644 --- a/platform/posix/io.c +++ b/platform/posix/io.c @@ -6,12 +6,23 @@ /** * @file platform/posix/io.c - * @brief Color-aware I/O overrides for POSIX. + * @brief Color-aware I/O overrides and filesystem helpers for POSIX. * * IMPORTANT: This file captures the real C-library fputs pointer * before ice.h overrides it. ice.h is NOT the first include. * (Same pattern as platform/win/wconv.c -- see its file-level comment.) */ +/* Pull in X/Open extensions -- the build's @c -D_POSIX_C_SOURCE=200112L + * doesn't expose realpath() under glibc. Must be set before any system + * header is included. The leading-underscore name is reserved for the + * implementation in general but is the canonical feature-test macro + * spelling here, so silence clang-tidy's bugprone-reserved-identifier + * check on this specific line. */ +#ifndef _XOPEN_SOURCE +/* NOLINTNEXTLINE(bugprone-reserved-identifier) */ +#define _XOPEN_SOURCE 500 +#endif + #include #include #include @@ -124,3 +135,12 @@ int dir_foreach(const char *path, int (*cb)(const char *name, void *ud), svec_clear(&names); return rc; } + +char *path_realpath(const char *path) +{ + /* The NULL second-argument extension allocates the result with + * malloc(). Standardized in POSIX.1-2008; pre-2008 glibc has + * supported it for two decades. _XOPEN_SOURCE=500 above exposes + * realpath() in . */ + return realpath(path, NULL); +} diff --git a/platform/win/io.c b/platform/win/io.c index 2fad4efe..25a2297a 100644 --- a/platform/win/io.c +++ b/platform/win/io.c @@ -891,6 +891,44 @@ char *getcwd_w(char *buf, size_t size) return buf; } +/* + * Canonicalize a path to absolute form. GetFullPathNameW makes the + * path absolute, normalizes separators, and collapses "."/".."; it + * does not resolve symlinks (rare on Windows -- see platform.h doc). + * Two-call dance: first to discover the required buffer length, then + * to fill it. Result is a malloc'd UTF-8 string the caller frees. + */ +char *path_realpath(const char *path) +{ + wchar_t *wpath; + wchar_t *wfull = NULL; + char *result = NULL; + DWORD n, m; + + wpath = mbs_to_wcs(path); + if (!wpath) + return NULL; + + n = GetFullPathNameW(wpath, 0, NULL, NULL); + if (n == 0) + goto done; + + wfull = malloc(n * sizeof(wchar_t)); + if (!wfull) + goto done; + + m = GetFullPathNameW(wpath, n, wfull, NULL); + if (m == 0 || m >= n) + goto done; + + result = wcs_to_mbs(wfull); + +done: + free(wpath); + free(wfull); + return result; +} + /* * Atomic-replace rename: POSIX rename() already replaces an existing * target atomically, but the Windows CRT rename() fails with EEXIST.