Skip to content
This repository has been archived by the owner on Nov 10, 2023. It is now read-only.

Automate importing third-party dependencies. #2706

Open
wants to merge 1 commit into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions tools/import_deps/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
libs
buck-out
.buckd
BUCK
venv
third-party
36 changes: 36 additions & 0 deletions tools/import_deps/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
## Import Gradle Dependencies to BUCK

The tool is designed to help with migrating from gradle to buck.
It imports all dependencies defined in a build.gradle and generates BUCK files.

Steps:

1. Create a dependencies file:

```
./gradlew -q :app:dependencies --configuration debugRuntimeClasspath >> report_file.txt
./gradlew -q :app:dependencies --configuration debugRuntimeClasspath >> report_file.txt
./gradlew -q :app:dependencies --configuration debugAnnotationProcessorClasspath >> report_file.txt
```

The gradle build resolves versions. The tool will import the packages of the versions
preferred by gradle.

2. Run import dependencies script:

Activate python virtual environment:
source venv/bin/activate

```
./importdeps.py --gdeps report_file.txt --libs third-party
```

The tool will import dependencies and generate BUCK files.

3. Test generated BUCK file:

```
buck targets //third-party:
```

4. Use imported targets in your project.
13 changes: 13 additions & 0 deletions tools/import_deps/dependency/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) Meta Platforms, Inc. and its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
145 changes: 145 additions & 0 deletions tools/import_deps/dependency/buck.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Copyright (c) Meta Platforms, Inc. and its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import io
import logging
import os
import re
from pathlib import Path

from dependency.pom import MavenDependency
from dependency.repo import Repo, ArtifactFlavor


class Buck:
BUCK_FILE = "BUCK"

def __init__(self, repo: Repo) -> None:
super().__init__()
self.repo = repo

def create(self, buck_path: Path, dependency: MavenDependency, visited: set):
"""
Creates or updates a BUCK file for a single dependency.
:param buck_path: a path to BUCK file.
:param dependency: a dependency.
:param visited: a set of visited dependencies
:return:
"""
assert dependency is not None
if visited.__contains__(dependency):
return

mode = "a+"

with buck_path.open(mode) as f:
f.write(self.generate_prebuilt_jar_or_aar_rule(dependency))
f.write("\n")
f.write(self.android_library(dependency))

visited.add(dependency)

def get_all_dependencies(self, pom: MavenDependency, visited=set()) -> []:
if visited.__contains__(pom):
return visited
visited.add(pom)
for d in pom.dependsOn:
self.get_all_dependencies(d, visited)
return visited

def get_path(self) -> Path:
p = Path(os.path.join(self.repo.get_root(), Buck.BUCK_FILE))
return p

def get_buck_path_for_dependency(self, dependency: MavenDependency) -> Path:
artifact_base_dir = Path(self.repo.root)
for d in dependency.group_id.split("."):
artifact_base_dir = os.path.join(artifact_base_dir, d)
p = Path(os.path.join(artifact_base_dir, Buck.BUCK_FILE))
return p

def fix_path(self, file: Path):
f = str(file)
f = f.replace("..", "/")
return f

def generate_prebuilt_jar_or_aar_rule(self, pom: MavenDependency) -> str:
"""
Generate android_prebuilt_aar or prebuilt_jar rules for an
artifact.
:param pom: a dependency.
:return:
"""
artifact = pom.get_artifact()
if artifact is None:
logging.debug(f"pom has no artifacts: {pom}")
return ""
# assert artifact is not None, f"pom has no artifacts: {pom}"
template = None
if re.match(r".*.aar$", artifact):
template = self.get_android_prebuilt_aar(pom)
elif re.match(r".*.jar$", artifact):
template = self.get_prebuilt_jar(pom)
return template

def android_library(self, pom: MavenDependency):
artifact = pom.get_artifact()
artifact_path = self.fix_path(artifact)
header = f"""
android_library(
name = "{pom.artifact_id}",
"""
footer = f""" visibility = [ "PUBLIC" ],
)
"""
with io.StringIO() as out:
out.write(header)
out.write(f""" exported_deps = [ \n ":_{pom.artifact_id}", \n""")
dependencies = pom.get_dependencies()
for d in dependencies:
out.write(f' "{self.repo.get_buck_target(d)}",\n')
out.write(f""" ],\n""")
out.write(footer)
contents = out.getvalue()
return contents

