Skip to content

Commit 8eebc79

Browse files
committed
Fix build on Windows
1 parent 1d2d085 commit 8eebc79

9 files changed

Lines changed: 272 additions & 14 deletions

File tree

.github/workflows/release.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -507,6 +507,12 @@ jobs:
507507
with:
508508
python-version: ${{ env.CI_PYTHON }}
509509

510+
- name: Setup MSVC developer environment
511+
if: matrix.target == 'x86_64-pc-windows-msvc'
512+
uses: ilammy/msvc-dev-cmd@v1
513+
with:
514+
arch: x64
515+
510516
- name: Install Linux cross-compiler for arm64
511517
if: matrix.target == 'aarch64-unknown-linux-gnu'
512518
shell: bash

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22

33
.DS_Store
44
__pycache__
5+
/.tmp

docs/cpp-api/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
<div class="headertitle"><div class="title">OpenPit C++ SDK </div></div>
120120
</div><!--header-->
121121
<div class="contents">
122-
<div class="textblock"><p><a class="anchor" id="md__2Users_2palchukovsky_2Projects_2pit_2pit_2bindings_2cpp_2DoxygenMainPage"></a></p>
122+
<div class="textblock"><p><a class="anchor" id="openpit-cpp-sdk-mainpage"></a></p>
123123
<p>The OpenPit C++ SDK is a C++17 interface for embedding the OpenPit pre-trade risk engine in CMake projects.</p>
124124
<p>Install the package generated by <span class="tt">cmake --install</span> and consume it with:</p>
125125
<div class="fragment"><div class="line">find_package(OpenPit CONFIG REQUIRED)</div>

pipeline.just

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,9 @@ build-ffi-target-release target:
312312
rustflags="${rustflags:+${rustflags} }--remap-path-prefix=$(pwd)=."
313313
case "{{ target }}" in
314314
*-pc-windows-msvc)
315-
rustflags="${rustflags} -C target-feature=+crt-static"
315+
export RUSTFLAGS="${rustflags}"
316+
PIT_WINDOWS_TARGET="{{ target }}" {{ bootstrap_python }} {{ just_helper }} build-ffi release
317+
exit 0
316318
;;
317319
esac
318320
export RUSTFLAGS="${rustflags}"

scripts/_generate_api_c_dlsym.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,9 @@ def generate(
217217
header_path: Path = DLSYM_HEADER_PATH, output_path: Path = DLSYM_OUTPUT_PATH
218218
) -> None:
219219
functions = parse_dlsym_functions(header_path.read_text(encoding="utf-8"))
220-
output_path.write_text(render_dlsym_source(functions), encoding="utf-8")
220+
output_path.write_text(
221+
render_dlsym_source(functions), encoding="utf-8", newline="\n"
222+
)
221223
with contextlib.suppress(ValueError):
222224
output_path = output_path.relative_to(ROOT)
223225
print(f"Generated {len(functions)} wrappers -> {output_path}")

scripts/_generate_api_c_h.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,12 +382,12 @@ def main() -> None:
382382

383383
HEADER_PATH.parent.mkdir(parents=True, exist_ok=True)
384384
DOCS_DIR.mkdir(parents=True, exist_ok=True)
385-
HEADER_PATH.write_text(header, encoding="utf-8")
385+
HEADER_PATH.write_text(header, encoding="utf-8", newline="\n")
386386
for path in DOCS_DIR.glob("*.md"):
387387
path.unlink()
388388
for rel_path, text in docs.items():
389389
path = DOCS_DIR / rel_path
390-
path.write_text(text, encoding="utf-8")
390+
path.write_text(text, encoding="utf-8", newline="\n")
391391
print(f"Generated {path.relative_to(ROOT)}")
392392
print(f"Generated {HEADER_PATH.relative_to(ROOT)}")
393393

scripts/_generate_api_c_sitemap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def generate() -> None:
174174
if existing == canonical:
175175
return
176176
SITEMAP_PATH.parent.mkdir(parents=True, exist_ok=True)
177-
SITEMAP_PATH.write_text(render_sitemap(canonical), encoding="utf-8")
177+
SITEMAP_PATH.write_text(render_sitemap(canonical), encoding="utf-8", newline="\n")
178178
print(f"Generated {SITEMAP_PATH.relative_to(ROOT)}")
179179

180180

scripts/just_helpers.py

Lines changed: 135 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,15 @@ def prepend_path(env: dict[str, str], directory: Path) -> None:
175175
env[path_key] = str(directory) + os.pathsep + current
176176

177177

