Skip to content

Commit

Permalink
Merge pull request #2 from NordSecurity/NUD-15_win_arm
Browse files Browse the repository at this point in the history
Support for -msvc targets
  • Loading branch information
nymoral authored Mar 5, 2024
2 parents 33805ef + 522e3f6 commit 99605ed
Show file tree
Hide file tree
Showing 6 changed files with 232 additions and 13 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
- arch: x86_64
target_os: windows
runner: windows-2022
- arch: aarch64
target_os: windows
runner: windows-2022
uses: ./.github/workflows/test_build_native.yml
with:
arch: ${{ matrix.arch }}
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Release notes

## 1.0.0
- Add support for Windows aarch64 architecture
- **Breaking** Windows cross compilation no longer supported. Requires MSVC toolchain to be installed. Please use with dedicated Windows runners

3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
## Overview
This is Nord Security's internal project. It contains utilities used for building libraries developed in Rust.

## Changelist
See CHANGELOG.md for changes

## Building docker images
It is recommended to use GitHub UI to build release versions of docker images. This way there is
a guarantee that the image is built from the versioned version of the Dockerfile and does not contain
Expand Down
191 changes: 191 additions & 0 deletions rust_build_utils/msvc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import os
import re
import subprocess
from pathlib import Path
from typing import Optional

EDITION_PREFERENCES = [
"BuildTools",
"Enterprise",
"Professional",
"Community",
"Preview",
]
MSV_PATHS = [
Path(r"C:\Program Files (x86)\Microsoft Visual Studio"),
Path(r"C:\Program Files\Microsoft Visual Studio"),
]


def is_msvc_active() -> bool:
return "VisualStudioVersion" in os.environ


def is_msv_version(path: Path):
"""Check if a given path is a MVS version installation.
An installation should contain subdirectories of MSV editions
"""
if not path.is_dir():
return False
for subdir in path.iterdir():
if subdir.name in EDITION_PREFERENCES:
return True
return False


def msv_versions():
for installation_path in MSV_PATHS:
for version in installation_path.iterdir():
if is_msv_version(version):
yield version


def activate_msvc(
arch: str,
version_preference: Optional[str] = None,
edition_preference: Optional[str] = None,
direct_pass_arch: bool = False,
) -> dict[str, Optional[str]]:
"""Activate MSVC tools for building a specific arch
Arguments:
arch: build output (target) architecture. The arch will be appended to 'amd64_'
(because our hosts are x64; if x64 or amd64 is given it will be passed directly) and passed to vcvarsall.bat.
aarch64 will be converted to arm64
For example:
amd64 -> vcvarsall.bat amd64
x64 -> vcvarsall.bat x64
x86 -> vcvarsall.bat amd64_x86
arm -> vcvarsall.bat amd64_arm
arm64 -> vcvarsall.bat amd64_arm64
aarch64 -> vcvarsall.bat amd64_arm64
https://learn.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=msvc-170#vcvarsall-syntax
version_preference (optional): version (year) preference (for example: "2022").
When requested version is not found an exception will be raised. If not given, a highest version will be used.
Folders in 'C:\Program Files (x86)\Microsoft Visual Studio' and 'C:\Program Files\Microsoft Visual Studio' can be used as versions.
version_preference (optional): edition preference. There can be multiple editions of VS installed at the time.
If requested edition is not found in automatically (or explicitly) chosen version,
an exception will be raised. Example values are Community, Professional, Enterprise, BuildTools, Preview.
If not given, a preference is given (in same order): BuildTools, Enterprise, Professional, Community, Preview.
If none of these is found, edition will be picked at random.
The editions are folders in 'C:\Program Files\Microsoft Visual Studio\<version>'.
direct_pass_arch (optional): if True, the `arch` value will be passed to vcvarsall.bat
without appending the 'amd64_' prefix.
Returns:
envrinmental variables and their original values that were modified by vcvarsall.bat script
Example usage:
orig_env = activate_msvc('arm64')
print(subprocess.run("link"))
print(subprocess.run("cl"))
deactivate_msvc(orig_env)
"""

# Sample location of vcvarsall script:
# "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat"
# We begin by finding microsoft visual studio installation.
if not any(p.is_dir() for p in MSV_PATHS):
raise Exception(
"Microsoft Visual Studio might not be installed. Was looking in '{}'".format(
MSV_PATHS
)
)