def get_android_prebuilt_aar(self, dep: MavenDependency):
artifact_name = self.repo.get_artifact_name(dep, ArtifactFlavor.AAR)
artifact_path = f"{dep.artifact_id}/{dep.version}/{artifact_name}"
with io.StringIO() as out:
out.write(
f"""
android_prebuilt_aar(
name = '_{dep.artifact_id}',
aar = '{artifact_path}',
"""
)
out.write(")\n")
contents = out.getvalue()
return contents

def get_prebuilt_jar(self, dep: MavenDependency):
artifact = dep.get_artifact()
artifact_name = self.repo.get_artifact_name(dep, ArtifactFlavor.JAR)
artifact_path = f"{dep.artifact_id}/{dep.version}/{artifact_name}"
with io.StringIO() as out:
out.write(
f"""
prebuilt_jar(
name = '_{dep.artifact_id}',
binary_jar = '{artifact_path}',
"""
)
out.write(")\n")
contents = out.getvalue()
return contents
117 changes: 117 additions & 0 deletions tools/import_deps/dependency/gradle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# Copyright (c) Meta Platforms, Inc. and its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from pathlib import Path

from dependency.pom import MavenDependency


class GradleDependencies:
"""
Import gradle dependencies.
"""

def __init__(self) -> None:
super().__init__()
self.re_deps_version = re.compile(r"([\d.]+) -> ([\d.]+)(( \([c*]\))|())$")
self.re_deps_version3 = re.compile(
r"(\{strictly [\d.]+\}) -> ([\d.]+)(( \([c*]\))|())$"
)
self.re_deps_version2 = re.compile(r"([\d.]+)( \([c*]\))|()$")
self.re_deps_line = re.compile(r"^[+|\\].*$")
self.re_deps_groups = re.compile(r"^([+|\\][+-\\| ]+)(.*):(.*):(.*)$")

def import_gradle_dependencies(self, report_file: Path):
"""
Import gradle dependencies created as
./gradlew -q :app:dependencies --configuration debugCompileClasspath > report_file.txt

Create a set of dependencies.
The method will pick a resolved version of the dependency.
I.e. the version that is used by the gradle build.
:param report_file: a report file path
:return: a set of dependencies.
"""
all_dependencies = set()
dependencies_stack = []
with open(report_file, "r") as f:
line = f.readline()
while line:
line = f.readline()
if self.re_deps_line.match(line):
m = self.re_deps_groups.match(line)
line_prefix = m.group(1)
extracted_dependency = self.extract_dependency(line)
pd = self.find_dependency(all_dependencies, extracted_dependency)
all_dependencies.add(pd)
if len(dependencies_stack) == 0:
dependencies_stack.append((line_prefix, pd))
continue
if len(dependencies_stack) > 0:
parent = dependencies_stack[len(dependencies_stack) - 1]
parent_prefix = parent[0]
parent_dep = parent[1]
if len(line_prefix) > len(parent_prefix):
parent_dep.add_dependency(pd)
dependencies_stack.append((line_prefix, pd))
elif len(line_prefix) < len(parent_prefix):
parent = dependencies_stack.pop()
while len(parent[0]) > len(line_prefix):
parent = dependencies_stack.pop()
if len(dependencies_stack) > 0:
parent = dependencies_stack[len(dependencies_stack) - 1]
parent_dep = parent[1]
parent_dep.add_dependency(pd)
dependencies_stack.append((line_prefix, pd))
else:
dependencies_stack.pop()
dependencies_stack.append((line_prefix, pd))
if len(dependencies_stack) >= 2:
grandparent = dependencies_stack[
len(dependencies_stack) - 2
]
grandparent_dep = grandparent[1]
grandparent_dep.add_dependency(pd)
return all_dependencies

def find_dependency(
self, all_dependencies: set, pd: MavenDependency
) -> MavenDependency:
for d in all_dependencies:
if pd.is_keys_equal(d):
return d
return pd

def extract_dependency(self, line: str) -> MavenDependency:
m = self.re_deps_groups.match(line)
group_id = m.group(2)
artifact_id = m.group(3)
version = self.match_version(m.group(4))
pd = MavenDependency(group_id, artifact_id, version, "")
return pd

def match_version(self, version: str):
v = version
vm = self.re_deps_version.match(version)
if vm:
v = vm.group(2)
else:
vm = self.re_deps_version2.match(version)
if vm:
v = vm.group(1)
else:
vm = self.re_deps_version3.match(version)
if vm:
v = vm.group(2)
return v
Loading