blob: 7229c75374dd4372c958206aba03d0bfad9983b7 [file] [log] [blame]
#!/usr/bin/env python2.7
# Copyright 2016 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.
import argparse
import os
import paths
import shutil
import subprocess
import sys
import tempfile
sys.path += [os.path.join(paths.FUCHSIA_ROOT, 'third_party', 'pyyaml', 'lib')]
import yaml
LICENSE_FILES = ['LICENSE', 'LICENSE.txt']
IGNORED_EXTENSIONS = ['css', 'html', 'js', 'log', 'old', 'out',
'packages', 'snapshot', 'zip']
LOCAL_PACKAGES = {
'build_integration': '//third_party/dart/pkg/build_integration',
'flutter': '//third_party/dart-pkg/git/flutter/packages/flutter',
'flutter_test': '//third_party/dart-pkg/git/flutter/packages/flutter_test',
'flutter_web_plugins': '//third_party/dart-pkg/git/flutter/packages/flutter_web_plugins',
'func': '//third_party/dart/third_party/pkg/func',
'intl': '//third_party/dart/third_party/pkg/intl',
'sky_engine': '//prebuilt/third_party/sky_engine',
'testing': '//third_party/dart/pkg/testing',
'typed_mock': '//third_party/dart/pkg/typed_mock',
}
FORBIDDEN_PACKAGES = ['mojo', 'mojo_services']
# This is to account for https://github.com/flutter/devtools/issues/1148
PACKAGES_WITH_NO_LIB = ['devtools']
def parse_packages_file(dot_packages_path):
""" parse the list of packages and paths in .packages file """
packages = []
with open(dot_packages_path) as dot_packages:
# The packages specification says both '\r' and '\n' are valid line
# delimiters, which matches Python's 'universal newline' concept.
# Packages specification: https://github.com/dart-lang/dart_enhancement_proposals/blob/master/Accepted/0005%20-%20Package%20Specification/DEP-pkgspec.md
contents = dot_packages.read()
for line in unicode.splitlines(unicode(contents)):
if line.startswith('#'):
continue
delim = line.find(':')
if delim == -1:
continue
name = line[:delim]
path = line[delim + 1:-1]
packages.append((name, path))
return packages
def get_deps(package_name, parsed_yaml, dep_type):
if dep_type in parsed_yaml and parsed_yaml[dep_type]:
deps = parsed_yaml[dep_type]
# This is to avoid circular dependencies. See fxb/40784.
if package_name == 'built_value' and 'built_value_generator' in deps:
del deps['built_value_generator']
return deps
else:
return {}
def parse_full_dependencies(yaml_path):
""" parse the content of a pubspec.yaml """
with open(yaml_path) as yaml_file:
parsed = yaml.safe_load(yaml_file)
if not parsed:
raise Exception('Could not parse yaml file: %s' % yaml_file)
package_name = parsed['name']
deps = get_deps(package_name, parsed, 'dependencies')
dev_deps = get_deps(package_name, parsed, 'dev_dependencies')
dep_overrides = get_deps(package_name, parsed, 'dependency_overrides')
return (package_name, deps, dev_deps, dep_overrides)
def parse_dependencies(yaml_path):
""" parse the dependency map out of a pubspec.yaml """
_, deps, _, _ = parse_full_dependencies(yaml_path)
return deps
def write_build_file(build_gn_path, package_name, name_with_version, deps):
""" writes BUILD.gn file for Dart package with dependencies """
with open(build_gn_path, 'w') as build_gn:
build_gn.write('''# This file is generated by importer.py for %s
import("//build/dart/dart_library.gni")
dart_library("%s") {
package_name = "%s"
# This parameter is left empty as we don't care about analysis or exporting
# these sources outside of the tree.
sources = []
disable_analysis = true
deps = [
''' % (name_with_version, package_name, package_name))
for dep in deps:
if dep in LOCAL_PACKAGES:
build_gn.write(' "%s",\n' % LOCAL_PACKAGES[dep])
else:
build_gn.write(' "//third_party/dart-pkg/pub/%s",\n' % dep)
build_gn.write(''' ]
}
''')
def read_package_versions(base):
'''Scans the packages in a given directory.'''
result = {}
for (root, dirs, files) in os.walk(base):
for dir in dirs:
spec = os.path.join(root, dir, 'pubspec.yaml')
if not os.path.exists(spec):
continue
with open(spec, 'r') as spec_file:
data = yaml.safe_load(spec_file)
result[data['name']] = data['version']
break
return result
def generate_package_diff(old_packages, new_packages, changelog):
'''Writes a changelog file with package version changes.'''
old = set(old_packages.iteritems())
new = set(new_packages.iteritems())
changed_keys = set([k for (k, _) in (old | new) - (old & new)])
if not changed_keys:
return
max_key_width = max(map(lambda k: len(k), changed_keys))
with open(changelog, 'w') as changelog_file:
for key in sorted(changed_keys):
old = old_packages.get(key, '<none>')
new = new_packages.get(key, '<none>')
changelog_file.write('%s %s --> %s\n' % (key.rjust(max_key_width),
old.rjust(10),
new.ljust(10)))
def valid_package_path(package_name, source_dir):
if package_name in PACKAGES_WITH_NO_LIB:
parent_dir = os.path.normpath(os.path.join(source_dir, os.pardir))
return os.path.exists(parent_dir)
else:
return os.path.exists(source_dir)
def main():
parser = argparse.ArgumentParser('Import dart packages from pub')
parser.add_argument('--pub', required=True,
help='Path to the pub executable')
parser.add_argument('--pubspecs', nargs='+',
help='Paths to packages containing pubspec.yaml files')
parser.add_argument('--projects', nargs='+',
help='Paths to projects containing dependency files')
parser.add_argument('--output', required=True,
help='Path to the output directory')
parser.add_argument('--changelog',
help='Path to the changelog file to write',
default=None)
parser.add_argument('--debug',
help='Turns on debugging mode',
action='store_true')
args = parser.parse_args()
def debug_print(message):
if args.debug:
print(message)
tempdir = tempfile.mkdtemp()
debug_print('Working directory: ' + tempdir)
try:
importer_dir = os.path.join(tempdir, 'importer')
os.mkdir(importer_dir)
# Read the requested dependencies from the canonical packages.
packages = {}
additional_deps = {}
debug_print('------------------------')
debug_print('Development dependencies')
debug_print('------------------------')
for path in args.pubspecs:
yaml_file = os.path.join(path, 'pubspec.yaml')
package_name, _, dev_deps, _ = parse_full_dependencies(yaml_file)
packages[package_name] = path
additional_deps.update(dev_deps)
debug_print('# From ' + yaml_file)
for pair in sorted(dev_deps.items()):
debug_print(' - %s: %s' % pair)
# Generate a manifest containing all the dependencies we care about.
manifest = {
'name': 'importer',
}
dependencies = {}
for package_name in packages.keys():
dependencies[package_name] = 'any'
for dep, version in additional_deps.iteritems():
if dep in packages:
continue
dependencies[dep] = version
debug_print('-------------------------')
debug_print('Manually-set dependencies')
debug_print('-------------------------')
for project in args.projects:
yaml_file = os.path.join(project, 'dart_dependencies.yaml')
project_deps = parse_dependencies(yaml_file)
debug_print('# From ' + yaml_file)
for dep, version in sorted(project_deps.iteritems()):
dependencies[dep] = version
debug_print(' - %s: %s' % (dep, version))
manifest['dependencies'] = dependencies
overrides = {}
for package_name, path in packages.iteritems():
overrides[package_name] = {
'path': path,
}
manifest['dependency_overrides'] = overrides
with open(os.path.join(importer_dir, 'pubspec.yaml'), 'w') as pubspec:
yaml.safe_dump(manifest, pubspec)
old_packages = read_package_versions(args.output)
# Use pub to load the dependencies into a local cache.
pub_cache_dir = os.path.join(tempdir, 'pub_cache')
os.mkdir(pub_cache_dir)
env = os.environ
env['PUB_CACHE'] = pub_cache_dir
pub_get = [args.pub, 'get']
if args.debug:
pub_get.append('-v')
subprocess.check_call(pub_get, cwd=importer_dir, env=env)
# Walk the cache and copy the packages we are interested in.
if os.path.exists(args.output):
for (root, dirs, files) in os.walk(args.output):
for dir in dirs:
if dir != '.git':
shutil.rmtree(os.path.join(root, dir))
# Only process the root of the output tree.
break
pub_packages = parse_packages_file(os.path.join(importer_dir, '.packages'))
for package in pub_packages:
if package[0] in packages:
# Skip canonical packages.
continue
if not package[1].startswith('file://'):
continue
source_dir = package[1][len('file://'):]
package_name = package[0]
if not valid_package_path(package_name, source_dir):
continue
if source_dir.find('pub.dartlang.org') == -1:
print 'Package %s not from dartlang (%s), ignoring' % (package[0], source_dir)
continue
# Don't import packages that live canonically in the tree.
if package_name in LOCAL_PACKAGES:
continue
if package_name in FORBIDDEN_PACKAGES:
print 'Warning: dependency on forbidden package %s' % package_name
continue
# We expect the .packages file to point to a directory called 'lib'
# inside the overall package, which will contain the LICENSE file
# and other potentially useful directories like 'bin'.
source_base_dir = os.path.dirname(os.path.abspath(source_dir))
name_with_version = os.path.basename(source_base_dir)
has_license = any(os.path.exists(os.path.join(source_base_dir, file_name))
for file_name in LICENSE_FILES)
if not has_license:
print 'Could not find license file for %s, skipping' % package_name
continue
pubspec_path = os.path.join(source_base_dir, 'pubspec.yaml')
deps = []
if os.path.exists(pubspec_path):
deps = parse_dependencies(pubspec_path)
dest_dir = os.path.join(args.output, package_name)
shutil.copytree(source_base_dir, dest_dir,
ignore=shutil.ignore_patterns(
*('*.' + extension for extension in IGNORED_EXTENSIONS)))
# We don't need the 'test' directory of packages we import as that
# directory exists to test that package and some of our packages
# have very heavy test directories, so nuke those.
test_path = os.path.join(dest_dir, 'test')
if os.path.exists(test_path):
shutil.rmtree(test_path)
write_build_file(os.path.join(dest_dir, 'BUILD.gn'), package_name,
name_with_version, deps)
if args.changelog:
new_packages = read_package_versions(args.output)
generate_package_diff(old_packages, new_packages, args.changelog)
finally:
if not args.debug:
shutil.rmtree(tempdir)
if __name__ == '__main__':
sys.exit(main())