Skip to content

Commit d973bd8

Browse files
authored
Add support for custom build-prerequisites Dockerfile (#260)
This commit adds `--prerequisites-dockerfile=[path]` flag that allows injecting custom image in place of ue4-build-prerequisites. Note that vanilla ue4-build-prerequisites is still build and available with ue4-base-build-prerequisites name, so user-provided Dockerfile can use it as a base. Primary goal of this feature is to allow installation of platform SDKs before engine is checked out into image. Placing platform SDKs in to ue4-build-prerequisites image allows to reuse the same SDKs for multiple engine builds.
1 parent f6b30c7 commit d973bd8

File tree

4 files changed

+130
-57
lines changed

4 files changed

+130
-57
lines changed

ue4docker/build.py

+32-15
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,11 @@ def build():
8888
# Create an auto-deleting temporary directory to hold our build context
8989
with tempfile.TemporaryDirectory() as tempDir:
9090

91-
# Copy our Dockerfiles to the temporary directory
9291
contextOrig = join(os.path.dirname(os.path.abspath(__file__)), "dockerfiles")
93-
contextRoot = join(tempDir, "dockerfiles")
94-
shutil.copytree(contextOrig, contextRoot)
9592

9693
# Create the builder instance to build the Docker images
9794
builder = ImageBuilder(
98-
contextRoot,
95+
join(tempDir, "dockerfiles"),
9996
config.containerPlatform,
10097
logger,
10198
config.rebuild,
@@ -383,17 +380,37 @@ def build():
383380
"VISUAL_STUDIO_BUILD_NUMBER=" + config.visualStudioBuildNumber,
384381
]
385382

386-
builder.build(
387-
"ue4-build-prerequisites",
388-
[config.prereqsTag],
389-
commonArgs + config.platformArgs + prereqsArgs,
390-
)
391-
builtImages.append("ue4-build-prerequisites")
383+
custom_prerequisites_dockerfile = config.args.prerequisites_dockerfile
384+
if custom_prerequisites_dockerfile is not None:
385+
builder.build_builtin_image(
386+
"ue4-base-build-prerequisites",
387+
[config.prereqsTag],
388+
commonArgs + config.platformArgs + prereqsArgs,
389+
builtin_name="ue4-build-prerequisites",
390+
)
391+
builtImages.append("ue4-base-build-prerequisites")
392+
else:
393+
builder.build_builtin_image(
394+
"ue4-build-prerequisites",
395+
[config.prereqsTag],
396+
commonArgs + config.platformArgs + prereqsArgs,
397+
)
392398

393399
prereqConsumerArgs = [
394400
"--build-arg",
395401
"PREREQS_TAG={}".format(config.prereqsTag),
396402
]
403+
404+
if custom_prerequisites_dockerfile is not None:
405+
builder.build(
406+
"ue4-build-prerequisites",
407+
[config.prereqsTag],
408+
commonArgs + config.platformArgs + prereqConsumerArgs,
409+
dockerfile_template=custom_prerequisites_dockerfile,
410+
context_dir=os.path.dirname(custom_prerequisites_dockerfile),
411+
)
412+
413+
builtImages.append("ue4-build-prerequisites")
397414
else:
398415
logger.info("Skipping ue4-build-prerequisities image build.")
399416

@@ -418,11 +435,11 @@ def build():
418435
"--build-arg",
419436
"VERBOSE_OUTPUT={}".format("1" if config.verbose == True else "0"),
420437
]
421-
builder.build(
438+
builder.build_builtin_image(
422439
"ue4-source",
423440
mainTags,
424441
commonArgs + config.platformArgs + ue4SourceArgs + credentialArgs,
425-
secrets,
442+
secrets=secrets,
426443
)
427444
builtImages.append("ue4-source")
428445
else:
@@ -436,7 +453,7 @@ def build():
436453

437454
# Build the UE4 Engine source build image, unless requested otherwise by the user
438455
if config.buildTargets["engine"]:
439-
builder.build(
456+
builder.build_builtin_image(
440457
"ue4-engine",
441458
mainTags,
442459
commonArgs + config.platformArgs + ue4BuildArgs,
@@ -453,7 +470,7 @@ def build():
453470
else []
454471
)
455472

456-
builder.build(
473+
builder.build_builtin_image(
457474
"ue4-minimal",
458475
mainTags,
459476
commonArgs + config.platformArgs + ue4BuildArgs + minimalArgs,
@@ -483,7 +500,7 @@ def build():
483500
)
484501

485502
# Build the image
486-
builder.build(
503+
builder.build_builtin_image(
487504
"ue4-full",
488505
mainTags,
489506
commonArgs

ue4docker/infrastructure/BuildConfiguration.py

+5
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,11 @@ def addArguments(parser):
264264
default=None,
265265
help="Set a specific changelist number in the Unreal Engine's Build.version file",
266266
)
267+
parser.add_argument(
268+
"--prerequisites-dockerfile",
269+
default=None,
270+
help="Specifies path to custom ue4-build-prerequisites dockerfile",
271+
)
267272

268273
def __init__(self, parser, argv, logger):
269274
"""

ue4docker/infrastructure/DockerUtils.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def exists(name):
4545
return False
4646

4747
@staticmethod
48-
def build(tags, context, args):
48+
def build(tags: [str], context: str, args: [str]) -> [str]:
4949
"""
5050
Returns the `docker build` command to build an image
5151
"""
@@ -58,7 +58,7 @@ def build(tags, context, args):
5858
)
5959

6060
@staticmethod
61-
def buildx(tags, context, args, secrets):
61+
def buildx(tags: [str], context: str, args: [str], secrets: [str]) -> [str]:
6262
"""
6363
Returns the `docker buildx` command to build an image with the BuildKit backend
6464
"""

ue4docker/infrastructure/ImageBuilder.py

+91-40
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,34 @@
55
from .GlobalConfiguration import GlobalConfiguration
66
import glob, humanfriendly, os, shutil, subprocess, tempfile, time
77
from os.path import basename, exists, join
8-
from jinja2 import Environment, Template
8+
from jinja2 import Environment
9+
10+
11+
class ImageBuildParams(object):
12+
def __init__(
13+
self, dockerfile: str, context_dir: str, env: Optional[Dict[str, str]] = None
14+
):
15+
self.dockerfile = dockerfile
16+
self.context_dir = context_dir
17+
self.env = env
918

1019

1120
class ImageBuilder(object):
1221
def __init__(
1322
self,
14-
root,
15-
platform,
23+
tempDir: str,
24+
platform: str,
1625
logger,
17-
rebuild=False,
18-
dryRun=False,
19-
layoutDir=None,
20-
templateContext=None,
21-
combine=False,
26+
rebuild: bool = False,
27+
dryRun: bool = False,
28+
layoutDir: str = None,
29+
templateContext: Dict[str, str] = None,
30+
combine: bool = False,
2231
):
2332
"""
2433
Creates an ImageBuilder for the specified build parameters
2534
"""
26-
self.root = root
35+
self.tempDir = tempDir
2736
self.platform = platform
2837
self.logger = logger
2938
self.rebuild = rebuild
@@ -32,17 +41,60 @@ def __init__(
3241
self.templateContext = templateContext if templateContext is not None else {}
3342
self.combine = combine
3443

35-
def build(self, name, tags, args, secrets=None):
44+
def get_built_image_context(self, name):
45+
"""
46+
Resolve the full path to the build context for the specified image
47+
"""
48+
return os.path.normpath(
49+
os.path.join(
50+
os.path.dirname(os.path.abspath(__file__)),
51+
"..",
52+
"dockerfiles",
53+
basename(name),
54+
self.platform,
55+
)
56+
)
57+
58+
def build_builtin_image(
59+
self,
60+
name: str,
61+
tags: [str],
62+
args: [str],
63+
builtin_name: str = None,
64+
secrets: Dict[str, str] = None,
65+
):
66+
context_dir = self.get_built_image_context(
67+
name if builtin_name is None else builtin_name
68+
)
69+
return self.build(
70+
name, tags, args, join(context_dir, "Dockerfile"), context_dir, secrets
71+
)
72+
73+
def build(
74+
self,
75+
name: str,
76+
tags: [str],
77+
args: [str],
78+
dockerfile_template: str,
79+
context_dir: str,
80+
secrets: Dict[str, str] = None,
81+
):
3682
"""
3783
Builds the specified image if it doesn't exist or if we're forcing a rebuild
3884
"""
3985

86+
workdir = join(self.tempDir, basename(name), self.platform)
87+
os.makedirs(workdir, exist_ok=True)
88+
4089
# Create a Jinja template environment and render the Dockerfile template
4190
environment = Environment(
4291
autoescape=False, trim_blocks=True, lstrip_blocks=True
4392
)
44-
dockerfile = join(self.context(name), "Dockerfile")
45-
templateInstance = environment.from_string(FilesystemUtils.readFile(dockerfile))
93+
dockerfile = join(workdir, "Dockerfile")
94+
95+
templateInstance = environment.from_string(
96+
FilesystemUtils.readFile(dockerfile_template)
97+
)
4698
rendered = templateInstance.render(self.templateContext)
4799

48100
# Compress excess whitespace introduced during Jinja rendering and save the contents back to disk
@@ -70,7 +122,6 @@ def build(self, name, tags, args, secrets=None):
70122

71123
# Determine whether we are building using `docker buildx` with build secrets
72124
imageTags = self._formatTags(name, tags)
73-
command = DockerUtils.build(imageTags, self.context(name), args)
74125

75126
if self.platform == "linux" and secrets is not None and len(secrets) > 0:
76127

@@ -82,9 +133,11 @@ def build(self, name, tags, args, secrets=None):
82133
secretFlags.append("id={},src={}".format(secret, secretFile))
83134

84135
# Generate the `docker buildx` command to use our build secrets
85-
command = DockerUtils.buildx(
86-
imageTags, self.context(name), args, secretFlags
87-
)
136+
command = DockerUtils.buildx(imageTags, context_dir, args, secretFlags)
137+
else:
138+
command = DockerUtils.build(imageTags, context_dir, args)
139+
140+
command += ["--file", dockerfile]
88141

89142
env = os.environ.copy()
90143
if self.platform == "linux":
@@ -97,41 +150,35 @@ def build(self, name, tags, args, secrets=None):
97150
command,
98151
"build",
99152
"built",
100-
env=env,
153+
ImageBuildParams(dockerfile, context_dir, env),
101154
)
102155

103-
def context(self, name):
104-
"""
105-
Resolve the full path to the build context for the specified image
106-
"""
107-
return join(self.root, basename(name), self.platform)
108-
109-
def pull(self, image):
156+
def pull(self, image: str) -> None:
110157
"""
111158
Pulls the specified image if it doesn't exist or if we're forcing a pull of a newer version
112159
"""
113160
self._processImage(image, None, DockerUtils.pull(image), "pull", "pulled")
114161

115-
def willBuild(self, name, tags):
162+
def willBuild(self, name: str, tags: [str]) -> bool:
116163
"""
117164
Determines if we will build the specified image, based on our build settings
118165
"""
119166
imageTags = self._formatTags(name, tags)
120167
return self._willProcess(imageTags[0])
121168

122-
def _formatTags(self, name, tags):
169+
def _formatTags(self, name: str, tags: [str]):
123170
"""
124171
Generates the list of fully-qualified tags that we will use when building an image
125172
"""
126173
return [
127174
"{}:{}".format(GlobalConfiguration.resolveTag(name), tag) for tag in tags
128175
]
129176

130-
def _willProcess(self, image):
177+
def _willProcess(self, image: [str]) -> bool:
131178
"""
132179
Determines if we will build or pull the specified image, based on our build settings
133180
"""
134-
return self.rebuild == True or DockerUtils.exists(image) == False
181+
return self.rebuild or not DockerUtils.exists(image)
135182

136183
def _processImage(
137184
self,
@@ -140,14 +187,14 @@ def _processImage(
140187
command: [str],
141188
actionPresentTense: str,
142189
actionPastTense: str,
143-
env: Optional[Dict[str, str]] = None,
144-
):
190+
build_params: Optional[ImageBuildParams] = None,
191+
) -> None:
145192
"""
146193
Processes the specified image by running the supplied command if it doesn't exist (use rebuild=True to force processing)
147194
"""
148195

149196
# Determine if we are processing the image
150-
if self._willProcess(image) == False:
197+
if not self._willProcess(image):
151198
self.logger.info(
152199
'Image "{}" exists and rebuild not requested, skipping {}.'.format(
153200
image, actionPresentTense
@@ -159,7 +206,7 @@ def _processImage(
159206
self.logger.action(
160207
'{}ing image "{}"...'.format(actionPresentTense.capitalize(), image)
161208
)
162-
if self.dryRun == True:
209+
if self.dryRun:
163210
print(command)
164211
self.logger.action(
165212
'Completed dry run for image "{}".'.format(image), newline=False
@@ -170,19 +217,19 @@ def _processImage(
170217
if self.layoutDir is not None:
171218

172219
# Determine whether we're performing a simple copy or combining generated Dockerfiles
173-
source = self.context(name)
174-
if self.combine == True:
220+
if self.combine:
175221

176222
# Ensure the destination directory exists
177223
dest = join(self.layoutDir, "combined")
178224
self.logger.action(
179-
'Merging "{}" into "{}"...'.format(source, dest), newline=False
225+
'Merging "{}" into "{}"...'.format(build_params.context_dir, dest),
226+
newline=False,
180227
)
181228
os.makedirs(dest, exist_ok=True)
182229

183230
# Merge the source Dockerfile with any existing Dockerfile contents in the destination directory
184231
# (Insert a single newline between merged file contents and ensure we have a single trailing newline)
185-
sourceDockerfile = join(source, "Dockerfile")
232+
sourceDockerfile = build_params.dockerfile
186233
destDockerfile = join(dest, "Dockerfile")
187234
dockerfileContents = (
188235
FilesystemUtils.readFile(destDockerfile)
@@ -199,7 +246,7 @@ def _processImage(
199246

200247
# Copy any supplemental files from the source directory to the destination directory
201248
# (Exclude any extraneous files which are not referenced in the Dockerfile contents)
202-
for file in glob.glob(join(source, "*.*")):
249+
for file in glob.glob(join(build_params.context_dir, "*.*")):
203250
if basename(file) in dockerfileContents:
204251
shutil.copy(file, join(dest, basename(file)))
205252

@@ -213,9 +260,11 @@ def _processImage(
213260
# Copy the source directory to the destination
214261
dest = join(self.layoutDir, basename(name))
215262
self.logger.action(
216-
'Copying "{}" to "{}"...'.format(source, dest), newline=False
263+
'Copying "{}" to "{}"...'.format(build_params.context_dir, dest),
264+
newline=False,
217265
)
218-
shutil.copytree(source, dest)
266+
shutil.copytree(build_params.context_dir, dest)
267+
shutil.copy(build_params.dockerfile, dest)
219268
self.logger.action(
220269
'Copied Dockerfile for image "{}".'.format(image), newline=False
221270
)
@@ -224,7 +273,9 @@ def _processImage(
224273

225274
# Attempt to process the image using the supplied command
226275
startTime = time.time()
227-
exitCode = subprocess.call(command, env=env)
276+
exitCode = subprocess.call(
277+
command, env=build_params.env if build_params else None
278+
)
228279
endTime = time.time()
229280

230281
# Determine if processing succeeded

0 commit comments

Comments
 (0)