blob: ba179adac99f36708e4de764d638d2820be90b41 [file] [log] [blame]
#!/usr/bin/env python3.8
# Copyright 2021 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generate Dart reference docs for one or more Fuchsia packages.
This script uses 'dart doc', which documents a single package. If called with more
than one package, an intermediary package is generated in order to document all
the given packages at once; in that case, the --gen-dir argument is required. If
this behavior is not desired, do not pass more than one package argument.
"""
import argparse
import tempfile
import os
import shutil
import subprocess
import sys
import yaml
import generate_dart_toc
_PUBSPEC_CONTENT = """name: Fuchsia
homepage: https://fuchsia.dev/reference/dart
description: API documentation for fuchsia
environment:
sdk: '>=2.10.0 <3.0.0'
dependencies:
"""
def is_dart_package_dir(package_dir):
"""Returns whether or not a directory resembles a Dart package dir."""
if not os.path.isdir(package_dir):
print('%s is not a directory' % (package_dir))
return False
if not os.path.isdir(os.path.join(package_dir, 'lib')):
print('%s is missing a lib subdirectory' % (package_dir))
return False
pubspec_file = os.path.join(package_dir, 'pubspec.yaml')
if not os.path.exists(pubspec_file):
print('%s is missing a pubspec.yaml' % (package_dir))
return False
with open(pubspec_file) as f:
pubspec = yaml.load(f, Loader=yaml.SafeLoader)
if not pubspec or pubspec['name'] != os.path.basename(package_dir):
# Implies an invalid pubspec and the package will be ignored in dartdocs
return False
return True
def collect_top_level_files(package_dir):
"""Return a list of dart filenames under the package's lib directory."""
return sorted(
os.path.basename(p)
for p in os.listdir(os.path.join(package_dir, 'lib'))
if os.path.basename(p).endswith('.dart'))
def compose_pubspec_content(package_dict):
"""Compose suitable contents for a pubspec file.
The pubspec will have dependencies on the packages in package_dict.
Args:
package_dict: Dictionary mapping package name to path.
Returns:
String with the pubspec content.
"""
pubspec_content = _PUBSPEC_CONTENT
for pkg in package_dict:
pubspec_content += ' %s:\n path: %s/\n' % (pkg, package_dict[pkg])
return pubspec_content
def compose_imports_content(imports_dict):
"""Compose suitable contents for an imports file.
The contents will include import statements for all items in imports_dict.
Args:
imports_dict: Dictionary mapping package name to a list of file names
belonging to that package.
Returns:
String with the imports content.
"""
lines = []
for pkg in imports_dict:
for i in imports_dict[pkg]:
lines.append("import 'package:%s/%s';\n" % (pkg, i))
lines.sort()
return 'library Fuchsia;\n' + ''.join(lines)
def fabricate_package(gen_dir, pubspec_content, imports_content):
"""Write given pubspec and imports content in the given directory.
Args:
packages: A list of package directories to use as dependencies.
gen_dir: The directory for generated files.
"""
if not os.path.exists(os.path.join(gen_dir, 'lib')):
os.makedirs(os.path.join(gen_dir, 'lib'))
# Fabricate pubspec.yaml.
pubspec_file = os.path.join(gen_dir, 'pubspec.yaml')
if os.path.exists(pubspec_file):
os.remove(pubspec_file)
with open(pubspec_file, 'w') as f:
f.write(pubspec_content)
# Fabricate a dart file with all the imports collected.
lib_file = os.path.join(gen_dir, 'lib', 'lib.dart')
if os.path.exists(lib_file):
os.remove(lib_file)
with open(lib_file, 'w') as f:
f.write(imports_content)
def walk_rmtree(directory):
"""Manually remove all subdirectories and files of a directory
via os.walk instead of using
shutil.rmtree, to avoid registering spurious reads on stale
subdirectories. See https://fxbug.dev/74084.
Args:
directory: path to directory which should have tree removed.
"""
for root, dirs, files in os.walk(directory, topdown=False):
for file in files:
os.unlink(os.path.join(root, file))
for dir in dirs:
full_path = os.path.join(root, dir)
if os.path.islink(full_path):
os.unlink(full_path)
else:
os.rmdir(full_path)
os.rmdir(directory)
def generate_docs(
package_dir,
out_dir,
dart_prebuilt_dir,
run_toc=False,
delete_artifact_files=False,
zipped_result=False):
"""Generate dart reference docs.
Args:
package_dir: The directory of the package to document.
out_dir: The output directory for documentation.
dart_prebuilt_dir: The directory with dart executables (pub).
run_toc: If true, will generate a toc.yaml file to represent dart docs.
delete_artifact_files: If true, will delete unused artifacts in output.
zipped_result: If true, will zip dart docs and delete orig. generated docs.
Returns:
0 if documentation was generated successfully, non-zero otherwise.
"""
# Run pub over this package to fetch deps.
# Use temp dir for pub_cache to ensure hermetic build.
# TODO (https://fxbug.dev/84343) to have all deps locally.
with tempfile.TemporaryDirectory() as tmpdirname:
process = subprocess.run(
[os.path.join(dart_prebuilt_dir, 'dart'), 'pub', 'get'],
cwd=package_dir,
# Pub requires HOME env variable which is wiped from BUILD.
env=dict(os.environ, PUB_CACHE=tmpdirname, HOME=tmpdirname),
capture_output=True,
universal_newlines=True)
if process.returncode:
print(process.stderr)
return 1
# Clear the docdir first.
docs_dir = os.path.join(out_dir, "dartdoc")
pkg_to_docs_path = os.path.join(package_dir, docs_dir)
if os.path.exists(pkg_to_docs_path):
walk_rmtree(pkg_to_docs_path)
# Run dart doc.
# TODO(fxb/93159): Re-enable `dart doc` after it is known
# how to incorporate the following dropped flags. Once done,
# we can get rid of this `dart pub global activate dartdoc`
# workaround.
activate_dartdoc_process = subprocess.run(
[
os.path.join(dart_prebuilt_dir, 'dart'), 'pub', 'global',
'activate', 'dartdoc'
],
cwd=package_dir,
env=dict(os.environ, PUB_CACHE=tmpdirname, HOME=tmpdirname),
capture_output=True,
universal_newlines=True)
if activate_dartdoc_process.returncode:
print(activate_dartdoc_process.stderr)
return 1
excluded_packages = ['Dart', 'logging']
process = subprocess.run(
[
os.path.join(dart_prebuilt_dir, 'dart'),
'pub',
'global',
'run',
'dartdoc',
'--auto-include-dependencies',
'--exclude-packages',
','.join(excluded_packages),
'--output',
docs_dir,
'--format',
'md',
],
cwd=package_dir,
env=dict(os.environ, PUB_CACHE=tmpdirname),
capture_output=True,
universal_newlines=True)
if process.returncode:
print(process.stderr)
return 1
# Clear the dartdoc artifacts if they exist.
dartdoc_artifacts = os.path.join(package_dir, '.dart')
if os.path.exists(dartdoc_artifacts):
walk_rmtree(dartdoc_artifacts)
# Run toc.yaml generation
if run_toc:
generate_dart_toc.no_args_main(
index_file=os.path.join(pkg_to_docs_path, 'index.json'),
outfile=os.path.join(pkg_to_docs_path, '_toc.yaml'))
# Delete useless artifact files
if delete_artifact_files:
for file in ['__404error.md', 'categories.json', 'index.json']:
os.remove(os.path.join(pkg_to_docs_path, file))
# Zip generated docs and delete original docs to make build hermetic.
if zipped_result:
pkg_to_out = os.path.join(package_dir, out_dir)
prefix, final_component = os.path.split(pkg_to_out)
# Action tracer is unable to trace dartdoc operations
# so an exemption at __untraced_dartdoc_output__ is created.
# However we do not want to write our final output there.
# See https://fxbug.dev/102217.
shutil.make_archive(
os.path.join(prefix if final_component ==
"__untraced_dartdoc_output__" else pkg_to_out,
'dartdoc_out'),
'zip',
root_dir=pkg_to_out,
base_dir='dartdoc')
walk_rmtree(pkg_to_docs_path)
return 0
def main():
parser = argparse.ArgumentParser(
description=__doc__, # Prepend help doc with this file's docstring.
formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
'-d',
'--delete-artifact-files',
action='store_true',
help=
'If set will delete __404error.md, categories.json, index.json in the out directory'
)
parser.add_argument(
'-z',
'--zipped-result',
action='store_true',
help=(
'If set will zip output documentation and delete md files.'
'This is to make the doc generation process hermetic'))
parser.add_argument(
'-r',
'--run-toc',
action='store_true',
help='If set will run generate_toc script on the generated dart docs.')
parser.add_argument(
'-g',
'--gen-dir',
type=str,
required=False,
help='Location where intermediate files can be generated (should be '
'different from the output directory). This is required if more '
'than one package argument is given.')
parser.add_argument(
'-o',
'--out-dir',
type=str,
required=True,
help='Output location where generated docs should go')
parser.add_argument(
'--dep-file', type=argparse.FileType('w'), required=True)
parser.add_argument(
'-p',
'--prebuilts-dir',
type=str,
required=False,
default='',
help="Location of dart prebuilts, usually a Dart SDK's bin directory")
parser.add_argument(
'packages', type=str, nargs='+', help='Paths of packages to document')
args = parser.parse_args()
if len(args.packages) == 1:
package_dir = args.packages[0]
else:
# `dart doc` runs over a single package only. Fabricate a package that
# depends on all the other packages and document that one.
if not args.gen_dir:
print('ERROR: --gen-dir is required to document multiple packages.')
parser.print_help()
return 1
package_dir = args.gen_dir
packages_dict = {}
imports_dict = {}
input_dart_files = []
for pkg in args.packages:
if is_dart_package_dir(pkg):
package_basename = os.path.basename(pkg)
packages_dict[package_basename] = os.path.abspath(pkg)
imports_dict[package_basename] = collect_top_level_files(pkg)
for root, dirs, files in os.walk(os.path.join(pkg),
topdown=False):
for name in files:
if os.path.basename(name).endswith(
'.dart') or name == 'pubspec.yaml':
input_dart_files.append(os.path.join(root, name))
else:
input_dart_files.append(os.path.join(pkg, 'pubspec.yaml'))
pubspec_content = compose_pubspec_content(packages_dict)
imports_content = compose_imports_content(imports_dict)
fabricate_package(package_dir, pubspec_content, imports_content)
args.dep_file.write(
'{}: {}\n'.format(
os.path.join(package_dir, args.out_dir, 'dartdoc_out.zip'),
' '.join(input_dart_files)))
return generate_docs(
package_dir, args.out_dir, args.prebuilts_dir, args.run_toc,
args.delete_artifact_files, args.zipped_result)
if __name__ == '__main__':
sys.exit(main())