vest-build-dotnet 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.3
2
+ Name: vest-build-dotnet
3
+ Version: 0.0.1
4
+ Summary: Vest build support for .NET projects
5
+ Keywords: vest,build,build-system,dotnet,.net,csharp,native-aot,ilc,msbuild,compilation,packaging
6
+ Author: ascpixi
7
+ Author-email: ascpixi <ascpixi@proton.me>
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: C#
17
+ Classifier: Topic :: Software Development :: Build Tools
18
+ Classifier: Topic :: Software Development :: Compilers
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: packaging>=26.0
21
+ Requires-Dist: vest-build>=0.0.2
22
+ Requires-Python: >=3.11
23
+ Project-URL: Repository, https://github.com/ascpixi/vest-dotnet
24
+ Description-Content-Type: text/markdown
25
+
26
+ # Vest support for .NET
27
+ [![image](https://img.shields.io/pypi/v/vest-build-dotnet.svg)](https://pypi.python.org/pypi/vest-build-dotnet)
28
+ [![image](https://img.shields.io/pypi/l/vest-build-dotnet.svg)](https://github.com/ascpixi/vest-dotnet/blob/master/LICENSE)
29
+ [![image](https://img.shields.io/pypi/pyversions/vest-build-dotnet.svg)](https://pypi.python.org/pypi/vest-build-dotnet)
30
+ [![Actions status](https://github.com/ascpixi/vest-dotnet/workflows/CI/badge.svg)](https://github.com/ascpixi/vest-dotnet/actions)
31
+
32
+ .NET build support for [Vest](https://pypi.org/project/vest-build/), a Python-based build
33
+ system for bespoke use.
34
+
35
+ ## Requirements
36
+ The project assumes that the following dependencies are installed:
37
+
38
+ - The [.NET SDK](https://dotnet.microsoft.com/download), with `dotnet` on `PATH`.
39
+ - `csc`. You can install this via `dotnet tool install csc`.
40
+ - `ilc` for Native AOT compilation. You can install this via `dotnet tool install ilc`.
41
+
42
+ ## Examples
43
+
44
+ ### Building or publishing an existing project
45
+
46
+ ```python
47
+ from vest import *
48
+ from vest.dotnet import dotnet_publish
49
+
50
+ component(name = "my-service")
51
+
52
+ @task
53
+ def publish():
54
+ artifacts = dotnet_publish(
55
+ configuration = "Release",
56
+ rid = "linux-x64"
57
+ )
58
+
59
+ return artifacts.publish
60
+ ```
61
+
62
+ ### Native AOT compilation
63
+
64
+ `ilc_compile` turns an IL assembly into a native object file via `dotnet ilc`, which can then
65
+ be fed into your own linker invocation:
66
+
67
+ ```python
68
+ from vest.dotnet import ilc_compile
69
+
70
+ @task
71
+ def aot():
72
+ asm = dotnet_build(
73
+ configuration = dotnet_configuration(),
74
+ rid = rid(),
75
+ properties = {
76
+ "Arch": "x64"
77
+ }
78
+ )
79
+
80
+ ilc_compile(
81
+ input = asm.build,
82
+ output = f"{artifact_dir()}/obj/my-object.o",
83
+ target_arch = "x64",
84
+ stdlib_assembly = "Azerou.CoreLib",
85
+ nativelib = True
86
+ )
87
+ ```
@@ -0,0 +1,62 @@
1
+ # Vest support for .NET
2
+ [![image](https://img.shields.io/pypi/v/vest-build-dotnet.svg)](https://pypi.python.org/pypi/vest-build-dotnet)
3
+ [![image](https://img.shields.io/pypi/l/vest-build-dotnet.svg)](https://github.com/ascpixi/vest-dotnet/blob/master/LICENSE)
4
+ [![image](https://img.shields.io/pypi/pyversions/vest-build-dotnet.svg)](https://pypi.python.org/pypi/vest-build-dotnet)
5
+ [![Actions status](https://github.com/ascpixi/vest-dotnet/workflows/CI/badge.svg)](https://github.com/ascpixi/vest-dotnet/actions)
6
+
7
+ .NET build support for [Vest](https://pypi.org/project/vest-build/), a Python-based build
8
+ system for bespoke use.
9
+
10
+ ## Requirements
11
+ The project assumes that the following dependencies are installed:
12
+
13
+ - The [.NET SDK](https://dotnet.microsoft.com/download), with `dotnet` on `PATH`.
14
+ - `csc`. You can install this via `dotnet tool install csc`.
15
+ - `ilc` for Native AOT compilation. You can install this via `dotnet tool install ilc`.
16
+
17
+ ## Examples
18
+
19
+ ### Building or publishing an existing project
20
+
21
+ ```python
22
+ from vest import *
23
+ from vest.dotnet import dotnet_publish
24
+
25
+ component(name = "my-service")
26
+
27
+ @task
28
+ def publish():
29
+ artifacts = dotnet_publish(
30
+ configuration = "Release",
31
+ rid = "linux-x64"
32
+ )
33
+
34
+ return artifacts.publish
35
+ ```
36
+
37
+ ### Native AOT compilation
38
+
39
+ `ilc_compile` turns an IL assembly into a native object file via `dotnet ilc`, which can then
40
+ be fed into your own linker invocation:
41
+
42
+ ```python
43
+ from vest.dotnet import ilc_compile
44
+
45
+ @task
46
+ def aot():
47
+ asm = dotnet_build(
48
+ configuration = dotnet_configuration(),
49
+ rid = rid(),
50
+ properties = {
51
+ "Arch": "x64"
52
+ }
53
+ )
54
+
55
+ ilc_compile(
56
+ input = asm.build,
57
+ output = f"{artifact_dir()}/obj/my-object.o",
58
+ target_arch = "x64",
59
+ stdlib_assembly = "Azerou.CoreLib",
60
+ nativelib = True
61
+ )
62
+ ```
@@ -0,0 +1,39 @@
1
+ [project]
2
+ name = "vest-build-dotnet"
3
+ version = "0.0.1"
4
+ description = "Vest build support for .NET projects"
5
+ authors = [ { name = "ascpixi", email = "ascpixi@proton.me" } ]
6
+ readme = "README.md"
7
+ requires-python = ">=3.11"
8
+
9
+ keywords = ["vest", "build", "build-system", "dotnet", ".net", "csharp", "native-aot", "ilc", "msbuild", "compilation", "packaging"]
10
+ classifiers = [
11
+ "Development Status :: 2 - Pre-Alpha",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Operating System :: OS Independent",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: C#",
20
+ "Topic :: Software Development :: Build Tools",
21
+ "Topic :: Software Development :: Compilers",
22
+ "Typing :: Typed",
23
+ ]
24
+
25
+ dependencies = [
26
+ "packaging>=26.0",
27
+ "vest-build>=0.0.2",
28
+ ]
29
+
30
+ [project.urls]
31
+ Repository = "https://github.com/ascpixi/vest-dotnet"
32
+
33
+ [build-system]
34
+ requires = ["uv_build>=0.9.18,<0.10.0"]
35
+ build-backend = "uv_build"
36
+
37
+ [tool.uv.build-backend]
38
+ module-name = "vest.dotnet"
39
+ namespace = true
@@ -0,0 +1,10 @@
1
+ from .sdk import (
2
+ dotnet_version,
3
+ dotnet_sdk_path,
4
+ dotnet_publish,
5
+ dotnet_build
6
+ )
7
+
8
+ from .aot import (
9
+ ilc_compile
10
+ )
@@ -0,0 +1,74 @@
1
+ from typing import Literal
2
+
3
+ from vest.common.collections import maybe
4
+ from vest.spec.core import run
5
+
6
+ def ilc_compile(
7
+ *,
8
+ input: str,
9
+ output: str,
10
+ references: list[str] = [],
11
+ target_arch: str,
12
+ target_os: str = "linux",
13
+ stdlib_assembly: str | None = None,
14
+ method_body_fold = False,
15
+ optimization: Literal["disable", "enable", "favor_size", "favor_time"] = "disable",
16
+ emit_debug_info = True,
17
+ codegen_options: list[str] = [],
18
+ pre_init_statics = True,
19
+ generate_reflection_data = False,
20
+ direct_pinvoke: list[str] = [],
21
+ nativelib = False,
22
+ disable_il_scanner = False
23
+ ):
24
+ """
25
+ Compiles an IL assembly into a native object file via `dotnet ilc`.
26
+
27
+ @input: The IL assembly to compile.
28
+ @output: The path to write the compiled object file to.
29
+ @references: The assemblies that `@input` references.
30
+ @target_arch: Specifies the architecture to compile for, e.g. `x64` or `arm64`.
31
+ @target_os: Specifies the ABI and binary format to compile for, e.g. `linux` or `windows`.
32
+ @stdlib_assembly: The name of the assembly to consider the standard library.
33
+ @method_body_fold: If `"true`", identical method bodies will be merged (folded).
34
+ @optimization: Controls program optimization.
35
+ @emit_debug_info: If `"true"`, debug information will be included in the object file.
36
+ @codegen_options: Code generation options in the form of `<key>=<value>`. See https://github.com/dotnet/runtime/blob/main/src/coreclr/jit/jitconfigvalues.h#L32 for details.
37
+ @pre_init_statics: If `"true"`, static fields will be evaluated at compile-time (if possible).
38
+ @generate_reflection_data: If `"true"`, reflection data will be included with the object file.
39
+ @direct_pinvoke: A list of libraries to call the functions of directly, without a P/Invoke stub/wrapper.
40
+ @nativelib: If `"true"`, the IL should be compiled as a static or shared library.
41
+ """
42
+
43
+ args = [
44
+ "ilc",
45
+ input,
46
+ f"-o:{output}",
47
+ *[f"-r:{x}" for x in references],
48
+ f"--targetos:{target_os}", # This option only chooses the ABI and binary format - for 'linux', the pair is System V and ELF.
49
+ f"--targetarch:{target_arch}",
50
+ *maybe("--debug", emit_debug_info == "true"),
51
+ *maybe(f"--systemmodule:{stdlib_assembly}", stdlib_assembly is not None),
52
+ *maybe("--preinitstatics", pre_init_statics == "true"),
53
+ *maybe("--methodbodyfolding", method_body_fold == "true"),
54
+ *maybe("--nativelib", nativelib == "true"),
55
+ *maybe("--noscan", disable_il_scanner == "true"),
56
+ *[f"--directpinvoke:{x}" for x in direct_pinvoke],
57
+ f"--reflectiondata:{'all' if generate_reflection_data == 'true' else 'none'}",
58
+ "--verbose",
59
+ "--noscan"
60
+ ]
61
+
62
+ if optimization != "disable":
63
+ match optimization:
64
+ case "enable":
65
+ args.append("--optimize")
66
+ case "favor_size":
67
+ args.append("--optimize-space")
68
+ case "favor_time":
69
+ args.append("--optimize-time")
70
+
71
+ for opt in codegen_options:
72
+ args.extend(["--codegenopt", opt])
73
+
74
+ run("dotnet", args)
@@ -0,0 +1,236 @@
1
+ # This file defines .NET-related functions used within the component-based build system.
2
+
3
+ from dataclasses import dataclass
4
+ import json
5
+ import os
6
+ import re
7
+ import subprocess
8
+ from typing import Literal, Optional
9
+ from packaging import version
10
+
11
+ from vest.common.collections import maybe, single
12
+ from vest.spec.types import Component
13
+ from vest.spec.core import host, run, artifact_dir
14
+
15
+ DotnetVerbosity = Literal["quiet", "minimal", "normal", "detailed", "diagnostic"]
16
+
17
+ cached_version: Optional[str] = None
18
+ cached_sdk_path: Optional[str] = None
19
+
20
+ def dotnet_version() -> str:
21
+ """Gets the short version string of the current .NET SDK (e.g. 8.0)."""
22
+
23
+ global cached_version
24
+ if cached_version is not None:
25
+ return cached_version
26
+
27
+ full = subprocess.check_output("dotnet --version".split()).decode()
28
+ ver = full[:full.rfind(".")]
29
+
30
+ cached_version = ver
31
+ return ver
32
+
33
+ def dotnet_sdk_path() -> str:
34
+ "Gets the full path to the latest .NET SDK installed on the host system."
35
+
36
+ global cached_sdk_path
37
+ if cached_sdk_path is not None:
38
+ return cached_sdk_path
39
+
40
+ sdks_raw = subprocess.check_output(["dotnet", "--list-sdks"]).decode()
41
+
42
+ # sdks[i][0]: version, sdks[i][1]: root path (pointing where the version dir is located, not to the dir itself)
43
+ sdks = [(x.group(1), x.group(2)) for x in re.finditer(r"(.+) \[(.+)\]", sdks_raw)]
44
+ latest_sdk = max(sdks, key = lambda x: version.parse(x[0]))
45
+
46
+ path = os.path.join(latest_sdk[1], latest_sdk[0])
47
+
48
+ cached_sdk_path = path
49
+ return path
50
+
51
+ def _dotnet(
52
+ action: Literal["build", "publish"],
53
+ rid: str | None,
54
+ configuration: str,
55
+ *,
56
+ verbosity: DotnetVerbosity = "quiet",
57
+ properties: dict[str, str] = {}
58
+ ):
59
+ final_props: list[str] = [
60
+ "-p:VestBuild=true",
61
+ f"-p:Repo={host().repo_dir}",
62
+ f"--artifacts-path", artifact_dir()
63
+ ]
64
+
65
+ for (k, v) in host().parameters:
66
+ if not k.startswith("--"):
67
+ continue # avoid exposing short-form parameters
68
+
69
+ # We need to convert the key into PascalCase, as all MSBuild properties are, by convention, pascal cased.
70
+ k = re.sub(r"([a-z])([A-Z])", r"\1 \2", k) # camelCase
71
+ k = re.sub(r"[-_]+", " ", k) # kebab-case and snake_case
72
+ k = "".join(word.capitalize() for word in k.split())
73
+
74
+ final_props.append(f"-p:{k}={v}")
75
+
76
+ for (k, v) in properties.items():
77
+ final_props.append(f"-p:{k}={v}")
78
+
79
+ run("dotnet", [
80
+ action,
81
+ "-c", configuration,
82
+ *(["-r", rid] if rid else []),
83
+ "-v", verbosity,
84
+ "-tl:off",
85
+ "--nologo",
86
+ *final_props
87
+ ])
88
+
89
+ process = subprocess.run(
90
+ [
91
+ "dotnet",
92
+ "build",
93
+ "-c", configuration,
94
+ *(["-r", rid] if rid else []),
95
+ *final_props,
96
+ "--getTargetResult:" + ",".join([
97
+ "BuiltProjectOutputGroup", # we always want `build` artifacts
98
+ "ResolveAssemblyReferences",
99
+ *maybe("PublishItemsOutputGroup", action == "publish") # `publish` artifacts only available in `publish` builds
100
+ ])
101
+ ],
102
+ capture_output = True,
103
+ text = True,
104
+ check = True
105
+ )
106
+
107
+ raw = json.loads(process.stdout)
108
+ assert type(raw) is dict, f".NET's --getTargetResult output was not a dictionary ({raw})"
109
+ return raw
110
+
111
+ def _find_key_artifact(artifacts: dict, group_name: str):
112
+ items = artifacts["TargetResults"][group_name]["Items"]
113
+ assert type(items) is list, f"TargetResults.{group_name}.Items was not an array; it's a {type(items)} instead ({items})"
114
+
115
+ for item in items:
116
+ if "IsKeyOutput" in item and item["IsKeyOutput"]:
117
+ p = item["FullPath"]
118
+ assert type(p) is str
119
+ return p
120
+
121
+ # If there's nothing marked as IsKeyOutput, just use the first one.
122
+ p = items[0]["FullPath"]
123
+ assert type(p) is str
124
+ return p
125
+
126
+ def _extract_all_artifacts(artifacts: dict):
127
+ results = artifacts["TargetResults"]
128
+ assert type(results) is dict, f"TargetResults was not a dict; it's a {type(results)} instead ({results})"
129
+
130
+ paths: list[str] = []
131
+
132
+ for value in results.values():
133
+ assert type(value) is dict, f"An element of TargetResults was not a dict; it's a {type(value)} instead ({value})"
134
+
135
+ items = value["Items"]
136
+ assert type(items) is list, f"TargetResults[?].Items was not a list; it's a {type(items)} instead ({items})"
137
+
138
+ for item in items:
139
+ p = item["FullPath"]
140
+ assert type(p) is str
141
+ paths.append(p)
142
+
143
+ return paths
144
+
145
+ def _extract_references(artifacts: dict) -> list[str]:
146
+ items = artifacts["TargetResults"]["ResolveAssemblyReferences"]["Items"]
147
+ assert type(items) is list, f"TargetResults.ResolveAssemblyReferences.Items was not an array; it's a {type(items)} instead ({items})"
148
+ return [x["FullPath"] for x in items]
149
+
150
+ @dataclass
151
+ class DotnetPublishArtifacts:
152
+ publish: str
153
+ "The path to the main, key publish artifact. For example, for AOT compilation, this would be the native executable."
154
+
155
+ build: str
156
+ "The path to the main, key build artifact. This is usually a binary that hasn't been processed by the publish target."
157
+
158
+ all: list[str]
159
+ "Full paths to all other artifacts. This also includes non-key artifacts, like debug information."
160
+
161
+ references: list[str]
162
+ "A list of full paths to assemblies that have been referenced by the published project."
163
+
164
+ def dotnet_publish(
165
+ configuration: str,
166
+ rid: str | None = None,
167
+ *,
168
+ verbosity: DotnetVerbosity = "quiet",
169
+ properties: dict[str, str] = {}
170
+ ) -> DotnetPublishArtifacts:
171
+ """
172
+ Publishes a .NET project. All resulting files will be placed in the root of the
173
+ calling component's artifact directory - that is, `/artifacts/<component name>/...`
174
+ if `artifacts_path` is set to `None`.
175
+
176
+ @artifacts_path: The directory to place all artifacts (`bin` and `obj`) in.
177
+ @verbosity: The verbosity level.
178
+ @configuration: The configuration to publish for. By default, `host().config` is used.
179
+ @rid: The runtime identifier, e.g. `linux-x64`.
180
+ @properties: The properties to forward to the build system. Aggregates into `-p:(key)=(value)`.
181
+ """
182
+ artifacts = _dotnet("publish",
183
+ verbosity = verbosity,
184
+ configuration = configuration,
185
+ rid = rid,
186
+ properties = properties
187
+ )
188
+
189
+ return DotnetPublishArtifacts(
190
+ publish = _find_key_artifact(artifacts, "PublishItemsOutputGroup"),
191
+ build = _find_key_artifact(artifacts, "BuiltProjectOutputGroup"),
192
+ all = _extract_all_artifacts(artifacts),
193
+ references = _extract_references(artifacts)
194
+ )
195
+
196
+ @dataclass
197
+ class DotnetBuildArtifacts:
198
+ build: str
199
+ "The path to the main, key build artifact."
200
+
201
+ all: list[str]
202
+ "Full paths to all other artifacts. This also includes non-key artifacts, like debug information."
203
+
204
+ references: list[str]
205
+ "A list of full paths to assemblies that have been referenced by the built project."
206
+
207
+ def dotnet_build(
208
+ configuration: str,
209
+ rid: str | None = None,
210
+ *,
211
+ verbosity: DotnetVerbosity = "quiet",
212
+ properties: dict[str, str] = {}
213
+ ) -> DotnetBuildArtifacts:
214
+ """
215
+ Builds a .NET project. All resulting files will be placed in the root of the
216
+ calling component's artifact directory - that is, `/artifacts/<component name>/...`
217
+ if `artifacts_path` is set to `None`.
218
+
219
+ @artifacts_path: The directory to place all artifacts (`bin` and `obj`) in.
220
+ @verbosity: The verbosity level.
221
+ @configuration: The configuration to publish for. By default, `host().config` is used.
222
+ @rid: The runtime identifier, e.g. `linux-x64`.
223
+ @properties: The properties to forward to the build system. Aggregates into `-p:(key)=(value)`.
224
+ """
225
+ artifacts = _dotnet("build",
226
+ verbosity = verbosity,
227
+ configuration = configuration,
228
+ rid = rid,
229
+ properties = properties
230
+ )
231
+
232
+ return DotnetBuildArtifacts(
233
+ build = _find_key_artifact(artifacts, "BuiltProjectOutputGroup"),
234
+ all = _extract_all_artifacts(artifacts),
235
+ references = _extract_references(artifacts)
236
+ )