| #!/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 |
| |
| # 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()) |