# Multiple versions and multiple editions might be installed, so we iterate over versions installed.
# If version preference is given, we filter only matching versions, othervise the highest version is picked.
sorted_versions = sorted(
[
v
for v in msv_versions()
# version should match the preference if given
if (version_preference is None or v.name == version_preference)
],
key=lambda p: p.name,
reverse=True,
)
if len(sorted_versions) == 0:
raise Exception(
"Microsoft Visual Studio version not found. Was looking in '{}'".format(
MSV_PATHS
)
)
msv_version = sorted_versions[0]

# There can be multiple editions of visual studio installed, but we're choosing based on a preference list.
# To pick the edition based on preference list, we create a function that returns an index in the list.
# When used as a sort key, the first element will be the closes to the start of the list
# To support values not in a list, the length of the list is used as fallback,
# putting those values effectively at the end.
def preference_index(e):
if e in EDITION_PREFERENCES:
return EDITION_PREFERENCES.index(e)
else:
return len(EDITION_PREFERENCES)

# if edition_preference is given, the list will only contain that edition.
msv_editions = sorted(
[
e
for e in msv_version.iterdir()
if e.is_dir()
and (edition_preference is None or e.name == edition_preference)
],
key=lambda p: preference_index(p.name),
)
if len(msv_editions) == 0:
raise Exception(
"Microsoft Visual Studio edition not found. Was looking in '{}'".format(
msv_version
)
)
msv = msv_editions[0]
vcvarsall = msv.joinpath(r"VC\Auxiliary\Build\vcvarsall.bat")

# architecture string that will be passed to vcvarsall
if not direct_pass_arch and arch == "aarch64":
arch = "arm64"
arch = (
arch
if direct_pass_arch or arch in ("amd64", "x64")
else "amd64_{}".format(arch)
)

original_env = {}
# Execute vcvarsall in a shell and print the environment after modification.
# Because the change happens in a separate process, after it exits the changes made to the env are lost.
# We collect the process output (modified environment) and set current environment to those values.
# When setting the environment we save the old values so they can be restored after exiting the context.
#
# `chcp 65001` changes output encoding to utf-8.
# https://learn.microsoft.com/en-gb/windows/win32/intl/code-page-identifiers?redirectedfrom=MSDN
p = subprocess.run(
["chcp", "65001", "&", str(vcvarsall), arch, "&", "set"],
shell=True,
check=True,
capture_output=True,
)
# Find ARG=VALUE pairs and capture them. Because the value might contain '=',
# we match until the first '=' character.
for m in re.finditer(r"^([^=]*)=(.*)$", p.stdout.decode("utf-8"), flags=re.M):
env_var = m.group(1)
env_new_val = m.group(2).strip()
env_old_val = os.environ.get(env_var, None)
if env_old_val != env_new_val:
original_env[env_var] = env_old_val
os.environ[env_var] = env_new_val
return original_env


def deactivate_msvc(env: dict[str, Optional[str]]):
"""Deactivate MSVC tools that were activated with `activate_msvc()`.
Restores the environmental variables set by vcvarsall.bat script
Arguments:
env: original system environment, returned from `activate_msvc()` call
"""
for k, v in env.items():
if v is None:
del os.environ[k]
else:
os.environ[k] = v
32 changes: 24 additions & 8 deletions rust_build_utils/rust_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from dataclasses import dataclass
from rust_build_utils.rust_utils_config import GLOBAL_CONFIG
from pathlib import Path
from rust_build_utils.msvc import activate_msvc, deactivate_msvc, is_msvc_active


PackageList = Dict[str, Dict[str, str]]
Expand All @@ -29,9 +30,13 @@ class CargoConfig:
def __post_init__(self):
if self.arch == "arm64":
self.arch = "aarch64"
self.rust_target = GLOBAL_CONFIG[self.target_os]["archs"][self.arch][
"rust_target"
]
if not self.rust_target:
self.rust_target = GLOBAL_CONFIG[self.target_os]["archs"][self.arch][
"rust_target"
]

def is_msvc(self):
return self.rust_target.endswith("-msvc")


