-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
Copy pathinstall.bzl
857 lines (751 loc) · 31 KB
/
install.bzl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
# -*- python -*-
load("//tools/skylark:py.bzl", "py_binary")
load("@drake//tools/skylark:drake_java.bzl", "MainClassInfo")
load("@drake//tools/skylark:drake_py.bzl", "drake_py_test")
load(
"@drake//tools/skylark:pathutils.bzl",
"dirname",
"join_paths",
"output_path",
)
load("@python//:version.bzl", "PYTHON_SITE_PACKAGES_RELPATH", "PYTHON_VERSION")
InstallInfo = provider()
InstalledTestInfo = provider()
#==============================================================================
#BEGIN internal helpers
#------------------------------------------------------------------------------
def _workspace(ctx):
"""Compute name of current workspace."""
# Check for override
if hasattr(ctx.attr, "workspace"):
if len(ctx.attr.workspace):
return ctx.attr.workspace
# Check for meaningful workspace_root
workspace = ctx.label.workspace_root.split("/")[-1]
if len(workspace):
return workspace
# If workspace_root is empty, assume we are the root workspace
return ctx.workspace_name
def _rename(file_dest, rename):
"""Compute file name if file renamed."""
if file_dest in rename:
renamed = rename[file_dest]
return join_paths(dirname(file_dest), renamed)
return file_dest
def _depset_to_list(x):
"""Helper function to convert depset to list."""
iter_list = x.to_list() if type(x) == "depset" else x
return iter_list
#------------------------------------------------------------------------------
def _output_path(ctx, input_file, strip_prefix = [], warn_foreign = True):
"""Compute output path (without destination prefix) for install action.
This computes the adjusted output path for an input file. It is the same as
:func:`output_path`, but additionally handles files outside the current
package when :func:`install` or :func:`install_files` is invoked with
non-empty ``allowed_externals``.
"""
# Try the current package first
path = output_path(ctx, input_file, strip_prefix)
if path != None:
return path
# If we were unable to resolve a path, the file must be "foreign", so try
# to resolve against the list of allowed externals.
if path == None and hasattr(ctx.attr, "allowed_externals"):
for x in ctx.attr.allowed_externals:
package_root = join_paths(x.label.workspace_root, x.label.package)
path = output_path(ctx, input_file, strip_prefix, package_root)
if path != None:
return path
# If we get here, we were not able to resolve the path; give up, and print
# a warning about installing the "foreign" file.
if warn_foreign:
print("%s installing file %s which is not in current package" %
(ctx.label, input_file.path))
return input_file.basename
#------------------------------------------------------------------------------
def _guess_files(target, candidates, scope, attr_name):
if scope == "EVERYTHING":
return candidates
elif scope == "WORKSPACE":
return [
f
for f in _depset_to_list(candidates)
if target.label.workspace_root == f.owner.workspace_root
]
elif scope == "PACKAGE":
return [
f
for f in _depset_to_list(candidates)
if (target.label.workspace_root == f.owner.workspace_root and
target.label.package == f.owner.package)
]
else:
msg_fmt = "'install' given unknown '%s' value '%s'"
fail(msg_fmt % (attr_name, scope), scope)
#------------------------------------------------------------------------------
def _install_action(
ctx,
artifact,
dests,
strip_prefixes = [],
rename = {},
warn_foreign = True):
"""Compute install action for a single file.
This takes a single file artifact and returns the appropriate install
action for the file. The parameters are the same as for
:func:`_install_action`.
"""
if type(dests) == "dict":
dest = dests.get(artifact.extension, dests[None])
else:
dest = dests
dest_replacements = (
("@WORKSPACE@", _workspace(ctx)),
("@PYTHON_SITE_PACKAGES@", PYTHON_SITE_PACKAGES_RELPATH),
)
for old, new in dest_replacements:
if old in dest:
dest = dest.replace(old, new)
if type(strip_prefixes) == "dict":
strip_prefix = strip_prefixes.get(
artifact.extension,
strip_prefixes[None],
)
else:
strip_prefix = strip_prefixes
file_dest = join_paths(
dest,
_output_path(ctx, artifact, strip_prefix, warn_foreign),
)
file_dest = _rename(file_dest, rename)
return struct(src = artifact, dst = file_dest)
#------------------------------------------------------------------------------
def _install_actions(
ctx,
file_labels,
dests,
strip_prefixes = [],
excluded_files = [],
rename = {},
warn_foreign = True):
"""Compute install actions for files.
This takes a list of labels (targets or files) and computes the install
actions for the files owned by each label.
Args:
file_labels (:obj:`list` of :obj:`Label`): List of labels to install.
dests (:obj:`str` or :obj:`dict` of :obj:`str` to :obj:`str`):
Install destination. A :obj:`dict` may be given to supply a mapping
of file extension to destination path. The :obj:`dict` must have an
entry with the key ``None`` that is used as the default when there
is no entry for the specific extension.
strip_prefixes (:obj:`list` of :obj:`str` or :obj:`dict` of :obj:`list`
of :obj:`str` to :obj:`str`): List of prefixes to strip from the
input path before prepending the destination. A :obj:`dict` may be
given to supply a mapping of file extension to list of prefixes to
strip. The :obj:`dict` must have an entry with the key ``None``
that is used as the default when there is no entry for the specific
extension.
excluded_files (:obj:`list` of :obj:`str`): List of files to exclude
from installation.
Returns:
:obj:`list`: A list of install actions.
"""
actions = []
# Iterate over files. We expect a list of labels, which will have a 'files'
# attribute that is a list of file artifacts. Thus this two-level loop.
for f in file_labels:
for a in _depset_to_list(f.files):
# TODO(mwoehlke-kitware) refactor this to separate computing the
# original relative path and the path with prefix(es) stripped,
# then use the original relative path for both exclusions and
# renaming.
if _output_path(ctx, a, warn_foreign = False) in excluded_files:
continue
actions.append(
_install_action(
ctx,
a,
dests,
strip_prefixes,
rename,
warn_foreign,
),
)
return actions
#------------------------------------------------------------------------------
# Compute install actions for a cc_library or cc_binary.
def _install_cc_actions(ctx, target):
# Compute actions for target artifacts.
dests = {
"a": ctx.attr.archive_dest,
"so": ctx.attr.library_dest,
None: ctx.attr.runtime_dest,
}
strip_prefixes = {
"a": ctx.attr.archive_strip_prefix,
"so": ctx.attr.library_strip_prefix,
None: ctx.attr.runtime_strip_prefix,
}
actions = _install_actions(
ctx,
[target],
dests,
strip_prefixes,
rename = ctx.attr.rename,
)
# Compute actions for guessed resource files.
if ctx.attr.guess_data != "NONE":
data = [
f
for f in _depset_to_list(target.data_runfiles.files)
if f.is_source
]
data = _guess_files(target, data, ctx.attr.guess_data, "guess_data")
actions += _install_actions(
ctx,
[struct(files = data)],
ctx.attr.data_dest,
ctx.attr.data_strip_prefix,
ctx.attr.guess_data_exclude,
rename = ctx.attr.rename,
)
# Compute actions for guessed headers.
if ctx.attr.guess_hdrs != "NONE":
hdrs = _guess_files(
target,
target[CcInfo].compilation_context.headers,
ctx.attr.guess_hdrs,
"guess_hdrs",
)
actions += _install_actions(
ctx,
[struct(files = hdrs)],
ctx.attr.hdr_dest,
ctx.attr.hdr_strip_prefix,
ctx.attr.guess_hdrs_exclude,
rename = ctx.attr.rename,
)
# Return computed actions.
return actions
#------------------------------------------------------------------------------
# Compute install actions for a java_library or java_binary.
def _install_java_actions(ctx, target):
dests = {
"jar": ctx.attr.java_dest,
None: ctx.attr.runtime_dest,
}
strip_prefixes = {
"jar": ctx.attr.java_strip_prefix,
None: ctx.attr.runtime_strip_prefix,
}
excluded_files = []
if target.files_to_run.executable:
excluded_files = [
_output_path(
ctx,
target.files_to_run.executable,
warn_foreign = False,
),
]
return _install_actions(
ctx,
[target],
dests,
strip_prefixes,
excluded_files,
rename = ctx.attr.rename,
)
#------------------------------------------------------------------------------
# Compute install actions for a py_library or py_binary.
# TODO(jamiesnape): Install native shared libraries that the target may use.
def _install_py_actions(ctx, target):
return _install_actions(
ctx,
[target],
ctx.attr.py_dest,
ctx.attr.py_strip_prefix,
rename = ctx.attr.rename,
)
#------------------------------------------------------------------------------
# Compute install actions for a script or an executable.
def _install_runtime_actions(ctx, target):
return _install_actions(
ctx,
[target],
ctx.attr.runtime_dest,
ctx.attr.runtime_strip_prefix,
rename = ctx.attr.rename,
)
#------------------------------------------------------------------------------
# Compute install actions for a java launchers.
def _install_java_launcher_actions(
ctx,
dest,
java_dest,
java_strip_prefix,
rename,
target):
main_class = target[MainClassInfo].main_class
# List runtime_classpath and compute their install paths.
classpath = []
actions = []
for jar in _depset_to_list(target[MainClassInfo].classpath):
jar_install = _install_action(
ctx,
jar,
java_dest,
java_strip_prefix,
rename,
warn_foreign = False,
)
# Adding double quotes around the generated scripts to avoid
# white-space problems when running the generated shell script. This
# string is used in a "for-loop" in the script.
classpath.append(join_paths("$prefix", jar_install.dst))
# Compute destination file name.
filename = target[MainClassInfo].filename
file_dest = join_paths(dest, filename)
file_dest = _rename(file_dest, rename)
jvm_flags = target[MainClassInfo].jvm_flags
actions.append(struct(
dst = file_dest,
classpath = classpath,
jvm_flags = jvm_flags,
main_class = main_class,
))
return actions
#------------------------------------------------------------------------------
def _install_test_actions(ctx):
"""Compute and return list of install test command lines.
This computes the install path for the install tests (tests run to verify
that the project works once installed).
Returns:
:obj:`struct`: A list of test actions containing the location of the
tests files in the source tree and in the install tree.
"""
test_actions = []
# For files, we run the file from the build tree.
for test in ctx.attr.install_tests:
for f in _depset_to_list(test.files):
test_actions.append(
struct(src = f, cmd = f.path),
)
return test_actions
#------------------------------------------------------------------------------
# Generate install code for an install action.
def _install_code(action):
return "install(%r, %r)" % (action.src.short_path, action.dst)
#------------------------------------------------------------------------------
# Generate install code for a java launcher.
def _java_launcher_code(action):
return "create_java_launcher(%r, %r, %r, %r)" % (
action.dst,
action.classpath,
" ".join(action.jvm_flags),
action.main_class,
)
#END internal helpers
#==============================================================================
#BEGIN rules
#------------------------------------------------------------------------------
# Generate information to install "stuff". "Stuff" can be library or binary
# targets, headers, or documentation files.
def _install_impl(ctx):
actions = []
installed_tests = []
rename = dict(ctx.attr.rename)
# Collect install actions from dependencies.
for d in ctx.attr.deps:
actions += d[InstallInfo].install_actions
rename.update(d[InstallInfo].rename)
if InstalledTestInfo in d:
installed_tests += d[InstalledTestInfo].tests
# Generate actions for data, docs and includes.
actions += _install_actions(
ctx,
ctx.attr.docs,
ctx.attr.doc_dest,
strip_prefixes = ctx.attr.doc_strip_prefix,
rename = rename,
)
actions += _install_actions(
ctx,
ctx.attr.data,
ctx.attr.data_dest,
strip_prefixes = ctx.attr.data_strip_prefix,
rename = rename,
)
actions += _install_actions(
ctx,
ctx.attr.hdrs,
ctx.attr.hdr_dest,
strip_prefixes = ctx.attr.hdr_strip_prefix,
rename = rename,
)
for t in ctx.attr.targets:
# TODO(jwnimmer-tri): Raise an error if a target has testonly=1.
if CcInfo in t:
actions += _install_cc_actions(ctx, t)
elif JavaInfo in t:
actions += _install_java_actions(ctx, t)
elif PyInfo in t:
actions += _install_py_actions(ctx, t)
elif MainClassInfo in t:
actions += _install_java_launcher_actions(
ctx,
ctx.attr.runtime_dest,
ctx.attr.java_dest,
ctx.attr.java_strip_prefix,
rename,
t,
)
elif hasattr(t, "files_to_run") and t.files_to_run.executable:
# Executable scripts copied from source directory.
actions += _install_runtime_actions(ctx, t)
# Generate install test actions.
installed_tests += _install_test_actions(ctx)
# Generate code for install actions.
script_actions = []
installed_files = {}
for a in actions:
src = None
if hasattr(a, "src"):
src = a.src
if a.dst not in installed_files:
if src:
script_actions.append(_install_code(a))
else:
script_actions.append(_java_launcher_code(a))
installed_files[a.dst] = src
elif src != installed_files[a.dst]:
fail("Install conflict detected:\n" +
"\n src1 = " + repr(installed_files[a.dst]) +
"\n src2 = " + repr(src) +
"\n dst = " + repr(a.dst))
# Generate installer manifest.
actions_file = ctx.actions.declare_file(ctx.attr.name + "_actions")
ctx.actions.write(
output = actions_file,
content = "\n".join(script_actions) + "\n",
)
# Generate install script.
ctx.actions.write(
output = ctx.outputs.executable,
content = "\n".join([
"#!/bin/bash",
"tools/install/installer --actions {} \"$@\"".format(
actions_file.short_path,
),
]),
is_executable = True,
)
script_tests = []
# Generate list containing all commands to run to test.
for i in installed_tests:
script_tests.append(i.cmd)
# Generate test installation script.
if ctx.attr.install_tests_script and not script_tests:
fail("`install_tests_script` is not empty but no `script_tests` were provided.") # noqa
if ctx.attr.install_tests_script:
ctx.actions.write(
output = ctx.outputs.install_tests_script,
content = "\n".join(script_tests),
is_executable = False,
)
# Return actions.
runfiles = (
[a.src for a in actions if not hasattr(a, "main_class")] +
[i.src for i in installed_tests] +
ctx.files._installer +
[actions_file]
)
return [
InstallInfo(
install_actions = actions,
rename = rename,
installed_files = installed_files,
),
InstalledTestInfo(tests = installed_tests),
DefaultInfo(runfiles = ctx.runfiles(files = runfiles)),
]
# TODO(mwoehlke-kitware) default guess_data to PACKAGE when we have better
# default destinations.
_install_rule = rule(
# Update buildifier-tables.json when this changes.
attrs = {
"deps": attr.label_list(providers = [InstallInfo]),
"docs": attr.label_list(allow_files = True),
"doc_dest": attr.string(default = "share/doc/@WORKSPACE@"),
"doc_strip_prefix": attr.string_list(),
"data": attr.label_list(allow_files = True),
"data_dest": attr.string(default = "share/@WORKSPACE@"),
"data_strip_prefix": attr.string_list(),
"guess_data": attr.string(default = "NONE"),
"guess_data_exclude": attr.string_list(),
"hdrs": attr.label_list(allow_files = True),
"hdr_dest": attr.string(default = "include"),
"hdr_strip_prefix": attr.string_list(),
"guess_hdrs": attr.string(default = "NONE"),
"guess_hdrs_exclude": attr.string_list(),
"targets": attr.label_list(),
"archive_dest": attr.string(default = "lib"),
"archive_strip_prefix": attr.string_list(),
"library_dest": attr.string(default = "lib"),
"library_strip_prefix": attr.string_list(),
"runtime_dest": attr.string(default = "bin"),
"runtime_strip_prefix": attr.string_list(),
"java_dest": attr.string(default = "share/java"),
"java_strip_prefix": attr.string_list(),
"py_dest": attr.string(default = "@PYTHON_SITE_PACKAGES@"),
"py_strip_prefix": attr.string_list(),
"rename": attr.string_dict(),
"install_tests": attr.label_list(
default = [],
allow_files = True,
),
"workspace": attr.string(),
"allowed_externals": attr.label_list(allow_files = True),
"_installer": attr.label(
executable = True,
cfg = "host",
default = Label("//tools/install:installer"),
),
"install_script": attr.output(),
"install_tests_script": attr.output(),
},
executable = True,
implementation = _install_impl,
)
def install(tags = [], **kwargs):
# (The documentation for this function is immediately below.)
_install_rule(
tags = tags + ["install"],
**kwargs
)
"""Generate installation information for various artifacts.
This generates installation information for various artifacts, including
documentation and header files, and targets (e.g. ``cc_binary``). By default,
the path of any files is included in the install destination.
See :rule:`install_files` for details.
Normally, you should not install files or targets from a workspace other than
the one invoking ``install``, and ``install`` will warn if asked to do so. In
cases (e.g. adding install rules to a project that is natively built with
bazel, but does not define an install) where this *is* the right thing to do,
the ``allowed_externals`` argument may be used to specify a list of externals
whose files it is okay to install, which will suppress the warning.
Destination paths may include the following placeholders:
* ``@WORKSPACE@``, replaced with ``workspace`` (if specified) or the name of
the workspace which invokes ``install``.
* ``@PYTHON_SITE_PACKAGES``, replaced with the Python version-specific path of
"site-packages".
Note:
By default, headers and resource files to be installed must be explicitly
listed. This is to work around an issue where Bazel does not appear to
provide any mechanism to obtain the public headers of a target, nor the
*direct* data files of a target, at rule instantiation. The ``guess_hdrs``
and ``guess_data`` parameters may be used to tell ``install`` to guess at
what headers and/or resource files will be installed. Possible values are:
* ``"NONE"``: Only install files which are explicitly listed (i.e. by
``hdrs`` or ``data``).
* ``PACKAGE``: For each target, install those files which are used by the
target and owned by a target in the same package.
* ``WORKSPACE``: For each target, install those files which are used by the
target and owned by a target in the same workspace.
* ``EVERYTHING``: Install all headers/resources used by the target.
The headers and resource files considered are *all* headers or resource
files transitively used by the target. Any option other than ``NONE`` is
also likely to install private headers, and may install resource files used
by other targets. In either case, this may result in the same file being
considered for installation more than once.
Note also that, because Bazel includes *all* run-time dependencies —
including e.g. shared libraries — in a target's ``runfiles``, only *source*
artifacts are considered when guessing resource files.
Java binary launchers are created at install time. The install script is
configured to generate them, but no file other than the install script is
created at build time. Java binary launchers rely on a target containing
a ``MainClassInfo`` provider that contains all the required information to
generate the launcher. Do not forget to provide as dependencies the install
targets that rename files. This will be necessary to use the appropriate
jar file name when creating the java launcher.
MainClassInfo(
main_class = Name of main class to run ("name.class.main")
classpath = List contained in
ctx.attr.target[JavaInfo].compilation_info.runtime_classpath
filename = Java launcher file name
)
A file containing all the commands to test executables after installation
is created if `install_tests_script` is set. The list of commands to run
is given by `install_tests`. The generated file location can be passed to
`install_test()` as an `args`.
Args:
deps: List of other install rules that this rule should include.
docs: List of documentation files to install.
doc_dest: Destination for documentation files
(default = "share/doc/@WORKSPACE@").
doc_strip_prefix: List of prefixes to remove from documentation paths.
guess_data: See note.
guess_data_exclude: List of resources found by ``guess_data`` to exclude
from installation.
data: List of (platform-independent) resource files to install.
data_dest: Destination for resource files (default = "share/@WORKSPACE@").
data_strip_prefix: List of prefixes to remove from resource paths.
guess_hdrs: See note.
guess_hdrs_exclude: List of headers found by ``guess_hdrs`` to exclude from
installation.
hdrs: List of header files to install.
hdr_dest: Destination for header files (default = "include").
hdr_strip_prefix: List of prefixes to remove from header paths.
targets: List of targets to install.
archive_dest: Destination for static library targets (default = "lib").
archive_strip_prefix: List of prefixes to remove from static library paths.
library_dest: Destination for shared library targets (default = "lib").
library_strip_prefix: List of prefixes to remove from shared library paths.
runtime_dest: Destination for executable targets (default = "bin").
runtime_strip_prefix: List of prefixes to remove from executable paths.
java_dest: Destination for Java library targets (default = "share/java").
java_strip_prefix: List of prefixes to remove from Java library paths.
py_dest: Destination for Python targets
(default = "lib/python{MAJOR}.{MINOR}/site-packages").
py_strip_prefix: List of prefixes to remove from Python paths.
rename: Mapping of install paths to alternate file names, used to rename
files upon installation.
install_tests: List of scripts that are designed to test the install
tree. These scripts will not be installed.
install_tests_script: Name of the generated file that contains the commands
run to test the install tree. This only needs to be specified for the
main `install()` call, and the same name should be passed to
`install_test()` as `"$(location :" + install_tests_script + ")"`.
workspace: Workspace name to use in default paths (overrides built-in
guess).
allowed_externals: List of external packages whose files may be installed.
"""
#------------------------------------------------------------------------------
# Generate information to install files to specified destination.
def _install_files_impl(ctx):
# Get path components.
dest = ctx.attr.dest
strip_prefix = ctx.attr.strip_prefix
# Generate actions.
actions = _install_actions(
ctx,
ctx.attr.files,
dest,
strip_prefix,
rename = ctx.attr.rename,
)
# Return computed actions.
return [InstallInfo(install_actions = actions, rename = ctx.attr.rename)]
_install_files_rule = rule(
# Update buildifier-tables.json when this changes.
attrs = {
"dest": attr.string(mandatory = True),
"files": attr.label_list(allow_files = True),
"rename": attr.string_dict(),
"strip_prefix": attr.string_list(),
"workspace": attr.string(),
"allowed_externals": attr.label_list(allow_files = True),
},
implementation = _install_files_impl,
)
def install_files(tags = [], **kwargs):
# (The documentation for this function is immediately below.)
_install_files_rule(
tags = tags + ["install"],
**kwargs
)
"""Generate installation information for files.
This generates installation information for a list of files. By default, any
path portion of the file as named is included in the install destination. For
example::
install_files(
dest = "docs",
files = ["foo/bar.txt"],
...)
This will install ``bar.txt`` to the destination ``docs/foo``.
When this behavior is undesired, the ``strip_prefix`` parameter may be used to
specify a list of prefixes to be removed from input file paths before computing
the destination path. Stripping is not recursive; the first matching prefix
will be stripped. Prefixes support the single-glob (``*``) to match any single
path component, or the multi-glob (``**``) to match any number of path
components. Multi-glob matching is greedy. Globs may only be matched against
complete path components (e.g. ``a/*/`` is okay, but ``a*/`` is not treated as
a glob and will be matched literally). Due to Skylark limitations, at most one
``**`` may be matched.
Destination paths may include the placeholder ``@WORKSPACE``, which is replaced
with ``workspace`` (if specified) or the name of the workspace which invokes
``install``.
``install_files`` has the same caveats regarding external files as
:func:`install`.
Args:
dest: Destination for files.
files: List of files to install.
strip_prefix: List of prefixes to remove from input paths.
rename: Mapping of install paths to alternate file names, used to rename
files upon installation.
workspace: Workspace name to use in default paths (overrides built-in
guess).
allowed_externals: List of external packages whose files may be installed.
"""
#END rules
#==============================================================================
#BEGIN macros
#------------------------------------------------------------------------------
def install_cmake_config(
package,
versioned = True,
name = "install_cmake_config",
visibility = ["//visibility:private"]):
"""Generate installation information for CMake package configuration and
package version files. The rule name is always ``:install_cmake_config``.
Args:
package (:obj:`str`): CMake package name.
versioned (:obj:`bool`): True if a version file should be installed.
"""
package_lower = package.lower()
cmake_config_dest = "lib/cmake/{}".format(package_lower)
cmake_config_files = ["{}-config.cmake".format(package_lower)]
if versioned:
cmake_config_files += ["{}-config-version.cmake".format(package_lower)]
install_files(
name = name,
dest = cmake_config_dest,
files = cmake_config_files,
visibility = visibility,
)
#------------------------------------------------------------------------------
def install_test(
name,
**kwargs):
"""A wrapper to test installed drake executables.
!!!Important: This command should be called only once, when the main
installation step occurs!!!
This wrapper uses `//tools/install:install_test.py` as its main script. It
expects to receive one argument which is the location of a file containing
the list of command to run in the test. The current limitation requires
each command to contain only one command per line. The file containing the
list of command is typically `install_tests_script` outputted by the
`install()` rule.
"""
if native.package_name():
fail("This command should be called only once, " +
"when the main installation step occurs.")
src = "//tools/install:install_test.py"
# We can't use drake_py_unittest here, because the srcs path is atypical.
drake_py_test(
name = name,
# This is an integration test with significant I/O that requires an
# "eternal" timeout so that debug builds are successful. Therefore,
# the test size is increased to "medium", and the timeout to "eternal".
# TODO(jamiesnape): Try to shorten the duration of this test.
size = "medium",
srcs = [src],
timeout = "eternal",
deps = ["//tools/install:install_test_helper"],
# The commands in our "list of commands" use unittest themselves, so we
# do the same for our own test rig. That means that both our rig and
# the "list of commands" python programs must have a __main__ clause
# (i.e., they must all be binaries, not libraries).
allow_import_unittest = True,
**kwargs
)
#END macros