Skip to content

Commit 66227f8

Browse files
committed
Detect failure of pre-assembly in R buildpack
If the pre-assembly step succeeds we do not re-run it after copying the repository contents.
1 parent 0eae8dc commit 66227f8

File tree

2 files changed

+62
-27
lines changed

2 files changed

+62
-27
lines changed

repo2docker/buildpacks/base.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,14 @@
125125
# of the repository but don't access any files in the repository. By executing
126126
# them before copying the repository itself we can cache these steps. For
127127
# example installing APT packages.
128-
{% for sd in pre_assemble_script_directives -%}
128+
{% if preassemble_script_files -%}
129+
# If scripts required during build are present, copy them
130+
{% for src, dst in preassemble_script_files.items() %}
131+
COPY src/{{ src }} ${REPO_DIR}/{{ dst }}
132+
{% endfor -%}
133+
{% endif -%}
134+
135+
{% for sd in preassemble_script_directives -%}
129136
{{ sd }}
130137
{% endfor %}
131138
@@ -379,6 +386,19 @@ def get_build_scripts(self):
379386

380387
return []
381388

389+
def get_preassemble_script_files(self):
390+
"""
391+
Dict of files to be copied to the container image for use in preassembly.
392+
393+
This is copied before the `build_scripts`, `preassemble_scripts` and
394+
`assemble_scripts` are run, so can be executed from either of them.
395+
396+
It's a dictionary where the key is the source file path in the
397+
repository and the value is the destination file path inside the
398+
repository in the container.
399+
"""
400+
return {}
401+
382402
def get_preassemble_scripts(self):
383403
"""
384404
Ordered list of shell snippets to build an image for this repository.
@@ -499,13 +519,13 @@ def render(self):
499519
"RUN {}".format(textwrap.dedent(script.strip("\n")))
500520
)
501521

502-
pre_assemble_script_directives = []
522+
preassemble_script_directives = []
503523
last_user = "root"
504524
for user, script in self.get_preassemble_scripts():
505525
if last_user != user:
506-
pre_assemble_script_directives.append("USER {}".format(user))
526+
preassemble_script_directives.append("USER {}".format(user))
507527
last_user = user
508-
pre_assemble_script_directives.append(
528+
preassemble_script_directives.append(
509529
"RUN {}".format(textwrap.dedent(script.strip("\n")))
510530
)
511531

@@ -516,7 +536,8 @@ def render(self):
516536
env=self.get_env(),
517537
labels=self.get_labels(),
518538
build_script_directives=build_script_directives,
519-
pre_assemble_script_directives=pre_assemble_script_directives,
539+
preassemble_script_files=self.get_preassemble_script_files(),
540+
preassemble_script_directives=preassemble_script_directives,
520541
assemble_script_directives=assemble_script_directives,
521542
build_script_files=self.get_build_script_files(),
522543
base_packages=sorted(self.get_base_packages()),

repo2docker/buildpacks/r.py

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -291,39 +291,53 @@ def get_build_scripts(self):
291291

292292
return super().get_build_scripts() + scripts
293293

294+
def get_preassemble_script_files(self):
295+
files = {}
296+
installR_path = self.binder_path("install.R")
297+
if os.path.exists(installR_path):
298+
files[installR_path] = installR_path
299+
300+
return files
301+
294302
def get_preassemble_scripts(self):
295-
"""Install contents of install.R"""
303+
"""Install contents of install.R
304+
305+
Attempt to execute `install.R` before copying the contents of the
306+
repository. We speculate that most of the time we do not need access.
307+
In case this fails we re-run it after copying the repository contents.
308+
309+
The advantage of executing it before copying is that minor edits to the
310+
repository content will not trigger a re-install making things faster.
311+
"""
296312
scripts = []
297313

298314
installR_path = self.binder_path("install.R")
299315
if os.path.exists(installR_path):
300-
packages = []
301-
with open(installR_path) as f:
302-
for line in f:
303-
line = line.strip()
304-
# skip commented out lines
305-
if line.startswith("#"):
306-
continue
307-
else:
308-
# XXX This needs a better solution for detecting which
309-
# XXX kind of string quoting the user used/how to
310-
# XXX escape quotes...
311-
# using " as quote
312-
if '"' in line:
313-
packages.append("-e '{}'".format(line))
314-
# using ' as quote or no quotes
315-
else:
316-
packages.append('-e "{}"'.format(line))
317-
318-
package_expressions = " \\ \n".join(sorted(packages))
319-
scripts += [("${NB_USER}", "R --quiet %s" % package_expressions)]
316+
scripts += [
317+
(
318+
"${NB_USER}",
319+
"Rscript %s && touch /tmp/.preassembled || true" % installR_path,
320+
)
321+
]
320322

321323
return super().get_preassemble_scripts() + scripts
322324

323325
def get_assemble_scripts(self):
324-
"""Install the repository itself as a R package"""
326+
"""Install the dependencies of or the repository itself"""
325327
assemble_scripts = super().get_assemble_scripts()
326328

329+
installR_path = self.binder_path("install.R")
330+
if os.path.exists(installR_path):
331+
assemble_scripts += [
332+
(
333+
"${NB_USER}",
334+
# only run install.R if the pre-assembly failed
335+
"if [ ! -f /tmp/.preassembled ]; then Rscript {}; fi".format(
336+
installR_path
337+
),
338+
)
339+
]
340+
327341
description_R = "DESCRIPTION"
328342
if not self.binder_dir and os.path.exists(description_R):
329343
assemble_scripts += [

0 commit comments

Comments
 (0)