From 986908a058cd56d8e80263d03fd77f04436f905c Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Fri, 19 Dec 2025 18:29:40 +0000 Subject: [PATCH] fix quoting issue --- scripts/dev_commands.py | 53 ++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/scripts/dev_commands.py b/scripts/dev_commands.py index 938e98c..38ae092 100644 --- a/scripts/dev_commands.py +++ b/scripts/dev_commands.py @@ -7,6 +7,11 @@ Design goals: - Explicit argv execution throughout. - Where practical, allow forwarding extra arguments to underlying tools. - Keep Windows/MSVC "one-liner" behavior via cmd.exe only when required. + +NOTE (Windows quoting): +cmd.exe has special/quirky parsing rules for /c when the command contains quotes. +We use: cmd.exe /d /s /c ""call "" && "" +The doubled quotes at both ends are intentional and required for robust behavior. """ from __future__ import annotations @@ -21,7 +26,6 @@ from typing import Iterable, Sequence IS_WINDOWS = platform.system() == "Windows" -# Generator presets and default build dirs DEFAULT_GENERATOR = "ninja-msvc" if IS_WINDOWS else "ninja" GENERATOR_DEFAULT_DIR = { "vs": "build", @@ -43,7 +47,6 @@ DEFAULT_VCVARSALL = ( def _print_cmd(argv: Sequence[str]) -> None: - # Use subprocess.list2cmdline for Windows-friendly rendering. if IS_WINDOWS: rendered = subprocess.list2cmdline(list(argv)) else: @@ -52,11 +55,12 @@ def _print_cmd(argv: Sequence[str]) -> None: def _sh_quote(s: str) -> str: - # Minimal POSIX shell quoting for display-only. if not s: return "''" - safe = set("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - "._-/:@=+") + safe = set( + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + "._-/:@=+" + ) if all(c in safe for c in s): return s return "'" + s.replace("'", "'\"'\"'") + "'" @@ -71,16 +75,13 @@ def run_argvs(argvs: Iterable[Sequence[str]], dry_run: bool) -> None: def _as_build_dir(path_str: str | None, fallback: str) -> str: - if path_str: - return path_str - return fallback + return path_str or fallback def dependencies(args: argparse.Namespace) -> None: cmd_detect = ["conan", "profile", "detect", "-f"] cmd_install = ["conan", "install", ".", "-of", "build", "-b", "missing"] - # Allow forward of arbitrary conan install args (e.g. -s, -o, -c). if args.conan_install_args: cmd_install.extend(args.conan_install_args) @@ -98,7 +99,6 @@ def configure(args: argparse.Namespace) -> None: if generator == "vs": cmake_args.extend(["-G", CMAKE_GENERATOR["vs"]]) - # Multi-config generators typically ignore CMAKE_BUILD_TYPE. else: cmake_args.extend(["-G", CMAKE_GENERATOR[generator]]) cmake_args.append(f"-DCMAKE_BUILD_TYPE={args.build_type}") @@ -118,7 +118,6 @@ def build(args: argparse.Namespace) -> None: if args.target: cmd.extend(["--target", args.target]) - # Forward extra args to the underlying build tool after "--". if args.build_tool_args: cmd.append("--") cmd.extend(args.build_tool_args) @@ -126,15 +125,27 @@ def build(args: argparse.Namespace) -> None: run_argvs([cmd], args.dry_run) -def _cmd_one_liner(call_parts: Sequence[str], then_parts: Sequence[str]) -> list[str]: +def _cmd_quote_arg_for_display(argv: Sequence[str]) -> str: + # Display-only: how this argv might look as a single command line. + return subprocess.list2cmdline(list(argv)) + + +def _cmd_one_liner_vcvars_then( + bat: str, + arch: str, + then_parts: Sequence[str], +) -> list[str]: """ - Build: cmd.exe /c "call && " - Uses list2cmdline to safely quote for cmd.exe. + Robust cmd.exe invocation for: + call "" && + + Uses cmd.exe quote rules: + cmd.exe /d /s /c ""call "" && "" """ - call_str = "call " + subprocess.list2cmdline(list(call_parts)) - then_str = subprocess.list2cmdline(list(then_parts)) - inner = f"{call_str} && {then_str}" - return ["cmd.exe", "/c", inner] + then_cmdline = _cmd_quote_arg_for_display(then_parts) + inner = f'call "{bat}" {arch} && {then_cmdline}' + wrapped = f'""{inner}""' + return ["cmd.exe", "/d", "/s", "/c", wrapped] def msvc_quick(args: argparse.Namespace) -> None: @@ -144,8 +155,6 @@ def msvc_quick(args: argparse.Namespace) -> None: bat = args.bat_path or DEFAULT_VCVARSALL arch = args.arch or "x64" - # Default action: build (mirrors the README-style one-liner) - # Users can override the "then" command via: msvc-quick -- if args.then_command: then_cmd = list(args.then_command) else: @@ -159,7 +168,7 @@ def msvc_quick(args: argparse.Namespace) -> None: then_cmd.append("--") then_cmd.extend(args.build_tool_args) - cmd = _cmd_one_liner([bat, arch], then_cmd) + cmd = _cmd_one_liner_vcvars_then(bat, arch, then_cmd) run_argvs([cmd], args.dry_run) @@ -249,7 +258,7 @@ def main() -> int: msvc = subparsers.add_parser( "msvc-quick", - help="run a VS Developer Prompt call + follow-on command (README one-liner style)", + help="run a VS env setup + follow-on command (README one-liner style)", ) msvc.add_argument( "--bat-path",