178+
def configure_windows_temp_env(env: dict[str, str]) -> None:
179+
if not is_windows():
180+
return
181+
temp_dir = ROOT / ".tmp" / "subprocess-temp"
182+
temp_dir.mkdir(parents=True, exist_ok=True)
183+
env["TEMP"] = str(temp_dir)
184+
env["TMP"] = str(temp_dir)
185+
186+
178187
def configure_windows_rust_env(env: dict[str, str], cargo: Sequence[str]) -> bool:
179188
rustup_fallback = (
180189
is_windows()
@@ -192,6 +201,100 @@ def configure_windows_rust_env(env: dict[str, str], cargo: Sequence[str]) -> boo
192201
return True
193202

194203

204+
def env_get(env: dict[str, str], name: str) -> str:
205+
value = env.get(name)
206+
if value is not None:
207+
return value
208+
name_lower = name.lower()
209+
for key, current in env.items():
210+
if key.lower() == name_lower:
211+
return current
212+
return ""
213+
214+
215+
def cargo_target_linker_env_var(target: str) -> str:
216+
normalized = target.upper().replace("-", "_")
217+
return f"CARGO_TARGET_{normalized}_LINKER"
218+
219+
220+
def msvc_target_arch_dir(target: str) -> str:
221+
if target.startswith("x86_64-"):
222+
return "x64"
223+
if target.startswith("aarch64-"):
224+
return "arm64"
225+
if target.startswith("i686-"):
226+
return "x86"
227+
raise SystemExit(f"unsupported MSVC target for linker lookup: {target}")
228+
229+
230+
def msvc_host_arch_dir() -> str:
231+
if host_arch() == "arm64":
232+
return "Hostarm64"
233+
return "Hostx64"
234+
235+
236+
def msvc_linker_candidates(env: dict[str, str], target: str) -> list[Path]:
237+
target_arch = msvc_target_arch_dir(target)
238+
host_arch_dir = msvc_host_arch_dir()
239+
candidates: list[Path] = []
240+
241+
tools_dir = env_get(env, "VCToolsInstallDir")
242+
if tools_dir:
243+
candidates.append(
244+
Path(tools_dir) / "bin" / host_arch_dir / target_arch / "link.exe"
245+
)
246+
247+
roots = [
248+
env_get(env, "VCINSTALLDIR"),
249+
(
250+
str(Path(env_get(env, "VSINSTALLDIR")) / "VC")
251+
if env_get(env, "VSINSTALLDIR")
252+
else ""
253+
),
254+
]
255+
for root in roots:
256+
if not root:
257+
continue
258+
tools_root = Path(root) / "Tools" / "MSVC"
259+
if not tools_root.is_dir():
260+
continue
261+
for version in sorted(tools_root.iterdir(), reverse=True):
262+
candidates.append(
263+
version / "bin" / host_arch_dir / target_arch / "link.exe"
264+
)
265+
266+
return candidates
267+
268+
269+
def git_for_windows_linker(path: str | None) -> bool:
270+
if not path:
271+
return False
272+
normalized = str(path).replace("\\", "/").lower()
273+
return "/git/usr/bin/link.exe" in normalized
274+
275+
276+
def configure_windows_msvc_linker_env(env: dict[str, str]) -> None:
277+
if not is_windows():
278+
return
279+
target = windows_target_triple()
280+
if not target.endswith("-pc-windows-msvc"):
281+
return
282+
linker_var = cargo_target_linker_env_var(target)
283+
if env_get(env, linker_var):
284+
return
285+
for candidate in msvc_linker_candidates(env, target):
286+
if candidate.is_file():
287+
env[linker_var] = str(candidate)
288+
return
289+
env_path = env_get(env, "PATH")
290+
if git_for_windows_linker(shutil.which("link", path=env_path or None)):
291+
raise SystemExit(
292+
"MSVC link.exe was not found, and Git for Windows link.exe is first "
293+
"in PATH. Run from a Visual Studio developer shell or configure "
294+
"ilammy/msvc-dev-cmd before Windows MSVC cargo recipes."
295+
)
296+
297+
195298
def ensure_windows_target_installed(command_prefix: Sequence[str] = ()) -> None:
196299
if not is_windows():
197300
return
@@ -327,10 +430,10 @@ def generate_windows_import_library(dll: Path, implib: Path) -> None:
327430
def ensure_windows_runtime_import_library(runtime: Path | None = None) -> Path:
328431
library = runtime or runtime_library_path()
329432
implib = runtime_import_library_path(library)
330-
if implib.is_file():
331-
return implib
332433
if not library.is_file():
333434
raise SystemExit(f"Windows runtime library not found at {library}")
435+
if implib.is_file() and implib.stat().st_mtime_ns >= library.stat().st_mtime_ns:
436+
return implib
334437
generate_windows_import_library(library, implib)
335438
return implib
336439

@@ -576,6 +679,8 @@ def command_compile_c_readme_examples(_: argparse.Namespace) -> None:
576679

577680
def command_build_cpp(args: argparse.Namespace) -> None:
578681
mode = build_mode(args.mode)
682+
env = os.environ.copy()
683+
configure_windows_temp_env(env)
579684
lib = runtime_library_path(mode)
580685
cmake_args = [
581686
f"-DOPENPIT_RUNTIME_LIBRARY={lib}",
@@ -589,8 +694,8 @@ def command_build_cpp(args: argparse.Namespace) -> None:
589694
build = cpp_build_dir(args.kind, mode)
590695
platform_args = cmake_platform_args()
591696
prepare_cmake_build_dir(build, platform_args)
592-
run(["cmake", "-S", source, "-B", build, *platform_args, *cmake_args])
593-
run(["cmake", "--build", build, "--config", cmake_build_type(mode)])
697+
run(["cmake", "-S", source, "-B", build, *platform_args, *cmake_args], env=env)
698+
run(["cmake", "--build", build, "--config", cmake_build_type(mode)], env=env)
594699

595700

596701
def command_ctest(args: argparse.Namespace) -> None:
@@ -646,6 +751,8 @@ def command_cpp_format(args: argparse.Namespace) -> None:
646751

647752

648753
def command_lint_cpp(_: argparse.Namespace) -> None:
754+
env = os.environ.copy()
755+
configure_windows_temp_env(env)
649756
repo = str(ROOT).replace("\\", "/")
650757
binding_build = cpp_build_dir("binding", "debug")
651758
examples_build = cpp_build_dir("examples", "debug")
@@ -666,7 +773,8 @@ def command_lint_cpp(_: argparse.Namespace) -> None:
666773
*platform_args,
667774
"-DCMAKE_BUILD_TYPE=Debug",
668775
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
669-
]
776+
],
777+
env=env,
670778
)
671779
if is_windows() and not (ROOT / binding_build / "compile_commands.json").is_file():
672780
print(
@@ -687,7 +795,7 @@ def command_lint_cpp(_: argparse.Namespace) -> None:
687795
]
688796
command.extend(["-p", binding_build])
689797
command.extend(str(path) for path in group)
690-
run(command)
798+
run(command, env=env)
691799

