@@ -117,31 +117,96 @@ def _lock_impl(ctx):
117117 args .run_shell .add ("--no-progress" )
118118 args .run_shell .add ("--quiet" )
119119
120+ # Generate a wrapper script that copies the existing output (if any) and
121+ # then runs uv. On POSIX, args are forwarded via exec "$@". On Windows,
122+ # the full command line is embedded in the .bat file with backslash paths
123+ # (CMD doesn't recognize forward slashes in executable paths).
124+ if ctx .attr .is_windows :
125+ ext = ".bat"
126+ lines = ["@echo off" ]
127+ else :
128+ ext = ".sh"
129+ lines = ["#!/usr/bin/env bash" , "set -euo pipefail" ]
130+
131+ python_path = getattr (python , "path" , python )
132+
120133 if ctx .files .existing_output :
121- command = '{python} -c {python_cmd} && "$@"' .format (
122- python = getattr (python , "path" , python ),
123- python_cmd = shell .quote (
124- "from shutil import copy; copy(\" {src}\" , \" {dst}\" )" .format (
134+ python_cmd = "from shutil import copy; copy(\" {src}\" , \" {dst}\" )" .format (
135+ src = ctx .files .existing_output [0 ].path ,
136+ dst = output .path ,
137+ )
138+ if ctx .attr .is_windows :
139+ # In batch files, use "" to escape internal double quotes.
140+ lines .append (
141+ "\" {py}\" -c \" from shutil import copy; copy(\" \" {src}\" \" , \" \" {dst}\" \" )\" " .format (
142+ py = python_path ,
125143 src = ctx .files .existing_output [0 ].path ,
126144 dst = output .path ,
127145 ),
146+ )
147+ else :
148+ lines .append ("{py} -c '{cmd}'" .format (
149+ py = python_path ,
150+ cmd = python_cmd ,
151+ ))
152+
153+ if ctx .attr .is_windows :
154+ # Build the command line with backslash paths for CMD.
155+ # args.run_info has most args; add the output/progress/quiet
156+ # args that were only added directly to args.run_shell.
157+ def _quote (arg ):
158+ if hasattr (arg , "path" ):
159+ arg = arg .path .replace ("/" , "\\ " )
160+ else :
161+ arg = str (arg )
162+ return '"' + arg .replace ('"' , '""' ) + '"'
163+
164+ bat_args = args .run_info + [
165+ "--output-file" ,
166+ output ,
167+ "--no-progress" ,
168+ "--quiet" ,
169+ ]
170+ lines .append (" " .join ([_quote (a ) for a in bat_args ]))
171+
172+ # Normalize CRLF line endings in the output on Windows.
173+ lines .append (
174+ "\" {py}\" -c \" import pathlib;p=pathlib.Path(r\" \" {dst}\" \" );p.write_bytes(p.read_bytes().replace(b'\\ r\\ n', b'\\ n'))\" " .format (
175+ py = python_path ,
176+ dst = output .path ,
128177 ),
129178 )
130179 else :
131- command = '"$@"'
180+ lines .append ('exec "$@"' )
181+
182+ script = ctx .actions .declare_file (ctx .label .name + "_lock" + ext )
183+ if ctx .attr .is_windows :
184+ content = "\r \n " .join (lines ) + "\r \n "
185+ else :
186+ content = "\n " .join (lines ) + "\n "
187+ ctx .actions .write (output = script , content = content , is_executable = True )
132188
133189 srcs = srcs + ctx .files .build_constraints + ctx .files .constraints
134190
135- ctx .actions .run_shell (
136- command = command ,
191+ ctx .actions .run (
192+ executable = script ,
137193 inputs = srcs + ctx .files .existing_output ,
138194 mnemonic = "PyRequirementsLockUv" ,
139195 outputs = [output ],
140- arguments = [args .run_shell ],
196+ # On Windows, the command line is embedded directly in the .bat
197+ # script (with backslash paths). On POSIX, args are forwarded via
198+ # exec "$@" in the .sh script.
199+ arguments = [args .run_shell ] if not ctx .attr .is_windows else [],
141200 tools = [
142201 uv ,
143202 python_files ,
203+ script ,
144204 ],
205+ # User reported being unable to add `--action_env` and get it to work.
206+ # Without this flag.
207+ #
208+ # Ref: https://app.slack.com/client/TA4K1KQ87/CA306CEV6
209+ use_default_shell_env = True ,
145210 progress_message = "Creating a requirements.txt with uv: %{label}" ,
146211 env = ctx .attr .env ,
147212 )
@@ -205,6 +270,7 @@ modifications and the locking is not done from scratch.
205270 doc = "Public, see the docs in the macro." ,
206271 default = True ,
207272 ),
273+ "is_windows" : attr .bool (mandatory = True ),
208274 "output" : attr .string (
209275 doc = "Public, see the docs in the macro." ,
210276 mandatory = True ,
@@ -241,7 +307,7 @@ The string to input for the 'uv pip compile'.
241307def _lock_run_impl (ctx ):
242308 if ctx .attr .is_windows :
243309 path_sep = "\\ "
244- ext = ".exe "
310+ ext = ".bat "
245311 else :
246312 path_sep = "/"
247313 ext = ""
@@ -250,7 +316,12 @@ def _lock_run_impl(ctx):
250316 if hasattr (arg , "short_path" ):
251317 arg = arg .short_path
252318
253- return shell .quote (arg .replace ("/" , path_sep ))
319+ arg = arg .replace ("/" , path_sep )
320+ if ctx .attr .is_windows :
321+ # On Windows, CMD uses double quotes for quoting, and internal
322+ # double quotes are escaped by doubling them.
323+ return '"' + arg .replace ('"' , '""' ) + '"'
324+ return shell .quote (arg )
254325
255326 info = ctx .attr .lock [_RunLockInfo ]
256327 executable = ctx .actions .declare_file (ctx .label .name + ext )
@@ -438,6 +509,10 @@ def lock(
438509 env = env ,
439510 existing_output = maybe_out ,
440511 generate_hashes = generate_hashes ,
512+ is_windows = select ({
513+ "@platforms//os:windows" : True ,
514+ "//conditions:default" : False ,
515+ }),
441516 python_version = python_version ,
442517 srcs = srcs ,
443518 strip_extras = strip_extras ,
0 commit comments