Repository navigation
Expand file tree
/
Copy pathdocs.bzl
More file actions
988 lines (899 loc) · 41.5 KB
/
Copy pathdocs.bzl
File metadata and controls
988 lines (899 loc) · 41.5 KB
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
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
# *******************************************************************************
# Copyright (c) 2025 Contributors to the Eclipse Foundation
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# This program and the accompanying materials are made available under the
# terms of the Apache License Version 2.0 which is available at
# https://www.apache.org/licenses/LICENSE-2.0
#
# SPDX-License-Identifier: Apache-2.0
# *******************************************************************************
"""Public Bazel macros for building and composing S-CORE documentation.
The ``docs_bundle`` macro describes which documentation files belong to a
reusable bundle and how nested bundles are composed. Source-bearing bundles
also create a ``<name>.__internal__.needs_local`` export containing Needs from
their own sources. The root bundle created by ``docs()`` follows the same rule;
its existing project-wide ``needs_json`` export remains available as well.
The root ``docs()`` build can import another public bundle's local inventory to
resolve references to Needs owned by that bundle. A bundle can associate itself
with a repository's root docs configuration without depending on the root
bundle's composed content.
"""
# Multiple approaches are available to build the same documentation output:
#
# 1. **Esbonio via IDE support (`ide_support` target)**:
# - Listed first as it offers the least flexibility in implementation.
# - Designed for live previews and quick iterations when editing documentation.
# - Integrates with IDEs like VS Code but requires the Esbonio extension.
# - Requires a virtual environment with consistent dependencies (see 2).
#
# 2. **Directly running Sphinx in the virtual environment**:
# - As mentioned above, a virtual environment is required for running esbonio.
# - Therefore, the same environment can be used to run Sphinx directly.
# - Option 1: Run Sphinx manually via `.venv_docs/bin/python -m sphinx docs _build --jobs auto`.
# - Option 2: Use the `incremental` target, which simplifies this process.
# - Usable in CI pipelines to validate the virtual environment used by Esbonio.
# - Ideal for quickly generating documentation during development.
#
# 3. **Bazel-based build (`docs` target)**:
# - Runs the documentation build in a Bazel sandbox, ensuring clean, isolated builds.
# - Less convenient for frequent local edits but ensures build reproducibility.
#
# **Consistency**:
# When modifying Sphinx extensions or configuration, ensure all three methods
# (Esbonio, incremental, and Bazel) work as expected to avoid discrepancies.
#
# For user-facing documentation, refer to `/README.md`.
load("@aspect_rules_py//py:defs.bzl", "py_binary", "py_venv")
load("@docs_as_code_hub_env//:requirements.bzl", "all_requirements")
load(
"@score_docs_as_code//:bzl/basics.bzl",
"glob_doc_sources",
"join_path",
)
load(
"@score_docs_as_code//:bzl/bundle_rules.bzl",
"collect_bundle_code_files",
"create_bundle",
"declare_docs_config_target",
"external_docs_runfiles",
"generate_code_target_sourcelinks",
"merge_bundle_sourcelinks",
"sphinx_config_options",
)
load(
"@score_docs_as_code//:bzl/code_file_lists.bzl",
"declare_code_file_lists",
)
load(
"@score_docs_as_code//:bzl/mount_rules.bzl",
"create_composition_manifest",
)
# Keep the low-level action behind this name so this macro owns the shared
# Sphinx policy while ``needs_rules.bzl`` owns Bazel's input/output plumbing.
load("@score_docs_as_code//:bzl/needs_rules.bzl", "sphinx_docs")
def _sphinx_define(name, value):
"""Return a Sphinx ``--define`` option when ``value`` is configured."""
if value == None:
return []
return ["--define=" + name + "=" + value]
def _needs_sphinx_extra_opts(
master_doc,
score_bundle_needs_export,
score_source_code_linker_plain_links):
"""Return Sphinx ``--define`` options that vary between Needs actions.
These values cross the Bazel action boundary as command-line options. The
shared warning and traceback flags are supplied by the launcher instead.
"""
# The launcher supplies diagnostics shared by every builder. Keep only
# target-specific defines here so the action does not receive duplicate
# ``-W``, ``--keep-going``, and ``-T`` options after JSON transport.
return [
option
for name, value in [
("master_doc", master_doc),
("score_bundle_needs_export", score_bundle_needs_export),
("score_source_code_linker_plain_links", score_source_code_linker_plain_links),
]
for option in _sphinx_define(name, value)
]
def _declare_sphinx_build_binary(name, data, deps):
"""Declare the private Sphinx executable used by one Needs target."""
sphinx_build_name = _bundle_internal_target(name, "sphinx_build")
py_binary(
name = sphinx_build_name,
srcs = [Label("//src/docs_cli:cli.py")],
data = data,
deps = deps,
# The Sphinx executable is an implementation detail of the Needs
# target; only the generated Needs target itself needs the requested
# visibility.
visibility = ["//visibility:private"],
tags = ["manual"],
)
return ":" + sphinx_build_name
def _needs_sphinx_docs(
name,
config,
sphinx_build_deps,
bundle,
master_doc = None,
external_needs_labels = "[]",
score_bundle_needs_export = None,
score_sourcelinks_json = None,
score_source_code_linker_plain_links = None,
mounts_manifest = None,
score_metamodel_yaml = None,
tools = [],
sphinx_build_data = [],
visibility = None):
"""Declare one Bazel action that runs Sphinx's Needs builder.
String settings travel as Sphinx ``--define`` options. Bazel labels stay
typed until the private rule can declare them as sandbox inputs.
"""
# These files use typed rule attributes below so the private rule can
# pass their paths to the action. Add them to ``tools`` as well: that puts
# the files in the Sphinx executable's runfiles (for Python-side lookup)
# and declares them as action inputs (for sandbox availability). Callers
# therefore do not need to list the same labels twice.
tools = tools + [
label
for label in [score_sourcelinks_json, mounts_manifest, score_metamodel_yaml]
if label
]
sphinx_build = _declare_sphinx_build_binary(
name,
sphinx_build_data + [tool for tool in tools if tool not in sphinx_build_data],
sphinx_build_deps,
)
sphinx_docs(
name = name,
bundle = bundle,
# A source conf.py is an optional action input. When it is absent,
# ``config_options`` puts the structured settings on Sphinx's command
# line and the launcher selects configuration-free mode.
config = config,
data = sphinx_build_data,
extra_opts = _needs_sphinx_extra_opts(
master_doc,
score_bundle_needs_export,
score_source_code_linker_plain_links,
),
# Keep these as labels rather than path strings in ``extra_opts``. The
# private rule declares them as action inputs and provides execroot
# paths directly through the environment.
external_needs_labels = external_needs_labels,
score_sourcelinks_json = score_sourcelinks_json,
mounts_manifest = mounts_manifest,
score_metamodel_yaml = score_metamodel_yaml,
sphinx = sphinx_build,
tools = tools,
visibility = visibility,
tags = ["manual"],
)
def _module_name_without_prefix():
"""Return the current Bazel module name without its first prefix."""
module_name = native.module_name()
if not module_name:
return ""
return module_name.split("_", 1)[-1]
def _bundle_internal_target(name, target):
"""Return the conventional name for a target internal to a bundle."""
return name + ".__internal__." + target
def _bundle_needs_local_label(bundle):
"""Map a public docs or docs_bundle label to its local Needs export.
The public target identifies the bundle that owns the Needs. Its private
sibling export has a predictable name, but callers should not have to use
that implementation name directly.
"""
label = str(bundle)
# A package-local label is meaningful inside the BUILD file invoking the
# macro. Turn it into an absolute workspace label before passing it to the
# Python extension, which resolves inventory paths from Bazel runfiles.
if label.startswith(":"):
label = "//" + native.package_name() + label
# The public API accepts Bazel targets, not file paths or arbitrary data
# labels. Check that here so malformed values fail close to the BUILD file
# declaration instead of much later during Sphinx startup.
if not (label.startswith("//") or label.startswith("@")) or ":" not in label:
fail(
"external_needs entries must be Bazel target labels, got %r" % label,
)
package, target = label.rsplit(":", 1)
# Bundle callers name public targets. Keep generated implementation names
# private as more internal targets are added over time.
if "__internal__" in target:
fail(
"external_needs entries must name a docs() or docs_bundle target, got %r" %
label,
)
return package + ":" + _bundle_internal_target(target, "needs_local")
def _resolve_external_needs_labels(external_needs):
"""Resolve public bundle labels and preserve supported inventory labels.
Public ``docs()`` and ``docs_bundle`` targets are translated to their
owner-only Needs exports. ``needs_json_file`` directly identifies an
inventory file, so keep that supported target unchanged. Also keep
``needs_json`` working as a deprecated form without a warning for now.
"""
inventory_labels = []
for external_need in external_needs:
label = str(external_need)
if label.startswith(":"):
label = "//" + native.package_name() + label
target = label.rsplit(":", 1)[-1]
if target in ("needs_json", "needs_json_file"):
inventory_labels.append(label)
else:
inventory_labels.append(_bundle_needs_local_label(label))
return inventory_labels
def _package_relative_project_url(base_url, package_path):
"""Append a workspace-relative Bazel package to a project URL."""
return join_path(base_url, package_path)
def _root_docs_config_label(root_docs):
"""Map a public ``docs`` label to its internal config target.
``root_docs`` is intentionally the user-facing label. The executable
``//:docs`` target may depend on the bundles that reference it, so using it
directly as a provider dependency would create a Bazel cycle. ``docs()``
publishes the provider on the sibling ``.__internal__.config`` target
instead; that target contains only scalar configuration and the metamodel.
"""
root_docs_label = str(root_docs)
if ":" not in root_docs_label:
fail("root_docs must be a Bazel target label, got %r" % root_docs)
return root_docs_label + ".__internal__.config"
def _is_needs_json_target(label):
"""Return whether ``label`` names the directory-valued ``needs_json`` target.
The target is a Bazel TreeArtifact containing ``needs.json`` and other
generated outputs, so it is a build/runfile input rather than a file that
belongs in a portable documentation-bundle mount. Keep this distinction at
the public macro boundary instead of making generic bundle and mount logic
handle the special-purpose generated directory.
"""
return str(label).rsplit(":", 1)[-1] == "needs_json"
def _is_needs_inventory_target(label):
"""Return whether ``label`` directly names a Needs inventory target.
These inventory targets are still accepted through ``external_needs``.
Identifying them separately lets ``docs()`` report the old ``data`` route
without reporting ordinary files and runtime dependencies.
"""
return str(label).rsplit(":", 1)[-1] in ("needs_json", "needs_json_file")
def _declare_docs_bundle(
name,
source_dir = None,
srcs = [],
data = [],
entry_doc = "index",
bundles = [],
code_targets = [],
primary_need_id = None,
root_docs_config = None,
is_root_bundle = False,
visibility = None,
**kwargs):
"""Declare the shared bundle target implementation.
This helper performs the bundle declaration used by both public entry
points. It returns the source and sourcelink inputs used by the standalone
Needs export created for source-bearing bundles.
Args:
name: target name.
source_dir: optional directory holding this bundle's own doc sources. It is
globbed like `docs()` (same file kinds) and the contents are stored after
stripping the `source_dir` prefix. Leave it unset for a pure aggregator.
For a source-bearing bundle this is a package-relative directory name;
`"."` means the package root. If `srcs` is supplied instead, those
explicit files determine the Sphinx action's source root. `None` means
that the bundle has no directory-glob source root.
srcs: Explicit documentation source files, including generated files.
Use this for a source-less bundle whose documentation is produced by a
build action. All files must share one parent directory so they can be
mounted as one bundle entry.
data: Files owned by this bundle that are not discovered as documentation
sources. Use this for runtime/support files that belong at the bundle's
eventual mount location. ``docs(data = [...])`` is the corresponding
shorthand for supporting files in the project's root bundle. Both
forms make their files available to a build; only bundle data travels
with a mounted bundle.
entry_doc: bundle-relative docname attached when this bundle is mounted.
Defaults to `index`.
bundles: nested bundles to compose, each a dict
{
"bundle": <docs_bundle label>,
"mount_at": <where it shall me mounted>,
"attach_to": <optional document to attach the bundle to; for a bundle root it defaults to the mount_at parent's index>
}.
code_targets: Implementation targets or filegroups to scan for source-code
links. Implementation target source files and their dependencies
are collected recursively within this bundle's repository and
BUILD package, including subpackages; filegroups expand to
their files.
primary_need_id: Sphinx-Needs ID of the primary Need representing this
bundle. Other Needs may remain in the bundle; only this
Need receives bundle-level target metadata.
root_docs_config: Internal provider target for the root bundle's
structured ``docs()`` configuration.
is_root_bundle: Whether this is the project root bundle. Root bundles
retain the configured canonical project URL.
Public callers provide the corresponding ``root_docs``
label instead.
visibility: Target visibility.
**kwargs: Additional attributes forwarded to the underlying rule.
"""
if source_dir != None and srcs:
fail(
("docs_bundle(%s): srcs cannot be combined with source_dir; " +
"put generated sources in a dedicated bundle") % name,
)
if "required_in_id" in kwargs:
fail(
("docs_bundle(%s): required_in_id is inherited from root_docs " +
"and cannot be set on the child bundle") % name,
)
# Keep directory-discovered sources separate from explicit Bazel targets so
# each kind can retain its own runtime path and staging behavior.
source_dir_globbed = glob_doc_sources(source_dir) if source_dir != None else []
# The private file set is the single source of implementation artifacts.
# Sourcelinks scan these same bounded files; consumers can inspect the
# internal target without adding a field to the public bundle manifest.
code_files = collect_bundle_code_files(
name = _bundle_internal_target(name, "files"),
code_targets = code_targets,
)
sourcelinks_json = None
if code_targets:
sourcelinks_json = generate_code_target_sourcelinks(
name = _bundle_internal_target(name, "sourcelinks_json"),
code_files = code_files,
)
# ``needs_json`` is an inventory consumed by score_metamodel, not content
# owned by this bundle. Do not propagate its TreeArtifact through
# DocsBundleInfo: a later bundle mount would treat that directory as a
# regular data file. Filter only this special target here; ordinary
# supporting files retain the usual behavior.
bundle_data = [
data_file
for data_file in data
if not _is_needs_json_target(data_file)
]
# The helper validates child declarations and creates the composed target
# that consumers mount or render. Keep this target's provider complete so
# documentation builds retain all nested bundle inputs and placements.
create_bundle(
name = name,
source_dir_globbed = source_dir_globbed,
source_targets = srcs,
sourcelinks_json = sourcelinks_json,
source_dir = source_dir,
entry_doc = entry_doc,
bundles = bundles,
data = bundle_data,
code_targets = code_targets,
code_files = code_files,
primary_need_id = primary_need_id,
root_docs_config = root_docs_config,
is_root_bundle = is_root_bundle,
visibility = visibility,
**kwargs
)
# File identities are intrinsic bundle metadata, independent of whether a
# coverage report is available. Export the composed view under the bundle's
# own internal name so docs() can consume it without a separate user target.
code_file_lists = declare_code_file_lists(
name = _bundle_internal_target(name, "code_file_lists"),
bundle = ":" + name,
)
# The public provider above represents the complete documentation tree:
# it includes nested bundles so consumers can mount and render them.
# Needs ownership has a different boundary. Create a sibling provider
# from only this target's direct sources, so its Needs export will neither
# absorb descendants as locally owned Needs nor acquire dependencies on
# those descendants. Consumers can therefore use this bundle's own
# inventory without inheriting its full rendered composition.
source_bundle_name = _bundle_internal_target(name, "source_bundle")
source_bundle = create_bundle(
name = source_bundle_name,
# The synthetic target name only separates this provider's dependency
# graph from the rendered bundle's graph. Sphinx writes its project
# name into needs.json, so keep that public metadata tied to the
# owning docs_bundle target rather than exposing the internal name.
project_name = name,
bundles = [],
source_dir_globbed = source_dir_globbed,
source_targets = srcs,
sourcelinks_json = sourcelinks_json,
source_dir = source_dir,
entry_doc = entry_doc,
primary_need_id = primary_need_id,
data = bundle_data,
code_targets = code_targets,
code_files = code_files,
root_docs_config = root_docs_config,
is_root_bundle = is_root_bundle,
visibility = visibility,
tags = ["manual"],
)
# The Sphinx action also needs the source-to-bundle manifest used to
# resolve ownership for each document. Build it from the same source-only
# provider as the local inventory; using the public manifest here would
# make mounted descendants appear to belong to this bundle's own export.
local_manifest = create_composition_manifest(
name = _bundle_internal_target(name, "local_manifest"),
bundle = source_bundle,
own_only = True,
visibility = visibility,
)
return struct(
source_dir_globbed = source_dir_globbed,
sourcelinks_json = sourcelinks_json,
source_bundle = source_bundle,
local_manifest = local_manifest,
code_file_lists = code_file_lists,
)
def _declare_bundle_local_needs(
name,
source_bundle,
source_dir_globbed,
srcs,
entry_doc,
sourcelinks_json,
mounts_manifest,
data = [],
visibility = None,
config = None,
deps = [],
resolved_external_needs_labels = []):
"""Create a standalone Needs export for a bundle's direct sources.
Standalone ``docs_bundle`` exports use Sphinx's configuration-free mode
with structured baseline overrides. The root bundle created by ``docs()``
may provide the project's own ``conf.py`` while that compatibility path is
being phased out.
The action consumes a sibling source-only provider instead of the public
composed provider, so this inventory contains Needs only from this
bundle's directly owned documents. A root ``docs()`` export may also load
already-resolved external inventories while checking links in its sources;
those imported Needs are not included in the output inventory.
"""
if not source_dir_globbed and not srcs:
return
# Build this inventory from the direct source provider. The public
# bundle may contain nested children; those descendants keep their own
# Needs export instead of being treated as locally owned here.
sphinx_build_deps = _sphinx_runtime_deps(deps)
needs_local = _bundle_internal_target(name, "needs_local")
needs_local_builder = _bundle_internal_target(name, "needs_local_builder")
# The source provider and local manifest describe the same owner-only
# document set. Pass both to Sphinx so mounted descendants are neither
# loaded as local sources nor assigned to this bundle's Needs export.
_needs_sphinx_docs(
name = needs_local_builder,
bundle = source_bundle,
config = config,
sphinx_build_deps = sphinx_build_deps,
# External inventories are action inputs so Sphinx can resolve links
# while building this local export. The source-only bundle still
# determines which Needs the builder writes to its own output.
sphinx_build_data = data + resolved_external_needs_labels,
master_doc = entry_doc,
external_needs_labels = json.encode(resolved_external_needs_labels),
score_bundle_needs_export = "1",
score_sourcelinks_json = sourcelinks_json,
score_source_code_linker_plain_links = "1",
mounts_manifest = mounts_manifest,
# Sphinx writes a directory containing several builder outputs. Keep
# that implementation output private; external consumers need only
# the extracted needs.json file below.
visibility = ["//visibility:private"],
)
# A Bazel directory artifact cannot also expose one of its children as a
# separate output of the same action. Keep Sphinx's directory output as an
# intermediate and publish just needs.json through the stable
# needs_local target. External-Needs loading then depends on a file artifact
# instead of reconstructing Sphinx's private output layout.
native.genrule(
name = needs_local,
srcs = [":" + needs_local_builder],
outs = [needs_local + "/needs.json"],
cmd = "cp $(execpath :" + needs_local_builder + ")/needs.json $@",
visibility = visibility,
tags = ["manual"],
)
def docs_bundle(
name,
source_dir = None,
srcs = [],
data = [],
entry_doc = "index",
bundles = [],
code_targets = [],
primary_need_id = None,
root_docs = None,
visibility = None,
**kwargs):
"""Declare a reusable documentation bundle.
The declaration itself is delegated to the shared helper. Keeping this
public entry point separate gives bundle-specific consumer targets a
distinct home while allowing ``docs()`` to use the shared declaration for
the project root. ``root_docs`` associates the bundle with the root
documentation project's structured configuration, including
``required_in_id``, without making it depend on the root bundle's composed
documentation sources.
"""
bundle = _declare_docs_bundle(
name = name,
source_dir = source_dir,
srcs = srcs,
data = data,
entry_doc = entry_doc,
bundles = bundles,
code_targets = code_targets,
primary_need_id = primary_need_id,
root_docs_config = _root_docs_config_label(root_docs) if root_docs != None else None,
visibility = visibility,
**kwargs
)
# The public bundle target above is composable and may contain nested
# bundles. The local Needs inventory must instead use its source-only
# sibling, so exported Needs reflect this bundle's ownership boundary.
_declare_bundle_local_needs(
name = name,
source_bundle = bundle.source_bundle,
source_dir_globbed = bundle.source_dir_globbed,
srcs = srcs,
entry_doc = entry_doc,
sourcelinks_json = bundle.sourcelinks_json,
mounts_manifest = bundle.local_manifest,
data = data,
visibility = visibility,
)
def _missing_requirements(deps):
"""Add Python hub dependencies if they are missing."""
found = []
missing = []
def _target_to_packagename(target):
return str(target).split("/")[-1].split(":")[0]
all_packages = [_target_to_packagename(pkg) for pkg in all_requirements]
def _find(pkg):
for dep in deps:
dep_pkg = _target_to_packagename(dep)
if dep_pkg == pkg:
return True
return False
for pkg in all_packages:
if _find(pkg):
found.append(pkg)
else:
missing.append(pkg)
if len(missing) == len(all_requirements):
#print("All docs-as-code dependencies are missing, adding all of them.")
return all_requirements
if len(missing) == 0:
#print("All docs-as-code dependencies are already included, no need to add any.")
return []
if len(found) > 0:
msg = "Some docs-as-code dependencies are in deps: " + ", ".join(found) + \
"\n ... but others are missing: " + ", ".join(missing) + \
"\nInconsistent deps for docs(): either include all dependencies or none of them."
fail(msg)
fail("This case should be unreachable?!")
def _sphinx_deps(deps):
"""Return the dependency set supplied by the documentation caller."""
return deps + _missing_requirements(deps)
def _sphinx_runtime_deps(deps):
"""Add the shared Sphinx dependencies and the CLI's summary library."""
result = _sphinx_deps(deps)
for fixed_dep in [
Label("//src:plantuml_for_python"),
Label("//src/extensions/score_sphinx_bundle:score_sphinx_bundle"),
Label("//src/docs_cli:lcov_summary_lib"),
]:
if fixed_dep not in result:
result.append(fixed_dep)
return result
def _declare_docs_binary(name, data, deps, env, action):
"""Declare one of the interactive documentation command targets."""
docs_cli_src = Label("//src/docs_cli:cli.py")
command_env = env | {"ACTION": action}
py_binary(
name = name,
srcs = [docs_cli_src],
data = data,
deps = deps,
env = command_env,
tags = ["manual"],
)
def docs(
source_dir = "docs",
project = None,
project_url = None,
data = [],
deps = [],
external_needs = [],
code_targets = [],
primary_need_id = None,
test_sources = [],
known_good = None,
metamodel = None,
bundles = []):
"""Creates all targets related to documentation.
By using this function, you'll get any and all updates for documentation targets in one place.
Args:
source_dir: The source directory containing documentation files. Defaults to "docs".
project: Optional project name. Required with ``project_url`` when no
root ``conf.py`` exists; passed to Sphinx as a structured
configuration override.
project_url: Optional project URL. Required with ``project`` when no
root ``conf.py`` exists; passed to Sphinx as a structured
configuration override. The calling Bazel package path is
appended relative to the workspace.
data: Additional files owned by this project's root ``:docs_bundle``.
This is shorthand for declaring the files in that root bundle; mounted
child content belongs in the child ``docs_bundle(data = [...])``.
For generated documentation in a mounted child, use that bundle's
explicit ``srcs`` instead; ``data`` remains for supporting/runtime
files. Passing a Needs inventory target through ``data`` is deprecated
and prints an informational message; put it in ``external_needs``.
deps: Additional dependencies for the documentation build.
external_needs: Labels of public ``docs()`` or ``docs_bundle`` targets
whose Needs can be referenced by this project. The
``needs_json`` label remains accepted as a deprecated
form, without a warning for now. ``needs_json_file`` is
also supported for directly naming an inventory file.
code_targets: Implementation targets or filegroups to scan for source code
links. Dependencies are scanned within this bundle's repository
and BUILD package, including subpackages; filegroups expand
to their files.
primary_need_id: Sphinx-Needs ID of the primary Need representing this
documentation project and its root bundle. Other Needs
remain unchanged.
test_sources: Optional list of repo-relative directory paths which will be used to filter testcases for documentation generation.
When empty (default), all testcases found in `bazel-testlogs` will be used.
known_good: Optional label to a "known good" JSON file for source links.
metamodel: Optional label to a metamodel.yaml file. When set, the extension loads this
file instead of the default metamodel shipped with score_metamodel.
bundles: List of placement dicts describing documentation bundles to overlay
into this documentation's source tree. Each entry is a dict
{
"bundle": <docs_bundle label>,
"mount_at": <where it shall me mounted>,
"attach_to": <optional, file where the bundle shall be attached, defaults to the parent section's index>,
}.
Note: a bundle label may also point at another module's auto-exposed
bundle, e.g. "@score_process_description//:docs_bundle".
``docs(data = [...])`` owns files in the root ``:docs_bundle``. A child
bundle uses the same ``data`` attribute and may omit ``source_dir`` when it
contains only supporting files. Use explicit ``srcs`` in a source-less
child bundle when its documentation is generated by a build action.
"""
# HINT: keep documentation sync docs/reference/bazel_macros.rst
# Report external Needs inventories in ``data`` while the original labels
# are still visible at the public macro boundary. After this point, ``data``
# and explicit ``external_needs`` labels are combined for the Needs builder,
# so their origin is no longer available.
if any([_is_needs_inventory_target(label) for label in data]):
print(
"INFO: ⚠️ DEPRECATED: Passing a Needs inventory through " +
"docs(data = [...]) is deprecated!!! Move it to " +
"external_needs = [...] instead.",
)
# Resolve public bundle names once, then pass the generated inventory labels
# to both the interactive build and the Needs export action. The deprecated
# ``needs_json`` form remains accepted through explicit ``external_needs``.
external_needs_labels = _resolve_external_needs_labels(external_needs)
config_file_path = join_path(source_dir, "conf.py")
config_is_missing = len(native.glob([config_file_path], allow_empty = True)) == 0
sphinx_config = None if config_is_missing else ":" + config_file_path
interactive_config_options = []
if config_is_missing:
if not project or not project_url:
fail("docs(): no " + config_file_path + " found; provide both project and project_url to docs().")
# Sphinx can run without a configuration file when ``-C`` is used.
# Keep the macro's structured values as command-line overrides instead
# of materializing a generated ``conf.py`` in the output tree.
interactive_config_options = sphinx_config_options(
project = project,
project_url = project_url,
required_in_id = _module_name_without_prefix(),
)
# Publish the root project's structured configuration independently from
# the root bundle. Keeping the scalar configuration separate from the
# composed bundle leaves the bundle graph free of configuration cycles.
bundle_config_metamodel = metamodel or Label(
"@score_docs_as_code//src/extensions/score_metamodel:metamodel_yaml",
)
config_visibility = _bundle_internal_target("docs", "config_visibility")
native.package_group(
name = config_visibility,
packages = ["//..."],
)
root_docs_config = declare_docs_config_target(
name = _bundle_internal_target("docs", "config"),
project = project or _module_name_without_prefix(),
project_url = project_url or "",
required_in_id = _module_name_without_prefix(),
metamodel = bundle_config_metamodel,
visibility = [":" + config_visibility],
)
# Convention in this macro: an optional Bazel label is named ``*_label``
# but represented as a 0/1 list. This lets it be appended directly to
# list-valued attributes such as ``data`` and ``tools``.
metamodel_label = [metamodel] if metamodel else []
deps = _sphinx_runtime_deps(deps)
known_good_label = [known_good] if known_good else []
# The public bundle carries both the complete source tree and the
# transitive source-code links of every nested bundle.
root_bundle = _declare_docs_bundle(
name = "docs_bundle",
source_dir = source_dir,
data = data,
entry_doc = "index",
bundles = bundles,
code_targets = code_targets,
primary_need_id = primary_need_id,
root_docs_config = root_docs_config,
is_root_bundle = True,
visibility = ["//visibility:public"],
tags = ["manual"],
)
# The runtime manifest must be generated from the actual root bundle. The
# root entry carries the project's direct code_targets and gives Python a
# complete composition snapshot alongside the nested mounts.
mounts_manifest = create_composition_manifest(
name = "_mounts_manifest",
bundle = ":docs_bundle",
)
# As with child docs_bundle targets, export Needs from the root's direct
# source provider. Nested bundles are rendered by docs_bundle but keep
# separate ownership and separate local Needs inventories.
_declare_bundle_local_needs(
# The public producer target is named ``docs``. Its local Needs export
# uses the same name so another project's ``external_needs = [":docs"]``
# resolves to the inventory this macro actually creates.
name = "docs",
source_bundle = root_bundle.source_bundle,
source_dir_globbed = root_bundle.source_dir_globbed,
srcs = [],
entry_doc = "index",
sourcelinks_json = root_bundle.sourcelinks_json,
mounts_manifest = root_bundle.local_manifest,
data = data,
visibility = ["//visibility:public"],
config = sphinx_config,
deps = deps,
resolved_external_needs_labels = external_needs_labels,
)
merge_bundle_sourcelinks(
name = "sourcelinks_json",
bundle = ":docs_bundle",
known_good = known_good,
)
external_docs_runfiles(
name = "_external_docs_runfiles",
bundle = ":docs_bundle",
visibility = ["//visibility:private"],
)
# ``bazel run`` reads local documentation from the workspace, so including
# the complete bundle in runfiles would duplicate those sources. External
# bundles do need runfiles, so keep only those sources.
docs_data = (
data + external_needs_labels + metamodel_label +
[":sourcelinks_json", ":_external_docs_runfiles"] +
[mounts_manifest]
)
docs_env = {
"SOURCE_DIRECTORY": source_dir,
"PACKAGE_DIR": native.package_name(),
"TEST_SOURCES": str(test_sources),
"DATA": str(data),
"EXTERNAL_NEEDS_FILES": str(external_needs_labels),
# `bazel run` starts from a runfiles tree, so this logical path is
# resolved by score_mounts through ``RUNFILES_DIR``.
"MOUNTS_MANIFEST": "$(rlocationpath :_mounts_manifest)" if mounts_manifest else "",
"SCORE_SOURCELINKS": "$(rlocationpath :sourcelinks_json)",
"SPHINX_CONFIG_OPTS": json.encode(interactive_config_options),
}
if metamodel:
# The interactive ``py_binary`` targets run from a runfiles tree.
# docs_cli resolves this logical path through ``RUNFILES_DIR``.
docs_env["SCORE_METAMODEL_YAML"] = "$(rlocationpath " + str(metamodel) + ")"
if known_good_label:
known_good_str = str(known_good_label[0])
docs_env["KNOWN_GOOD_JSON"] = "$(rlocationpath " + known_good_str + ")"
docs_data += known_good_label
# The CLI combines these declared file identities with the optional report
# in the workspace when bazel run starts. Coverage collection and summary
# generation therefore leave sandboxed Needs inventories independent of
# downloaded artifacts. Fast validation uses the base data/environment.
coverage_data = docs_data + [root_bundle.code_file_lists]
coverage_env = docs_env | {
"SCORE_CODE_FILE_LISTS": "$(rlocationpath " + root_bundle.code_file_lists + ")",
}
# Generated documentation artifacts may live below ``docs/``. A
# py_binary named ``docs`` would own the conflicting Bazel output path
# ``docs``; expose this binary via the alias below instead.
_declare_docs_binary(
name = "_score_docs_cli",
data = coverage_data,
deps = deps,
env = coverage_env,
action = "incremental",
)
native.alias(
name = "docs",
actual = ":_score_docs_cli",
tags = ["manual"],
)
_declare_docs_binary(
name = "docs_link_check",
data = docs_data,
deps = deps,
env = docs_env,
action = "linkcheck",
)
_declare_docs_binary(
name = "docs_check",
data = docs_data,
deps = deps,
env = docs_env,
action = "check",
)
_declare_docs_binary(
name = "live_preview",
data = coverage_data,
deps = deps,
env = coverage_env,
action = "live_preview",
)
py_venv(
name = "ide_support",
tags = ["manual"],
venv_name = ".venv_docs",
# Tests import score_pytest (e.g. add_test_properties), so the IDE
# venv needs it for type checking and for running tests from the IDE.
deps = deps + [Label("@score_tools//score_pytest:attribute_plugin")],
data = data,
package_collisions = "warning",
)
_needs_sphinx_docs(
name = "needs_json",
bundle = ":docs_bundle",
config = sphinx_config,
sphinx_build_deps = deps,
sphinx_build_data = data + external_needs_labels + metamodel_label + [":docs_bundle"],
external_needs_labels = str(data + external_needs_labels),
score_sourcelinks_json = ":sourcelinks_json",
score_source_code_linker_plain_links = "1",
mounts_manifest = mounts_manifest,
score_metamodel_yaml = metamodel,
tools = external_needs_labels + [":docs_bundle"],
visibility = ["//visibility:public"],
)
native.genrule(
name = "metrics_json",
srcs = [":needs_json"],
outs = ["metrics.json"],
# Copy metrics.json out of the directory produced by :needs_json.
# $(execpath ...) expands to that input directory's path for this build
# action, and $@ is the path of the metrics.json output created here.
cmd = "cp $(execpath :needs_json)/metrics.json $@",
visibility = ["//visibility:public"],
tags = ["manual"],
)
native.genrule(
# In contrast to the "needs_json" target represents *only* the needs.json file,
# not the whole needs build output.
name = "needs_json_file",
srcs = [":needs_json"],
outs = ["needs.json"],
# Copy needs.json out of the directory produced by :needs_json.
# $(execpath ...) gives this build action the input directory's path;
# $@ is the path of the needs.json output created here.
cmd = "cp $(execpath :needs_json)/needs.json $@",
visibility = ["//visibility:public"],
tags = ["manual"],
)
native.alias(
name = "traceability_gate",
actual = Label("//scripts_bazel:traceability_gate"),
tags = ["manual"],
)