1
1
from __future__ import annotations
2
2
3
3
import functools
4
- import itertools
5
- from typing import Any , Iterable , Literal , TypeVar , get_args , NamedTuple , get_type_hints
4
+ import re
5
+ from typing import Iterable , Literal , Mapping , NamedTuple , TypeVar
6
+
6
7
from shrub .v3 .evg_build_variant import BuildVariant
8
+ from shrub .v3 .evg_command import EvgCommandType , subprocess_exec
7
9
from shrub .v3 .evg_task import EvgTaskRef
8
- from .. etc . utils import Task
9
- from shrub . v3 . evg_command import subprocess_exec , EvgCommandType
10
+
11
+ from .. etc . utils import Task , all_possible
10
12
11
13
T = TypeVar ("T" )
12
14
13
15
_ENV_PARAM_NAME = "MONGOC_EARTHLY_ENV"
14
- "The name of the EVG expansion parameter used to key the Earthly build env"
15
-
16
- EnvKey = Literal ["u22" , "alpine3.18" , "archlinux" ]
17
- "Identifiers for environments. These correspond to special '*-env' targets in the Earthfile."
18
-
19
- _ENV_NAMES : dict [EnvKey , str ] = {
20
- "u22" : "Ubuntu 22.04" ,
21
- "alpine3.18" : "Alpine 3.18" ,
22
- "archlinux" : "Arch Linux" ,
23
- }
24
- "A mapping from environment keys to 'pretty' environment names"
16
+ _CC_PARAM_NAME = "MONGOC_EARTHLY_C_COMPILER"
17
+ "The name of the EVG expansion for the Earthly c_compiler argument"
18
+
19
+
20
+ EnvKey = Literal [
21
+ "u18" ,
22
+ "u20" ,
23
+ "u22" ,
24
+ "alpine3.16" ,
25
+ "alpine3.17" ,
26
+ "alpine3.18" ,
27
+ "alpine3.19" ,
28
+ "archlinux" ,
29
+ ]
30
+ "Identifiers for environments. These correspond to special 'env.*' targets in the Earthfile."
31
+ CompilerName = Literal ["gcc" , "clang" ]
32
+ "The name of the compiler program that is used for the build. Passed via --c_compiler to Earthly."
25
33
26
34
# Other options: SSPI (Windows only), AUTO (not reliably test-able without more environments)
27
35
SASLOption = Literal ["Cyrus" , "off" ]
28
36
"Valid options for the SASL configuration parameter"
29
37
TLSOption = Literal ["LibreSSL" , "OpenSSL" , "off" ]
30
38
"Options for the TLS backend configuration parameter (AKA 'ENABLE_SSL')"
31
- CxxVersion = Literal ["master " , "r3.8 .0" ]
39
+ CxxVersion = Literal ["r3.8.0 " , "r3.9 .0" ]
32
40
"C++ driver refs that are under CI test"
33
41
34
- # A Unicode non-breaking space character
35
- _BULLET = "\N{Bullet} "
42
+ # A separator character, since we cannot use whitespace
43
+ _SEPARATOR = "\N{no-break space} \N{bullet} \N{no-break space} "
44
+
45
+
46
+ def os_split (env : EnvKey ) -> tuple [str , None | str ]:
47
+ """Convert the environment key into a pretty name+version pair"""
48
+ match env :
49
+ # match 'alpine3.18' 'alpine53.123' etc.
50
+ case alp if mat := re .match (r"alpine(\d+\.\d+)" , alp ):
51
+ return ("Alpine" , mat [1 ])
52
+ case "archlinux" :
53
+ return "ArchLinux" , None
54
+ # Match 'u22', 'u20', 'u71' etc.
55
+ case ubu if mat := re .match (r"u(\d\d)" , ubu ):
56
+ return "Ubuntu" , f"{ mat [1 ]} .04"
57
+ case _:
58
+ raise ValueError (
59
+ f"Failed to split OS env key { env = } into a name+version pair (unrecognized)"
60
+ )
61
+
62
+
63
+ class EarthlyVariant (NamedTuple ):
64
+ """
65
+ Define a "variant" that runs under a set of Earthly parameters. These are
66
+ turned into real EVG variants later on. The Earthly arguments are passed via
67
+ expansion parameters.
68
+ """
69
+
70
+ env : EnvKey
71
+ c_compiler : CompilerName
72
+
73
+ @property
74
+ def display_name (self ) -> str :
75
+ """The pretty name for this variant"""
76
+ base : str
77
+ match os_split (self .env ):
78
+ case name , None :
79
+ base = name
80
+ case name , version :
81
+ base = f"{ name } { version } "
82
+ toolchain : str
83
+ match self .c_compiler :
84
+ case "clang" :
85
+ toolchain = "LLVM/Clang"
86
+ case "gcc" :
87
+ toolchain = "GCC"
88
+ return f"{ base } ({ toolchain } )"
89
+
90
+ @property
91
+ def task_selector_tag (self ) -> str :
92
+ """
93
+ The task tag that is used to select the tasks that want to run on this
94
+ variant.
95
+ """
96
+ return f"{ self .env } -{ self .c_compiler } "
97
+
98
+ @property
99
+ def expansions (self ) -> Mapping [str , str ]:
100
+ """
101
+ Expansion values that are defined for the build variant that is generated
102
+ from this object.
103
+ """
104
+ return {
105
+ _CC_PARAM_NAME : self .c_compiler ,
106
+ _ENV_PARAM_NAME : self .env ,
107
+ }
108
+
109
+ def as_evg_variant (self ) -> BuildVariant :
110
+ return BuildVariant (
111
+ name = f"{ self .task_selector_tag } " ,
112
+ tasks = [EvgTaskRef (name = f".{ self .task_selector_tag } " )],
113
+ display_name = self .display_name ,
114
+ expansions = dict (self .expansions ),
115
+ )
36
116
37
117
38
118
class Configuration (NamedTuple ):
@@ -42,63 +122,44 @@ class Configuration(NamedTuple):
42
122
43
123
Adding/removing fields will add/remove dimensions on the task matrix.
44
124
45
- The 'env' parameter is not encoded here, but is managed separately.
46
-
47
- Keep this in sync with the 'PartialConfiguration' class defined below!
125
+ Some Earthly parameters are not encoded here, but are rather part of the variant (EarthlyVariant).
48
126
"""
49
127
50
128
sasl : SASLOption
51
129
tls : TLSOption
52
130
test_mongocxx_ref : CxxVersion
53
131
54
- @classmethod
55
- def all (cls ) -> Iterable [Configuration ]:
56
- """
57
- Generate all configurations for all options of our parameters.
58
- """
59
- # Iter each configuration parameter:
60
- fields : Iterable [tuple [str , type ]] = get_type_hints (Configuration ).items ()
61
- # Generate lists of pairs of parameter names their options:
62
- all_pairs : Iterable [Iterable [tuple [str , str ]]] = (
63
- # Generate a (key, opt) pair for each option in parameter 'key'
64
- [(key , opt ) for opt in get_args (typ )]
65
- # Over each parameter and type thereof:
66
- for key , typ in fields
67
- )
68
- # Now generate the cross product of all alternative for all options:
69
- matrix : Iterable [dict [str , Any ]] = map (dict , itertools .product (* all_pairs ))
70
- for items in matrix :
71
- # Convert each item to a Configuration:
72
- yield Configuration (** items )
73
-
74
132
@property
75
133
def suffix (self ) -> str :
76
- return f"{ _BULLET } " .join (f"{ k } ={ v } " for k , v in self ._asdict ().items ())
134
+ return f"{ _SEPARATOR } " .join (f"{ k } ={ v } " for k , v in self ._asdict ().items ())
77
135
78
136
79
- def task_filter (env : EnvKey , conf : Configuration ) -> bool :
137
+ def task_filter (env : EarthlyVariant , conf : Configuration ) -> bool :
80
138
"""
81
139
Control which tasks are actually defined by matching on the platform and
82
140
configuration values.
83
141
"""
84
142
match env , conf :
85
143
# We only need one task with "sasl=off"
86
- case "u22" , ("off" , "OpenSSL" , "master " ):
144
+ case [ "u22" , "gcc" ], ("off" , "OpenSSL" , "r3.8.0 " ):
87
145
return True
146
+ # The Ubuntu 18.04 GCC has a bug that fails to build the 3.8.0 C++ driver
147
+ case ["u18" , "gcc" ], [_, _, "r3.8.0" ]:
148
+ return False
88
149
# Other sasl=off tasks we'll just ignore:
89
150
case _, ("off" , _tls , _cxx ):
90
151
return False
91
152
# Ubuntu does not ship with a LibreSSL package:
92
- case e , (_sasl , "LibreSSL" , _cxx ) if _ENV_NAMES [ e ] .startswith ("Ubuntu" ):
153
+ case e , (_sasl , "LibreSSL" , _cxx ) if e . display_name .startswith ("Ubuntu" ):
93
154
return False
94
155
# Anything else: Allow it to run:
95
156
case _:
96
157
return True
97
158
98
159
99
- def envs_for (config : Configuration ) -> Iterable [EnvKey ]:
100
- """Get all environment keys that are not excluded for the given configuration"""
101
- all_envs : tuple [ EnvKey , ...] = get_args ( EnvKey )
160
+ def variants_for (config : Configuration ) -> Iterable [EarthlyVariant ]:
161
+ """Get all Earthly variants that are not excluded for the given build configuration"""
162
+ all_envs = all_possible ( EarthlyVariant )
102
163
allow_env_for_config = functools .partial (task_filter , conf = config )
103
164
return filter (allow_env_for_config , all_envs )
104
165
@@ -109,16 +170,26 @@ def earthly_task(
109
170
targets : Iterable [str ],
110
171
config : Configuration ,
111
172
) -> Task | None :
112
- # Attach "earthly-xyz" tags to the task to allow build variants to select
173
+ """
174
+ Create an EVG task which executes earthly using the given parameters. If this
175
+ function returns `None`, then the task configuration is excluded from executing
176
+ and no task should be defined.
177
+ """
178
+ # Attach tags to the task to allow build variants to select
113
179
# these tasks by the environment of that variant.
114
- env_tags = sorted (f"earthly- { e } " for e in sorted (envs_for (config )))
180
+ env_tags = sorted (e . task_selector_tag for e in sorted (variants_for (config )))
115
181
if not env_tags :
116
182
# All environments have been excluded for this configuration. This means
117
183
# the task itself should not be run:
118
184
return
119
185
# Generate the build-arg arguments based on the configuration options. The
120
186
# NamedTuple field names must match with the ARG keys in the Earthfile!
121
187
earthly_args = [f"--{ key } ={ val } " for key , val in config ._asdict ().items ()]
188
+ # Add arguments that come from parameter expansions defined in the build variant
189
+ earthly_args += [
190
+ f"--env=${{{ _ENV_PARAM_NAME } }}" ,
191
+ f"--c_compiler=${{{ _CC_PARAM_NAME } }}" ,
192
+ ]
122
193
return Task (
123
194
name = name ,
124
195
commands = [
@@ -131,7 +202,6 @@ def earthly_task(
131
202
args = [
132
203
"tools/earthly.sh" ,
133
204
"+env-warmup" ,
134
- f"--env=${{{ _ENV_PARAM_NAME } }}" ,
135
205
* earthly_args ,
136
206
],
137
207
working_dir = "mongoc" ,
@@ -144,7 +214,6 @@ def earthly_task(
144
214
"tools/earthly.sh" ,
145
215
"+run" ,
146
216
f"--targets={ ' ' .join (targets )} " ,
147
- f"--env=${{{ _ENV_PARAM_NAME } }}" ,
148
217
* earthly_args ,
149
218
],
150
219
working_dir = "mongoc" ,
@@ -170,7 +239,7 @@ def earthly_task(
170
239
171
240
172
241
def tasks () -> Iterable [Task ]:
173
- for conf in Configuration . all ( ):
242
+ for conf in all_possible ( Configuration ):
174
243
task = earthly_task (
175
244
name = f"check:{ conf .suffix } " ,
176
245
targets = ("test-example" , "test-cxx-driver" ),
@@ -181,15 +250,4 @@ def tasks() -> Iterable[Task]:
181
250
182
251
183
252
def variants () -> list [BuildVariant ]:
184
- envs : tuple [EnvKey , ...] = get_args (EnvKey )
185
- return [
186
- BuildVariant (
187
- name = f"earthly-{ env } " ,
188
- tasks = [EvgTaskRef (name = f".earthly-{ env } " )],
189
- display_name = _ENV_NAMES [env ],
190
- expansions = {
191
- _ENV_PARAM_NAME : env ,
192
- },
193
- )
194
- for env in envs
195
- ]
253
+ return [ev .as_evg_variant () for ev in all_possible (EarthlyVariant )]
0 commit comments