692800
prepare_cmake_build_dir(examples_build, platform_args)
693801
run(
@@ -700,7 +808,8 @@ def command_lint_cpp(_: argparse.Namespace) -> None:
700808
*platform_args,
701809
"-DCMAKE_BUILD_TYPE=Debug",
702810
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON",
703-
]
811+
],
812+
env=env,
704813
)
705814
examples = [
706815
path
@@ -717,7 +826,7 @@ def command_lint_cpp(_: argparse.Namespace) -> None:
717826
]
718827
command.extend(["-p", examples_build])
719828
command.extend(str(path) for path in group)
720-
run(command)
829+
run(command, env=env)
721830

722831

723832
def command_gen_docs_cpp(_: argparse.Namespace) -> None:
@@ -735,9 +844,25 @@ def command_gen_docs_cpp(_: argparse.Namespace) -> None:
735844
return
736845
shutil.rmtree(ROOT / "docs" / "cpp-api", ignore_errors=True)
737846
run(["doxygen", "bindings/cpp/Doxyfile"])
847+
normalize_doxygen_mainpage_anchor()
738848
run([sys.executable, "scripts/_generate_api_c_sitemap.py"])
739849

740850

851+
def normalize_doxygen_mainpage_anchor() -> None:
852+
index = ROOT / "docs" / "cpp-api" / "index.html"
853+
if not index.is_file():
854+
return
855+
text = index.read_text(encoding="utf-8")
856+
normalized = re.sub(
857+
r'id="md_[^"]*DoxygenMainPage"',
858+
'id="openpit-cpp-sdk-mainpage"',
859+
text,
860+
count=1,
861+
)
862+
if normalized != text:
863+
index.write_text(normalized, encoding="utf-8", newline="\n")
864+
865+
741866
def command_build_ffi(args: argparse.Namespace) -> None:
742867
mode = build_mode(args.mode)
743868
env = os.environ.copy()
@@ -755,6 +880,7 @@ def command_build_ffi(args: argparse.Namespace) -> None:
755880
if is_windows():
756881
command_prefix = cargo[:-1] if cargo[-1] == "cargo" else []
757882
ensure_windows_target_installed(command_prefix)
883+
configure_windows_msvc_linker_env(env)
758884
rustflags = env.get("RUSTFLAGS", "")
759885
env["RUSTFLAGS"] = f"{rustflags} -C target-feature=+crt-static".strip()
760886
command.extend(["--target", windows_target_triple()])
@@ -785,6 +911,7 @@ def command_python_develop(args: argparse.Namespace) -> None:
785911
configure_windows_rust_env(env, cargo)
786912
command_prefix = cargo[:-1] if cargo[-1] == "cargo" else []
787913
ensure_windows_target_installed(command_prefix)
914+
configure_windows_msvc_linker_env(env)
788915
command.extend(["--target", windows_target_triple()])
789916
run(command, env=env)
790917

0 commit comments

Comments
 (0)