1
- from typing import Iterable
1
+ from __future__ import annotations
2
+
3
+ import functools
4
+ import itertools
5
+ from typing import Any , Iterable , Literal , TypeVar , get_args , NamedTuple , get_type_hints
2
6
from shrub .v3 .evg_build_variant import BuildVariant
3
7
from shrub .v3 .evg_task import EvgTaskRef
4
8
from ..etc .utils import Task
5
9
from shrub .v3 .evg_command import subprocess_exec , EvgCommandType
6
10
7
- _ENV_PARAM_NAME = "MONGOC_EARTHLY_ENV"
8
-
11
+ T = TypeVar ("T" )
9
12
10
- class EarthlyTask (Task ):
11
- def __init__ (self , * , suffix : str , target : str , sasl : str ) -> None :
12
- super ().__init__ (
13
- name = f"earthly-{ suffix } " ,
14
- commands = [
15
- subprocess_exec (
16
- "bash" ,
17
- args = [
18
- "tools/earthly.sh" ,
19
- f"+{ target } " ,
20
- f"--env=${{{ _ENV_PARAM_NAME } }}" ,
21
- f"--enable_sasl={ sasl } " ,
22
- ],
23
- working_dir = "mongoc" ,
24
- command_type = EvgCommandType .TEST ,
25
- )
26
- ],
27
- tags = [f"earthly" , "pr-merge-gate" ],
28
- run_on = CONTAINER_RUN_DISTROS ,
29
- )
13
+ _ENV_PARAM_NAME = "MONGOC_EARTHLY_ENV"
14
+ "The name of the EVG expansion parameter used to key the Earthly build env"
30
15
16
+ EnvKey = Literal ["u22" , "alpine3.18" , "archlinux" ]
17
+ "Identifiers for environments. These correspond to special '*-env' targets in the Earthfile."
31
18
32
- #: A mapping from environment keys to the environment name.
33
- #: These correspond to special "*-env" targets in the Earthfile.
34
- ENVS = {
19
+ _ENV_NAMES : dict [EnvKey , str ] = {
35
20
"u22" : "Ubuntu 22.04" ,
36
21
"alpine3.18" : "Alpine 3.18" ,
37
22
"archlinux" : "Arch Linux" ,
38
23
}
24
+ "A mapping from environment keys to 'pretty' environment names"
25
+
26
+ # Other options: SSPI (Windows only), AUTO (not reliably test-able without more environments)
27
+ SASLOption = Literal ["Cyrus" , "off" ]
28
+ "Valid options for the SASL configuration parameter"
29
+ TLSOption = Literal ["LibreSSL" , "OpenSSL" , "off" ]
30
+ "Options for the TLS backend configuration parameter (AKA 'ENABLE_SSL')"
31
+ CxxVersion = Literal ["master" , "r3.8.0" ]
32
+ "C++ driver refs that are under CI test"
33
+
34
+ # A Unicode non-breaking space character
35
+ _BULLET = "\N{Bullet} "
36
+
37
+
38
+ class Configuration (NamedTuple ):
39
+ """
40
+ Represent a complete set of argument values to give to the Earthly task
41
+ execution. Each field name matches the ARG in the Earthfile.
42
+
43
+ Adding/removing fields will add/remove dimensions on the task matrix.
44
+
45
+ The 'env' parameter is not encoded here, but is managed separately.
46
+
47
+ Keep this in sync with the 'PartialConfiguration' class defined below!
48
+ """
49
+
50
+ sasl : SASLOption
51
+ tls : TLSOption
52
+ test_mongocxx_ref : CxxVersion
53
+
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
+ @property
75
+ def suffix (self ) -> str :
76
+ return f"{ _BULLET } " .join (f"{ k } ={ v } " for k , v in self ._asdict ().items ())
77
+
78
+
79
+ def task_filter (env : EnvKey , conf : Configuration ) -> bool :
80
+ """
81
+ Control which tasks are actually defined by matching on the platform and
82
+ configuration values.
83
+ """
84
+ match env , conf :
85
+ # We only need one task with "sasl=off"
86
+ case "u22" , ("off" , "OpenSSL" , "master" ):
87
+ return True
88
+ # Other sasl=off tasks we'll just ignore:
89
+ case _, ("off" , _tls , _cxx ):
90
+ return False
91
+ # Ubuntu does not ship with a LibreSSL package:
92
+ case e , (_sasl , "LibreSSL" , _cxx ) if _ENV_NAMES [e ].startswith ("Ubuntu" ):
93
+ return False
94
+ # Anything else: Allow it to run:
95
+ case _:
96
+ return True
97
+
98
+
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 )
102
+ allow_env_for_config = functools .partial (task_filter , conf = config )
103
+ return filter (allow_env_for_config , all_envs )
104
+
105
+
106
+ def earthly_task (
107
+ * ,
108
+ name : str ,
109
+ targets : Iterable [str ],
110
+ config : Configuration ,
111
+ ) -> Task | None :
112
+ # Attach "earthly-xyz" tags to the task to allow build variants to select
113
+ # these tasks by the environment of that variant.
114
+ env_tags = sorted (f"earthly-{ e } " for e in sorted (envs_for (config )))
115
+ if not env_tags :
116
+ # All environments have been excluded for this configuration. This means
117
+ # the task itself should not be run:
118
+ return
119
+ # Generate the build-arg arguments based on the configuration options. The
120
+ # NamedTuple field names must match with the ARG keys in the Earthfile!
121
+ earthly_args = [f"--{ key } ={ val } " for key , val in config ._asdict ().items ()]
122
+ return Task (
123
+ name = name ,
124
+ commands = [
125
+ # First, just build the "env-warmup" which will prepare the build environment.
126
+ # This won't generate any output, but allows EVG to track it as a separate build step
127
+ # for timing and logging purposes. The subequent build step will cache-hit the
128
+ # warmed-up build environments.
129
+ subprocess_exec (
130
+ "bash" ,
131
+ args = [
132
+ "tools/earthly.sh" ,
133
+ "+env-warmup" ,
134
+ f"--env=${{{ _ENV_PARAM_NAME } }}" ,
135
+ * earthly_args ,
136
+ ],
137
+ working_dir = "mongoc" ,
138
+ command_type = EvgCommandType .SETUP ,
139
+ ),
140
+ # Now execute the main tasks:
141
+ subprocess_exec (
142
+ "bash" ,
143
+ args = [
144
+ "tools/earthly.sh" ,
145
+ "+run" ,
146
+ f"--targets={ ' ' .join (targets )} " ,
147
+ f"--env=${{{ _ENV_PARAM_NAME } }}" ,
148
+ * earthly_args ,
149
+ ],
150
+ working_dir = "mongoc" ,
151
+ command_type = EvgCommandType .TEST ,
152
+ ),
153
+ ],
154
+ tags = [f"earthly" , "pr-merge-gate" , * env_tags ],
155
+ run_on = CONTAINER_RUN_DISTROS ,
156
+ )
157
+
39
158
40
159
CONTAINER_RUN_DISTROS = [
41
160
"ubuntu2204-small" ,
@@ -49,28 +168,28 @@ def __init__(self, *, suffix: str, target: str, sasl: str) -> None:
49
168
"amazon2" ,
50
169
]
51
170
52
- # Other options: SSPI (Windows only), AUTO (not reliably test-able without more environments)
53
- _SASL_OPTIONS = ["CYRUS" , "OFF" ]
54
-
55
171
56
172
def tasks () -> Iterable [Task ]:
57
- for sasl in _SASL_OPTIONS :
58
- yield EarthlyTask (
59
- suffix = f"build- check-sasl: { sasl } " . lower () ,
60
- target = "test-example" ,
61
- sasl = sasl ,
173
+ for conf in Configuration . all () :
174
+ task = earthly_task (
175
+ name = f"check: { conf . suffix } " ,
176
+ targets = ( "test-example" , "test-cxx-driver" ) ,
177
+ config = conf ,
62
178
)
179
+ if task is not None :
180
+ yield task
63
181
64
182
65
183
def variants () -> list [BuildVariant ]:
184
+ envs : tuple [EnvKey , ...] = get_args (EnvKey )
66
185
return [
67
186
BuildVariant (
68
- name = f"earthly-{ env_key } " ,
69
- tasks = [EvgTaskRef (name = ".earthly" )],
70
- display_name = env_name ,
187
+ name = f"earthly-{ env } " ,
188
+ tasks = [EvgTaskRef (name = f ".earthly- { env } " )],
189
+ display_name = _ENV_NAMES [ env ] ,
71
190
expansions = {
72
- _ENV_PARAM_NAME : env_key ,
191
+ _ENV_PARAM_NAME : env ,
73
192
},
74
193
)
75
- for env_key , env_name in ENVS . items ()
194
+ for env in envs
76
195
]
0 commit comments