@dataclass
Expand Down Expand Up @@ -102,7 +107,7 @@ def clear_env_variables(config):
for key, value in GLOBAL_CONFIG[config.target_os]["env"].items():
if value[1] == "set":
os.environ[key] = ""
if "env" in GLOBAL_CONFIG[config.target_os]["archs"][config.arch]:
if "env" in GLOBAL_CONFIG[config.target_os]["archs"].get(config.arch, {}):
for key, value in GLOBAL_CONFIG[config.target_os]["archs"][config.arch][
"env"
].items():
Expand All @@ -115,7 +120,7 @@ def set_env_var(config):
if "env" in GLOBAL_CONFIG[config.target_os]:
for key, value in GLOBAL_CONFIG[config.target_os]["env"].items():
concatenate_env_variable(key, value[0])
if "env" in GLOBAL_CONFIG[config.target_os]["archs"][config.arch]:
if "env" in GLOBAL_CONFIG[config.target_os]["archs"].get(config.arch, {}):
for key, value in GLOBAL_CONFIG[config.target_os]["archs"][config.arch][
"env"
].items():
Expand All @@ -128,8 +133,7 @@ def config_local_env_vars(config, local_config):
for env, tuple in local_config[config.target_os]["env"].items():
if not "env" in GLOBAL_CONFIG[config.target_os]:
GLOBAL_CONFIG[config.target_os]["env"] = {env: tuple}
return
if env in GLOBAL_CONFIG[config.target_os]["env"]:
if env in GLOBAL_CONFIG[config.target_os]["env"] and tuple[1] == "append":
if tuple[0] not in GLOBAL_CONFIG[config.target_os]["env"][env][0]:
GLOBAL_CONFIG[config.target_os]["env"][env][0].append(tuple[0])
else:
Expand All @@ -148,7 +152,10 @@ def config_local_env_vars(config, local_config):
env: tuple
}
return
if env in GLOBAL_CONFIG[config.target_os]["archs"][config.arch]["env"]:
if (
env in GLOBAL_CONFIG[config.target_os]["archs"][config.arch]["env"]
and tuple[1] == "append"
):
if (
tuple[0]
not in GLOBAL_CONFIG[config.target_os]["archs"][config.arch][
Expand Down Expand Up @@ -179,6 +186,7 @@ def create_cli_parser() -> Any:
build_parser = subparsers.add_parser("build", help="build a specific os/arch pair")
build_parser.add_argument("os", type=str, choices=list(GLOBAL_CONFIG.keys()))
build_parser.add_argument("arch", type=str)
build_parser.add_argument("--target", type=str)
build_parser.add_argument("--debug", action="store_true", help="Create debug build")

lipo_parser = subparsers.add_parser(
Expand Down Expand Up @@ -390,6 +398,11 @@ def _cargo(
run_command(["rustup", "default", project.rust_version])
run_command(["rustup", "target", "add", config.rust_target])

msvc_context = None
if config.rust_target.endswith("-msvc") and not is_msvc_active():
# For msvc based toolchains msvc development environment needs activation
msvc_context = activate_msvc(config.arch)

_build_packages(config, list(packages.keys()), extra_args, subcommand)

for _, bins in packages.items():
Expand All @@ -404,6 +417,9 @@ def _cargo(

post_build(project, config, packages)

if msvc_context is not None:
deactivate_msvc(msvc_context)


def str_to_func_call(func_string):
func_array = func_string.split(".")
Expand Down
10 changes: 5 additions & 5 deletions rust_build_utils/rust_utils_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# "env" : [Optional, Dictionary], a dict of arch specific environment variables
# {
# "{env_var}" : [Tuple(List<String>, String)], values are tuples that contain a list of strings to set the variable to and another String which tells
# whether the environment variable should be set to blank first("set") or kept as is and concatenated on top ("append")
# whether the environment variable should be set to blank first("set") or kept as is and concatenated on top ("append").
# }
# }
# }
Expand Down Expand Up @@ -66,11 +66,11 @@
"windows": {
"archs": {
"x86_64": {
"rust_target": "x86_64-pc-windows-gnu",
"rust_target": "x86_64-pc-windows-msvc",
},
"aarch64": {
"rust_target": "aarch64-pc-windows-msvc",
},
},
"env": {
"RUSTFLAGS": ([" -C link-arg=-s "], "set"),
},
},
"macos": {
Expand Down

0 comments on commit 99605ed

Please sign in to comment.