Commit 9502326d authored by Mykola Dzyuba's avatar Mykola Dzyuba
Browse files

Automate importing third-party dependencies.

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

Please see tools/import_deps/README.md for more details.
1 merge request!2706Automate importing third-party dependencies.
Pipeline #807 failed with stages
in 0 seconds
Showing with 2307 additions and 0 deletions
+2307 -0
libs
buck-out
.buckd
BUCK
venv
third-party
\ No newline at end of file
## 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.
# 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.
# 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
# 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
# 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 logging
import os
from collections import deque
from .pom import MavenDependency, PomXmlAdapter, Artifact
from .repo import Repo, ArtifactFlavor
class Manager:
"""
Provides methods to import Maven dependencies to a local repository (../libs).
"""
# A stack of pom files to be updated and parsed as we add more dependencies.
pom_files = []
def __init__(self, repo: Repo) -> None:
super().__init__()
self.repo = repo
def import_maven_dep(self, gradle_dependency: str):
"""
Import a maven dependency as well as its dependencies.
Create BUCK files.
:param gradle_dependency:
:return: a PomDependency
"""
pom_dependency = self.to_maven_dependency(gradle_dependency)
self.import_dep(pom_dependency)
return pom_dependency
def import_dep(self, dependency: MavenDependency) -> set:
all_dependencies = set()
visited = set()
return self._import_dep(dependency, all_dependencies, visited)
def _import_dep(
self, dependency: MavenDependency, all_dependencies, visited=set()
) -> set:
"""
Import a maven dependency with its transitive dependencies.
:param all_dependencies:
:param dependency:
:param all_dependencies: set of all dependencies
:param visited: a set of visited dependencies
:return:
"""
if visited.__contains__(dependency.__str__()):
return all_dependencies
visited.add(dependency.__str__())
all_dependencies.add(dependency)
for flavor in Repo.artifact_flavors:
file = self.repo.download_maven_dep(dependency, flavor)
if not os.path.exists(file):
logging.debug(f"Unable to download: {file}")
else:
dependency.add_artifact(Artifact(file))
if flavor == ArtifactFlavor.POM:
self.pom_files.append(file)
# parse pom
while len(self.pom_files) > 0:
pom_file = self.pom_files.pop()
pom_xml_adapter = PomXmlAdapter(pom_file)
deps = pom_xml_adapter.get_deps()
for d in deps:
dependency.add_dependency(d)
self._import_dep(d, all_dependencies, visited)
return all_dependencies
def import_dep_shallow(self, dependency: MavenDependency) -> []:
"""
Import a maven dependency artifacts without its transitive dependencies.
:param dependency:
:return: a list of downloaded files
"""
downloaded_files = []
for flavor in Repo.artifact_flavors:
file = self.repo.download_maven_dep(dependency, flavor)
if os.path.exists(file):
downloaded_files.append(file)
dependency.add_artifact(Artifact(file))
return downloaded_files
def resolve_deps_versions(self, dependency: MavenDependency, deps: []) -> None:
"""
In some cases POM does not have versions for dependencies.
This method obtains a release version for such dependencies.
:param dependency: a dependncy to be updated
:param deps: dependencies
:return: None
"""
for d in deps:
if d.version is None:
d.version = self.repo.get_release_version(d)
dependency.add_dependency(d)
def import_deps_shallow(self, deps: set):
"""
Import dependencies from the set.
Do not import dependsOn dependencies.
:param deps: a set of dependencies
:return:
"""
for d in deps:
self.import_dep_shallow(d)
def to_maven_dependency(self, gradle_dependency: str) -> MavenDependency:
d = gradle_dependency.split(":")
pd = MavenDependency(d[0], d[1], d[2], "")
return pd
def import_missing_dependencies(self, deps: set, versions: dict) -> set:
visited = set()
self._import_missing_dependencies(deps, versions, visited)
return visited
def _import_missing_dependencies(self, deps: set, versions: dict, visited: set):
queue = deque(deps)
while len(queue) > 0:
dep: MavenDependency = queue.popleft()
if dep in visited:
continue
visited.add(dep)
dep = self.get_larger_version(dep, versions)
missing_dependencies = self.check_missing_dependencies(dep)
for md in missing_dependencies:
if md.version is None:
md.version = self.repo.get_release_version(md)
md = self.get_larger_version(md, versions)
assert md.version is not None
dep.add_dependency(md)
self.import_dep_shallow(md)
queue.append(md)
def get_larger_version(
self, dep: MavenDependency, versions: dict
) -> MavenDependency:
"""
Find a matching version in the versions dict. Return it if it is a larger version.
Or return the dep.
:param dep:
:param versions: a repository of objects with the larger version
:return:
"""
key = dep.get_group_and_artifactid()
if key not in versions.keys():
versions[key] = dep
return dep
larger_version = dep.get_larger_version(versions[key].version, dep.version)
if dep != larger_version:
return versions[key]
return dep
def check_missing_dependencies(self, dep: MavenDependency) -> set:
pom_file = self.repo.download_maven_dep(dep, ArtifactFlavor.POM)
pom_xml_adapter = PomXmlAdapter(pom_file)
pom_deps = pom_xml_adapter.get_deps()
missing = set(pom_deps) - dep.get_dependencies()
return missing
def update_versions(
self, dep: MavenDependency, versions: dict, visited=set()
) -> None:
"""
Build a set of all dependencies with the largest version numbers./
:param dep:
:param versions:
:param visited:
:param all_dependencies:
:return:
"""
if visited.__contains__(dep):
return
visited.add(dep)
# if the dep has a lover version than in versions, then return also
group_and_artifactid = dep.get_group_and_artifactid()
if group_and_artifactid in versions.keys():
v = versions[group_and_artifactid]
largest_version = dep.get_larger_version(dep.version, v)
if largest_version is v:
return
# at this point dep either unique or has a large version
# set or bump the version up
versions[group_and_artifactid] = dep.version
pom_file = self.repo.download_maven_dep(dep, ArtifactFlavor.POM)
pom_xml_adapter = PomXmlAdapter(pom_file)
pom_deps = pom_xml_adapter.get_deps()
for d in pom_deps:
self.get_all_dependencies(d, versions, visited)
def update_dependencies(
self, dep: MavenDependency, versions: dict, visited=set()
) -> None:
if visited.__contains__(dep):
return
visited.add(dep)
pom_file = self.repo.download_maven_dep(dep, ArtifactFlavor.POM)
pom_xml_adapter = PomXmlAdapter(pom_file)
pom_deps = pom_xml_adapter.get_deps()
deps = dep.get_dependencies()
for d in pom_deps:
if d not in deps:
v = versions[d.get_group_and_artifactid()]
x = d
if v is not d.version:
x = MavenDependency(d.group_id, d.artifact_id, v)
self.import_dep_shallow(x)
dep.add_dependency(x)
self.update_dependencies(d, versions, visited)
# 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 logging
import os
import re
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import List, Optional
class Artifact:
"""
A specific file of a dependency: jar, aar, pom, etc.
"""
def __init__(self, file: Path):
self.file = file
def _key(self):
return self.file
def __hash__(self) -> int:
return hash(self._key())
def __eq__(self, o: object) -> bool:
if not isinstance(o, Artifact):
return False
return o.file == self.file
class MavenDependency:
"""
A Maven dependency that contains artifacts (jar, aar, ...) and has its
own dependencies.
"""
def __init__(
self, group_id: str, artifact_id: str, version: str, scope: str = "runtime"
) -> None:
self.group_id = group_id
self.artifact_id = artifact_id
self.version = version
self.scope = scope
self.dependsOn = set()
self.artifacts = set()
def __str__(self) -> str:
return f"{self.group_id}:{self.artifact_id}:{self.version}:{self.scope}"
def __hash__(self) -> int:
return hash(self._key())
def add_dependency(self, dependency: "MavenDependency") -> None:
"""
Adds a dependency. The dependency will be added if
it has a different group and artifactId than self,
or it is new to the collection of dependencies,
or if its version is larger than the version of an existing
dependency. In this case, the dependency is substituted.
:param dependency: a dependency to be added.
"""
if dependency == self:
return
if self.get_group_and_artifactid() == dependency.get_group_and_artifactid():
return
if self.dependsOn.__contains__(dependency):
return
partial_match = self.find_dep(dependency.group_id, dependency.artifact_id)
if partial_match is not None:
larger_version = self.get_larger_version(
partial_match.version, dependency.version
)
if partial_match.version is not larger_version:
self.dependsOn.remove(partial_match)
self.dependsOn.add(dependency)
return
return
self.dependsOn.add(dependency)
def get_larger_version(self, v1: str, v2: str) -> str:
x = [v1, v2]
x.sort()
return x[1]
def add_artifact(self, artifact: Artifact):
self.artifacts.add(artifact)
def get_artifact(self):
for artifact in self.artifacts:
file = artifact.file
if (
re.match(r".*.aar$", file)
or re.match(r".*.jar$", file)
and not re.match(r".*-sources.jar$", file)
):
return file
def get_artifacts(self):
return self.artifacts
def is_keys_equal(self, pd: "MavenDependency"):
return (
pd.artifact_id == self.artifact_id
and pd.group_id == self.group_id
and pd.version == self.version
)
def is_group_and_artifactid_equal(self, pd: "MavenDependency"):
return pd.artifact_id == self.artifact_id and pd.group_id == self.group_id
def get_group_and_artifactid(self):
return f"{self.group_id}_{self.artifact_id}"
def get_version(self):
return self.version
def update_version(self, version: str):
self.version = version
def get_dependencies(self) -> set:
return self.dependsOn
def get_all_dependencies(self) -> set:
"""
Find all dependencies recursively.
:return: a set of dependencies.
"""
visited = set()
queue = set()
queue.add(self)
while len(queue) > 0:
d = queue.pop()
if d in visited:
continue
visited.add(d)
for n in d.get_dependencies():
queue.add(n)
return visited
def print_dependency_tree(self):
visited = set()
prefix = ""
self._print_dependency_tree(prefix, visited)
def _print_dependency_tree(self, prefix: str, visited: set):
if self in visited:
if len(self.get_dependencies()) > 0:
print(f"{prefix} {self.__str__()} ...")
else:
print(f"{prefix} {self.__str__()}")
return
visited.add(self)
print(f"{prefix} {self.__str__()}")
prefix = f"{prefix} >"
for d in self.get_dependencies():
d._print_dependency_tree(prefix, visited)
def get_compile_dependencies(self) -> set:
compile_dependencies = set()
for d in self.dependsOn:
if d.scope == "compile":
compile_dependencies.add(d)
return compile_dependencies
def get_runtime_dependencies(self) -> set:
runtime_dependencies = set()
for d in self.dependsOn:
if d.scope != "compile":
runtime_dependencies.add(d)
return runtime_dependencies
def _key(self):
return self.group_id, self.artifact_id, self.version, self.scope
def get_libs_path(self):
l = self.group_id.split(".")
l.append(self.artifact_id)
l.append(self.version)
return l
def get_artifact_versions_dir(self) -> []:
"""
Provides a master folder for the artifact. This is a base for sub-folders with specific artifact versions.
:return: a folder path.
"""
l = self.group_id.split(".")
l.append(self.artifact_id)
return l
def __eq__(self, o: object) -> bool:
if not isinstance(o, MavenDependency):
return False
return (
self.group_id == o.group_id
and self.artifact_id == o.artifact_id
and self.version == o.version
and self.scope == o.scope
)
def get_full_name(self):
return f"{self.group_id}:{self.artifact_id}"
def get_group_id(self):
return self.group_id
def get_artifact_id(self):
return self.artifact_id
def get_scope(self):
return self.scope
def set_scope(self, scope: str):
self.scope = scope
def find_dep(self, group_id: str, artifact_id: str):
for d in self.dependsOn:
if d.get_full_name() == f"{group_id}:{artifact_id}":
return d
class PomXmlAdapter:
_NAMESPACES = {"xmlns": "http://maven.apache.org/POM/4.0.0"}
def __init__(self, pom_file: str):
self._pom_file = pom_file
def get_deps(self) -> List[MavenDependency]:
deps = []
if not os.path.exists(self._pom_file):
logging.warning(f"The pom file is not found: {self._pom_file}")
return deps
tree = ET.parse(self._pom_file)
root = tree.getroot()
dependencies = root.findall(
"./xmlns:dependencies/xmlns:dependency", namespaces=self._NAMESPACES
)
for dep_node in dependencies:
dependency = self._create_pom_dependency(dep_node, root)
deps.append(dependency)
# TODO: revisit the "test" scope
deps = [d for d in deps if d.version != "" and d.scope != "test"]
return deps
def get_group_id(self, root):
group_id = root.find("./xmlns:groupId", namespaces=self._NAMESPACES)
if group_id is None:
group_id = root.find(
"./xmlns:parent/xmlns:groupId", namespaces=self._NAMESPACES
)
return group_id.text
def get_package_version(self, root):
"""
Provides a version of the main artifact of the POM.
:param root: an XML tree root
:return: an artifact version
"""
version = root.find("./xmlns:version", namespaces=self._NAMESPACES)
if version is None:
version = root.find(
"./xmlns:parent/xmlns:version", namespaces=self._NAMESPACES
)
if version is None:
version = root.find("./version")
if version is None:
logging.warning(f"Unable to find the version for {self._pom_file}")
version = root.find("version")
logging.debug(f"Project version: {version}")
return version
def get_dependency_version(self, dependency_node, root):
"""
Provides a version of a dependency.
:param dependency_node: a dependency XML node
:param artifact_version: a version of the artifact
:param root: an XML tree root
:return: a dependency version. None if no version is provided.
"""
version_element = dependency_node.find(
"xmlns:version", namespaces=self._NAMESPACES
)
if version_element is None:
# Some POMs have dependencies without versions. For example:
# third-party/com/google/guava/guava-testlib/31.1-jre/guava-testlib-31.1-jre.pom
return None
# Sometimes version is specified with a property or a reference to a
# project (main artifact) version.
# Check if the version is specified with a variable that starts with ${...
x = re.compile(r"\${(.*)}")
r = x.match(version_element.text)
if r is not None:
property_name = r.group(1)
if property_name.startswith("project"):
version_element = self.get_package_version(root)
else:
# <version>${hamcrestVersion}</version>
version_element = root.find(
f"./xmlns:properties/xmlns:{property_name}",
namespaces=self._NAMESPACES,
)
if version_element is not None:
logging.warning(f"Unable to find definition of {property_name}")
version = None
if version_element is not None:
version_text = PomXmlAdapter.get_or_empty(version_element)
version = self.resolve_complicated_version(version_text)
return version
def _create_pom_dependency(self, dep_node, root) -> MavenDependency:
group_id = dep_node.find("xmlns:groupId", namespaces=self._NAMESPACES).text
if group_id == "${project.groupId}":
group_id = self.get_group_id(root)
assert group_id is not None, f"groupId is not found for {self._pom_file}"
artifact_id = dep_node.find(
"xmlns:artifactId", namespaces=self._NAMESPACES
).text
version_text = self.get_dependency_version(dep_node, root)
scope = PomXmlAdapter.get_or_empty(
dep_node.find("xmlns:scope", namespaces=self._NAMESPACES)
)
logging.debug(
f"groupid: {group_id}, artifactId: {artifact_id}, version: {version_text}"
)
pd = MavenDependency(group_id, artifact_id, version_text, scope)
return pd
def get_or_empty(e: Optional[ET.Element]) -> str:
if e is None:
return ""
else:
# pyre-ignore
return e.text
def resolve_complicated_version(self, v: str, default_version: str = None) -> str:
"""
Eventually we want to resolve all uncommon pom.xml version patterns:
- <version>1.0.1</version> - simple version (Supported)
- <version>[1.0.1]</version> - exact version (Supported)
- <version>[1.0.0,2.0.0)</version> - range with upper bound (NOT supported)
- <version>[1.0.0,)</version> - range without upper bound (NOT supported)
Will not support:
- <version>LATEST</version> - removed in Maven 3.0
- <version>RELEASE</version> - removed in Maven 3.0
- <version>{VERSION_VARIABLE_FROM_POM}</version> - too complicated
"""
# (v), [v), or [v]
if v.isdecimal():
return v
if re.match(r"\d+\.\d+\.\d+", v):
return v
if re.match(r"\d+\.\d+", v):
return v
bounded = re.search(r"^[\[\(].*[\]\)]$", v)
if bounded:
return v[1:-1]
else:
# default
return default_version
class MavenMetadataParser:
"""
This is a maven-metadata.xml parser.
"""
def get_release_version(self, file: str) -> str:
"""
Parses maven-metadata.xml and returns a release version.
:param file: a maven-metadata.xml
:return: a release version
"""
assert os.path.exists(file)
tree = ET.parse(file)
root = tree.getroot()
version = root.find("./versioning/release")
return version.text
# 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 logging
import os
from enum import Enum
from pathlib import Path
from typing import List
import urllib3
from .pom import Artifact, PomXmlAdapter, MavenMetadataParser
from .pom import MavenDependency
class ArtifactFlavor(Enum):
JAR = ".jar"
AAR = ".aar"
POM = ".pom"
SOURCE_JAR = "-sources.jar"
class Repo:
repo_url_maven = "https://repo1.maven.org/maven2/"
repo_url_google = "https://maven.google.com/"
artifact_flavors: List[ArtifactFlavor] = [
ArtifactFlavor.JAR,
ArtifactFlavor.AAR,
ArtifactFlavor.POM,
ArtifactFlavor.SOURCE_JAR,
]
def __init__(self, libs="libs") -> None:
super().__init__()
self.libs = libs
self.root = f"./{libs}"
def get_libs_name(self):
"""
:return: a name of the repo libs folder
"""
return self.libs
def get_base_name(self) -> str:
return f"//{os.path.basename(self.libs)}"
def get_buck_target(self, dep: MavenDependency) -> str:
"""
Provides a fully qualified name for the dependency library in the repo.
:param dep: a dependency
:return: a fully qualified name for the dependency library in the repo.
"""
artifact_base_dir = f"{self.get_base_name()}"
for p in dep.group_id.split("."):
artifact_base_dir = artifact_base_dir + "/" + p
target = artifact_base_dir + ":" + dep.artifact_id
return target
def get_root(self):
return self.root
def mkdirs(self, path: list) -> bool:
"""
Create directories in libs folder.
"""
if not os.path.exists(self.root) or not os.path.isdir(self.root):
return False
d = self.get_path(path)
if not os.path.isdir(d):
os.makedirs(name=d, exist_ok=True)
return os.path.isdir(d)
def get_path(self, path: list) -> Path:
d = self.root
for folder in path:
d = os.path.join(d, folder)
return d
def get_dependency_dir(self, dep: MavenDependency) -> Path:
d = Path(self.root)
for folder in dep.get_libs_path():
d = os.path.join(d, folder)
return d
def get_dependency_path(self, dep: MavenDependency, flavor: ArtifactFlavor) -> Path:
"""
Provides a path to an artifact file in the local repo.
"""
d = self.get_dependency_dir(dep)
file_name = self.get_artifact_name(dep, flavor)
path = os.path.join(d, file_name)
return path
def get_artifact_name(self, dep: MavenDependency, flavor: ArtifactFlavor):
file_name = f"{dep.artifact_id}-{dep.version}{flavor.value}"
return file_name
def get_base_url(self, dep: MavenDependency):
group = dep.group_id.replace(".", "/")
maven_repo = self.get_maven_repo(dep)
base_url: str = f"{maven_repo}{group}/{dep.artifact_id}/{dep.version}/{dep.artifact_id}-{dep.version}"
return base_url
def get_maven_repo(self, dep: MavenDependency):
if dep.group_id.startswith("androidx") or dep.group_id.startswith(
"com.google.android"
):
return self.repo_url_google
return self.repo_url_maven
def get_release_version(self, dep: MavenDependency) -> str:
"""
Check Maven repos and obtain a release version of a dependency.
:param dep:
:return: a version from maven-metadata.xml.
"""
maven_repo = self.get_maven_repo(dep)
group = dep.group_id.replace(".", "/")
maven_metadata_url: str = (
f"{maven_repo}{group}/{dep.artifact_id}/maven-metadata.xml"
)
artifact_base_dir = Path(self.root)
for d in dep.get_artifact_versions_dir():
artifact_base_dir = os.path.join(artifact_base_dir, d)
if not os.path.exists(artifact_base_dir):
os.makedirs(artifact_base_dir, exist_ok=True)
maven_metadata_file = os.path.join(artifact_base_dir, "maven-metadata.xml")
downloaded = self.download(maven_metadata_url, maven_metadata_file)
if not downloaded:
return None
parser = MavenMetadataParser()
version = parser.get_release_version(maven_metadata_file)
return version
def get_url(self, dep: MavenDependency, flavor: ArtifactFlavor):
return f"{self.get_base_url(dep)}{flavor.value}"
def download(self, url, path) -> bool:
with urllib3.PoolManager() as http:
r = http.request("GET", url, preload_content=False)
if r.status != 200:
return False
with open(path, "wb") as out:
while True:
data = r.read()
if not data:
break
out.write(data)
r.release_conn()
return True
def download_maven_dep(
self, dep: MavenDependency, flavor: ArtifactFlavor, force: bool = False
) -> Path:
"""
Download a Maven dependency
:param dep: the base dependency
:param flavor: a flavor: jar, aar, pom, etc
:param force: download even if the file exists
:return: a dependency file path
"""
destination = self.get_dependency_dir(dep)
if not os.path.exists(destination):
os.makedirs(destination, exist_ok=True)
url = self.get_url(dep, flavor)
repo_file = self.get_dependency_path(dep, flavor)
if not os.path.exists(repo_file) or force:
self.download(url, repo_file)
logging.debug(f"url: {url}, file: {os.path.abspath(repo_file)}")
else:
logging.debug(f"url: {url}, file: {repo_file} exists")
return repo_file
def load_artifacts(self, pom: MavenDependency):
"""
Check the disk for available artifacts and add them to the
collection.
:return:
"""
destination = self.get_dependency_dir(pom)
if not os.path.exists(destination):
return
for flavor in Repo.artifact_flavors:
repo_file = self.get_dependency_path(pom, flavor)
if os.path.exists(repo_file):
pom.add_artifact(Artifact(repo_file))
def update_scope_from_pom(self, dep: MavenDependency):
pom_file = self.download_maven_dep(dep, ArtifactFlavor.POM)
pom_xml_adapter = PomXmlAdapter(pom_file)
pom_deps = pom_xml_adapter.get_deps()
for pd in pom_deps:
d = dep.find_dep(pd.get_group_id(), pd.get_artifact_id())
if d is None:
logging.debug(f"dependency not found: {pd} in {dep}")
return
if d.get_scope() != pd.get_scope():
d.set_scope(pd.get_scope())
#!/usr/bin/env python3
# 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 argparse
import logging
import os
from dependency.buck import Buck
from dependency.gradle import GradleDependencies
from dependency.manager import Manager
from dependency.repo import Repo
def main():
parser = argparse.ArgumentParser(
description="""Import gradle dependencies and generate BUCK file.
Created gradle dependencies file by running
./gradlew -q :app:dependencies --configuration debugCompileClasspath > report_file.txt
"""
)
parser.add_argument("--gdeps", required=True, help="gradle dependencies file")
parser.add_argument(
"--libs", default="third-party", help="libs folder for imported dependencies"
)
try:
args = parser.parse_args()
print(f"args: {args}")
gdeps = args.gdeps
libs = args.libs
print(f"gdeps: {gdeps}, libs: {libs}")
import_deps(gdeps, libs)
except argparse.ArgumentError:
print("Invalid arguments")
def import_deps(gradle_deps: str, libs: str) -> None:
"""
Import dependencies and create BUCK file.
:param gradle_deps: file with gradle dependencies
:param libs: path to a folder where to store the dependencies
:return: None
"""
logging.basicConfig(level=logging.INFO)
assert os.path.exists(gradle_deps), f"Unable to open {gradle_deps}"
if not os.path.exists(libs):
os.mkdir(libs)
assert os.path.exists(libs)
gd = GradleDependencies()
# Importing dependencies from a file, will not determine the scope.
# I.e. whether it is a runtime or compile time dependency.
# We will need to parse pom for the dependency and update it.
deps = gd.import_gradle_dependencies(gradle_deps)
repo = Repo(libs=libs)
buck = Buck(repo)
# Remove ./libs/BUCK file if it exists
buck_file = buck.get_path()
if os.path.exists(buck_file):
os.remove(buck_file)
manager = Manager(repo)
versions = dict()
for dep in deps:
manager.import_dep_shallow(dep)
versions[dep.get_group_and_artifactid()] = dep
# Check for missing dependencies
manager.import_missing_dependencies(deps, versions)
visited = set()
for dep in versions.values():
buck_file = buck.get_buck_path_for_dependency(dep)
# This will also update a BUCK file
print(f"adding {dep}")
buck.create(buck_file, dep, visited)
visited.add(dep)
main()
# 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.
------------------------------------------------------------
Project ':app'
------------------------------------------------------------
debugCompileClasspath - Resolved configuration for compilation for variant: debug
+--- androidx.appcompat:appcompat:1.4.1
| +--- androidx.annotation:annotation:1.3.0
| +--- androidx.core:core:1.7.0
| | +--- androidx.annotation:annotation:1.2.0 -> 1.3.0
| | +--- androidx.annotation:annotation-experimental:1.1.0
| | +--- androidx.lifecycle:lifecycle-runtime:2.3.1 -> 2.4.0
| | | +--- androidx.lifecycle:lifecycle-common:2.4.0
| | | | \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | | +--- androidx.arch.core:core-common:2.1.0
| | | | \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | | \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | \--- androidx.versionedparcelable:versionedparcelable:1.1.1
| | +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | \--- androidx.collection:collection:1.0.0 -> 1.1.0
| | \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| +--- androidx.cursoradapter:cursoradapter:1.0.0
| | \--- androidx.annotation:annotation:1.0.0 -> 1.3.0
| +--- androidx.activity:activity:1.2.4 -> 1.3.1
| | +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | +--- androidx.core:core:1.1.0 -> 1.7.0 (*)
| | +--- androidx.lifecycle:lifecycle-runtime:2.3.1 -> 2.4.0 (*)
| | +--- androidx.lifecycle:lifecycle-viewmodel:2.3.1
| | | \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | +--- androidx.savedstate:savedstate:1.1.0
| | | \--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | \--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1
| | +--- androidx.annotation:annotation:1.0.0 -> 1.3.0
| | +--- androidx.savedstate:savedstate:1.1.0 (*)
| | +--- androidx.lifecycle:lifecycle-livedata-core:2.3.1
| | | \--- androidx.lifecycle:lifecycle-common:2.3.1 -> 2.4.0 (*)
| | \--- androidx.lifecycle:lifecycle-viewmodel:2.3.1 (*)
| +--- androidx.fragment:fragment:1.3.6
| | +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | +--- androidx.core:core:1.2.0 -> 1.7.0 (*)
| | +--- androidx.collection:collection:1.1.0 (*)
| | +--- androidx.viewpager:viewpager:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0 -> 1.3.0
| | | +--- androidx.core:core:1.0.0 -> 1.7.0 (*)
| | | \--- androidx.customview:customview:1.0.0 -> 1.1.0
| | | +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | | \--- androidx.core:core:1.3.0 -> 1.7.0 (*)
| | +--- androidx.loader:loader:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0 -> 1.3.0
| | | +--- androidx.core:core:1.0.0 -> 1.7.0 (*)
| | | +--- androidx.lifecycle:lifecycle-livedata:2.0.0
| | | | +--- androidx.arch.core:core-runtime:2.0.0 -> 2.1.0
| | | | | +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | | | | \--- androidx.arch.core:core-common:2.1.0 (*)
| | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.0.0 -> 2.3.1 (*)
| | | | \--- androidx.arch.core:core-common:2.0.0 -> 2.1.0 (*)
| | | \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 -> 2.3.1 (*)
| | +--- androidx.activity:activity:1.2.4 -> 1.3.1 (*)
| | +--- androidx.lifecycle:lifecycle-livedata-core:2.3.1 (*)
| | +--- androidx.lifecycle:lifecycle-viewmodel:2.3.1 (*)
| | +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1 (*)
| | +--- androidx.savedstate:savedstate:1.1.0 (*)
| | \--- androidx.annotation:annotation-experimental:1.0.0 -> 1.1.0
| +--- androidx.appcompat:appcompat-resources:1.4.1
| | +--- androidx.annotation:annotation:1.2.0 -> 1.3.0
| | +--- androidx.core:core:1.0.1 -> 1.7.0 (*)
| | +--- androidx.vectordrawable:vectordrawable:1.1.0
| | | +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | | +--- androidx.core:core:1.1.0 -> 1.7.0 (*)
| | | \--- androidx.collection:collection:1.1.0 (*)
| | \--- androidx.vectordrawable:vectordrawable-animated:1.1.0
| | +--- androidx.vectordrawable:vectordrawable:1.1.0 (*)
| | +--- androidx.interpolator:interpolator:1.0.0
| | | \--- androidx.annotation:annotation:1.0.0 -> 1.3.0
| | \--- androidx.collection:collection:1.1.0 (*)
| +--- androidx.drawerlayout:drawerlayout:1.0.0 -> 1.1.1
| | +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | +--- androidx.core:core:1.2.0 -> 1.7.0 (*)
| | \--- androidx.customview:customview:1.1.0 (*)
| \--- androidx.savedstate:savedstate:1.1.0 (*)
+--- com.google.android.material:material:1.5.0
| +--- androidx.annotation:annotation:1.2.0 -> 1.3.0
| +--- androidx.appcompat:appcompat:1.1.0 -> 1.4.1 (*)
| +--- androidx.cardview:cardview:1.0.0
| | \--- androidx.annotation:annotation:1.0.0 -> 1.3.0
| +--- androidx.coordinatorlayout:coordinatorlayout:1.1.0
| | +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | +--- androidx.core:core:1.1.0 -> 1.7.0 (*)
| | +--- androidx.customview:customview:1.0.0 -> 1.1.0 (*)
| | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
| +--- androidx.constraintlayout:constraintlayout:2.0.1 -> 2.1.3
| +--- androidx.core:core:1.5.0 -> 1.7.0 (*)
| +--- androidx.drawerlayout:drawerlayout:1.1.1 (*)
| +--- androidx.dynamicanimation:dynamicanimation:1.0.0
| | +--- androidx.core:core:1.0.0 -> 1.7.0 (*)
| | +--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
| | \--- androidx.legacy:legacy-support-core-utils:1.0.0
| | +--- androidx.annotation:annotation:1.0.0 -> 1.3.0
| | +--- androidx.core:core:1.0.0 -> 1.7.0 (*)
| | +--- androidx.documentfile:documentfile:1.0.0
| | | \--- androidx.annotation:annotation:1.0.0 -> 1.3.0
| | +--- androidx.loader:loader:1.0.0 (*)
| | +--- androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
| | | \--- androidx.annotation:annotation:1.0.0 -> 1.3.0
| | \--- androidx.print:print:1.0.0
| | \--- androidx.annotation:annotation:1.0.0 -> 1.3.0
| +--- androidx.annotation:annotation-experimental:1.0.0 -> 1.1.0
| +--- androidx.fragment:fragment:1.0.0 -> 1.3.6 (*)
| +--- androidx.lifecycle:lifecycle-runtime:2.0.0 -> 2.4.0 (*)
| +--- androidx.recyclerview:recyclerview:1.0.0 -> 1.1.0
| | +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | +--- androidx.core:core:1.1.0 -> 1.7.0 (*)
| | +--- androidx.customview:customview:1.0.0 -> 1.1.0 (*)
| | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
| +--- androidx.transition:transition:1.2.0
| | +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| | +--- androidx.core:core:1.0.1 -> 1.7.0 (*)
| | \--- androidx.collection:collection:1.0.0 -> 1.1.0 (*)
| +--- androidx.vectordrawable:vectordrawable:1.1.0 (*)
| \--- androidx.viewpager2:viewpager2:1.0.0
| +--- androidx.annotation:annotation:1.1.0 -> 1.3.0
| +--- androidx.fragment:fragment:1.1.0 -> 1.3.6 (*)
| +--- androidx.recyclerview:recyclerview:1.1.0 (*)
| +--- androidx.core:core:1.1.0 -> 1.7.0 (*)
| \--- androidx.collection:collection:1.1.0 (*)
+--- androidx.constraintlayout:constraintlayout:2.1.3
+--- com.google.dagger:hilt-android:2.41
| +--- com.google.dagger:dagger:2.41
| | \--- javax.inject:javax.inject:1
| +--- com.google.dagger:dagger-lint-aar:2.41
| +--- com.google.dagger:hilt-core:2.41
| | +--- com.google.dagger:dagger:2.41 (*)
| | +--- com.google.code.findbugs:jsr305:3.0.2
| | \--- javax.inject:javax.inject:1
| +--- com.google.code.findbugs:jsr305:3.0.2
| +--- androidx.activity:activity:1.3.1 (*)
| +--- androidx.annotation:annotation:1.2.0 -> 1.3.0
| +--- androidx.fragment:fragment:1.3.6 (*)
| +--- androidx.lifecycle:lifecycle-common:2.3.1 -> 2.4.0 (*)
| +--- androidx.lifecycle:lifecycle-viewmodel:2.3.1 (*)
| +--- androidx.lifecycle:lifecycle-viewmodel-savedstate:2.3.1 (*)
| +--- androidx.savedstate:savedstate:1.1.0 (*)
| +--- javax.inject:javax.inject:1
| \--- org.jetbrains.kotlin:kotlin-stdlib:1.5.32
| +--- org.jetbrains:annotations:13.0
| \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.32
+--- androidx.appcompat:appcompat:{strictly 1.4.1} -> 1.4.1 (c)
+--- com.google.android.material:material:{strictly 1.5.0} -> 1.5.0 (c)
+--- androidx.constraintlayout:constraintlayout:{strictly 2.1.3} -> 2.1.3 (c)
+--- com.google.dagger:hilt-android:{strictly 2.41} -> 2.41 (c)
+--- androidx.annotation:annotation:{strictly 1.3.0} -> 1.3.0 (c)
+--- androidx.core:core:{strictly 1.7.0} -> 1.7.0 (c)
+--- androidx.cursoradapter:cursoradapter:{strictly 1.0.0} -> 1.0.0 (c)
+--- androidx.activity:activity:{strictly 1.3.1} -> 1.3.1 (c)
+--- androidx.fragment:fragment:{strictly 1.3.6} -> 1.3.6 (c)
+--- androidx.appcompat:appcompat-resources:{strictly 1.4.1} -> 1.4.1 (c)
+--- androidx.drawerlayout:drawerlayout:{strictly 1.1.1} -> 1.1.1 (c)
+--- androidx.savedstate:savedstate:{strictly 1.1.0} -> 1.1.0 (c)
+--- androidx.cardview:cardview:{strictly 1.0.0} -> 1.0.0 (c)
+--- androidx.coordinatorlayout:coordinatorlayout:{strictly 1.1.0} -> 1.1.0 (c)
+--- androidx.dynamicanimation:dynamicanimation:{strictly 1.0.0} -> 1.0.0 (c)
+--- androidx.annotation:annotation-experimental:{strictly 1.1.0} -> 1.1.0 (c)
+--- androidx.lifecycle:lifecycle-runtime:{strictly 2.4.0} -> 2.4.0 (c)
+--- androidx.recyclerview:recyclerview:{strictly 1.1.0} -> 1.1.0 (c)
+--- androidx.transition:transition:{strictly 1.2.0} -> 1.2.0 (c)
+--- androidx.vectordrawable:vectordrawable:{strictly 1.1.0} -> 1.1.0 (c)
+--- androidx.viewpager2:viewpager2:{strictly 1.0.0} -> 1.0.0 (c)
+--- com.google.dagger:dagger:{strictly 2.41} -> 2.41 (c)
+--- com.google.dagger:dagger-lint-aar:{strictly 2.41} -> 2.41 (c)
+--- com.google.dagger:hilt-core:{strictly 2.41} -> 2.41 (c)
+--- com.google.code.findbugs:jsr305:{strictly 3.0.2} -> 3.0.2 (c)
+--- androidx.lifecycle:lifecycle-common:{strictly 2.4.0} -> 2.4.0 (c)
+--- androidx.lifecycle:lifecycle-viewmodel:{strictly 2.3.1} -> 2.3.1 (c)
+--- androidx.lifecycle:lifecycle-viewmodel-savedstate:{strictly 2.3.1} -> 2.3.1 (c)
+--- javax.inject:javax.inject:{strictly 1} -> 1 (c)
+--- org.jetbrains.kotlin:kotlin-stdlib:{strictly 1.5.32} -> 1.5.32 (c)
+--- androidx.versionedparcelable:versionedparcelable:{strictly 1.1.1} -> 1.1.1 (c)
+--- androidx.collection:collection:{strictly 1.1.0} -> 1.1.0 (c)
+--- androidx.viewpager:viewpager:{strictly 1.0.0} -> 1.0.0 (c)
+--- androidx.loader:loader:{strictly 1.0.0} -> 1.0.0 (c)
+--- androidx.lifecycle:lifecycle-livedata-core:{strictly 2.3.1} -> 2.3.1 (c)
+--- androidx.vectordrawable:vectordrawable-animated:{strictly 1.1.0} -> 1.1.0 (c)
+--- androidx.customview:customview:{strictly 1.1.0} -> 1.1.0 (c)
+--- androidx.legacy:legacy-support-core-utils:{strictly 1.0.0} -> 1.0.0 (c)
+--- androidx.arch.core:core-common:{strictly 2.1.0} -> 2.1.0 (c)
+--- org.jetbrains:annotations:{strictly 13.0} -> 13.0 (c)
+--- org.jetbrains.kotlin:kotlin-stdlib-common:{strictly 1.5.32} -> 1.5.32 (c)
+--- androidx.lifecycle:lifecycle-livedata:{strictly 2.0.0} -> 2.0.0 (c)
+--- androidx.interpolator:interpolator:{strictly 1.0.0} -> 1.0.0 (c)
+--- androidx.documentfile:documentfile:{strictly 1.0.0} -> 1.0.0 (c)
+--- androidx.localbroadcastmanager:localbroadcastmanager:{strictly 1.0.0} -> 1.0.0 (c)
+--- androidx.print:print:{strictly 1.0.0} -> 1.0.0 (c)
\--- androidx.arch.core:core-runtime:{strictly 2.1.0} -> 2.1.0 (c)
(c) - dependency constraint
(*) - dependencies omitted (listed previously)
A web-based, searchable dependency report is available by adding the --scan option.
------------------------------------------------------------
Project ':app'
------------------------------------------------------------
debugCompileClasspath - Resolved configuration for compilation for variant: debug
+--- androidx.appcompat:appcompat:1.4.1
| +--- androidx.annotation:annotation:1.3.0
| +--- androidx.core:core:1.7.0
| | +--- androidx.annotation:annotation:1.2.0 -> 1.3.0
\--- androidx.arch.core:core-runtime:{strictly 2.1.0} -> 2.1.0 (c)
(c) - dependency constraint
(*) - dependencies omitted (listed previously)
A web-based, searchable dependency report is available by adding the --scan option.
# 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 glob
import os
import unittest
from dependency.repo import Repo
class BaseTestCase(unittest.TestCase):
@classmethod
def setUpClass(self):
self.repo = Repo("../third-party")
self.deps_file = "data/deps.txt"
@classmethod
def tearDownClass(self):
print(f"cleaning up the {self.repo.get_libs_name()}")
files = glob.glob(f"{self.repo.get_libs_name()}/**/*.*", recursive=True)
for f in files:
try:
if os.path.isfile(f):
os.remove(f)
except OSError as e:
self.fail(e)
# 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 filecmp
import os
import unittest
from pathlib import Path
from dependency.buck import Buck
from dependency.gradle import GradleDependencies
from dependency.manager import Manager
from dependency.pom import MavenDependency
from tests.test_base import BaseTestCase
class BuckTestCase(BaseTestCase):
def test_create(self):
buck = Buck(self.repo)
dependency = MavenDependency("com.google.dagger", "dagger", "2.41", "")
buck_file = buck.get_buck_path_for_dependency(dependency)
manager = Manager(self.repo)
downloaded_files = manager.import_dep_shallow(dependency)
buck.create(buck_file, dependency, set())
self.assertTrue(os.path.exists(buck_file))
self.assertTrue(
filecmp.cmp("data/buck/buck1.txt", buck_file),
f"files are different: {buck_file}",
)
def test_create_with_some_dependencies(self):
buck = Buck(self.repo)
dependency = MavenDependency("com.google.dagger", "dagger", "2.41", "")
buck_file = buck.get_buck_path_for_dependency(dependency)
manager = Manager(self.repo)
downloaded_files = manager.import_dep(dependency)
self.assertEqual(3, len(dependency.get_artifacts()))
self.assertEqual(1, len(dependency.get_dependencies()))
buck.create(buck_file, dependency, set())
self.assertTrue(os.path.exists(buck_file))
self.assertTrue(
filecmp.cmp("data/buck/buck2.txt", buck_file),
"dagger BUCK files are different",
)
def test_create_with_aar_dependencies(self):
buck = Buck(self.repo)
dependency = MavenDependency("com.google.dagger", "hilt-android", "2.41", "")
buck_file = buck.get_buck_path_for_dependency(dependency)
expected_file = os.path.join(Path(self.repo.root), "com/google/dagger/BUCK")
self.assertEqual(expected_file, buck_file.__str__())
manager = Manager(self.repo)
downloaded_files = manager.import_dep(dependency)
self.assertEqual(3, len(dependency.get_artifacts()))
self.assertEqual(13, len(dependency.get_dependencies()))
buck.create(buck_file, dependency, set())
self.assertTrue(os.path.exists(expected_file))
def test_create_with_all_dependencies(self):
"""
Read all dependencies from the gradle.
Create BUCK for all these dependencies.
"""
file = self.deps_file
self.assertTrue(os.path.exists(file))
gd = GradleDependencies()
# Importing dependencies from a file, will not determine the scope.
# I.e. whether it is runtime or a compile time dependency.
# We will need to parse pom for the dependency and update it.
deps = gd.import_gradle_dependencies(file)
self.assertEqual(47, len(deps))
buck = Buck(self.repo)
# Remove ./libs/BUCK file if it exists
buck_file = buck.get_path()
if os.path.exists(buck_file):
os.remove(buck_file)
manager = Manager(self.repo)
versions = dict()
for dep in deps:
manager.import_dep_shallow(dep)
versions[dep.get_group_and_artifactid()] = dep
# Check for missing dependencies
manager.import_missing_dependencies(deps, versions)
visited = set()
for dep in deps:
buck_file = buck.get_buck_path_for_dependency(dep)
# This will also update a BUCK file
buck.create(buck_file, dep, visited)
visited.add(dep)
def test_get_path_for_dependency(self):
buck = Buck(self.repo)
dependency = MavenDependency(
"com.google.guava", "guava-testlib", "31.1-jre", ""
)
path = buck.get_buck_path_for_dependency(dependency)
self.assertEqual(Path("../third-party/com/google/guava/BUCK"), path)
def tearDown(self) -> None:
super().tearDown()
buck1 = os.path.join(self.repo.root, "com/google/dagger/BUCK")
if os.path.exists(buck1):
os.rename(buck1, os.path.join(self.repo.root, "com/google/dagger/BUCK.bak"))
buck2 = os.path.join(self.repo.root, "javax/inject/BUCK")
if os.path.exists(buck2):
os.rename(buck2, os.path.join(self.repo.root, "javax/inject/BUCK.bak"))
if __name__ == "__main__":
unittest.main()
# 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 os
import unittest
from dependency.gradle import GradleDependencies
from dependency.manager import Manager
from dependency.pom import MavenDependency
from tests.test_base import BaseTestCase
class MyTestCase(BaseTestCase):
def test_import_gradle_dependencies(self):
file = "data/deps.txt"
self.assertTrue(os.path.exists(file))
gd = GradleDependencies()
deps = gd.import_gradle_dependencies(file)
self.assertEqual(47, len(deps))
self.print_dependencies_tree(deps, ">")
core_runtime: MavenDependency = gd.find_dependency(
deps, MavenDependency("androidx.arch.core", "core-runtime", "2.1.0")
)
self.repo.load_artifacts(core_runtime)
if len(core_runtime.get_artifacts()) < 1:
manager = Manager(self.repo)
manager.import_dep_shallow(core_runtime)
self.assertEqual(3, len(core_runtime.get_artifacts()))
core_runtime_deps = core_runtime.get_dependencies()
self.assertEqual(2, len(core_runtime_deps))
core_common = gd.find_dependency(
core_runtime_deps,
MavenDependency("androidx.arch.core", "core-common", "2.1.0"),
)
self.assertEqual(1, len(core_common.get_dependencies()))
expected_annotation = MavenDependency(
"androidx.annotation", "annotation", "1.3.0"
)
actual_annotation = core_common.get_dependencies().pop()
self.assertTrue(expected_annotation.is_keys_equal(actual_annotation))
def print_dependencies_tree(self, dependencies: [], prefix: str = ""):
for d in dependencies:
print(f"{prefix} {d}")
if len(d.get_dependencies()) > 0:
self.print_dependencies_tree(d.get_dependencies(), f">{prefix}")
def test_match_version(self):
gd = GradleDependencies()
self.assertEqual("1.3.0", gd.match_version("1.3.0"))
self.assertEqual("1.3.0", gd.match_version("1.3.0 (*)"))
self.assertEqual("1.3.0", gd.match_version("1.1.0 -> 1.3.0"))
self.assertEqual("1.3.0", gd.match_version("1.1.0 -> 1.3.0 (*)"))
self.assertEqual("1", gd.match_version("1"))
self.assertEqual("1", gd.match_version("1 (*)"))
self.assertEqual("2", gd.match_version("1 -> 2"))
self.assertEqual("2", gd.match_version("1 -> 2 (*)"))
self.assertEqual("2", gd.match_version("1 -> 2 (c)"))
self.assertEqual("1.5.0", gd.match_version("{strictly 1.5.0} -> 1.5.0 (c)"))
def test_extract_dependency(self):
gd = GradleDependencies()
self.assertEqual(
MavenDependency("androidx.appcompat", "appcompat", "1.4.1", ""),
gd.extract_dependency("+--- androidx.appcompat:appcompat:1.4.1"),
)
self.assertEqual(
MavenDependency("androidx.annotation", "annotation", "1.3.0", ""),
gd.extract_dependency("| +--- androidx.annotation:annotation:1.3.0"),
)
self.assertEqual(
MavenDependency("androidx.lifecycle", "lifecycle-runtime", "2.4.0", ""),
gd.extract_dependency(
"| | +--- androidx.lifecycle:lifecycle-runtime:2.3.1 -> 2.4.0 (*)"
),
)
self.assertEqual(
MavenDependency("androidx.lifecycle", "lifecycle-viewmodel", "2.3.1", ""),
gd.extract_dependency(
"| | \\--- androidx.lifecycle:lifecycle-viewmodel:2.3.1 (*)"
),
)
self.assertEqual(
MavenDependency("androidx.lifecycle", "lifecycle-livedata", "2.0.0", ""),
gd.extract_dependency(
"| | | +--- androidx.lifecycle:lifecycle-livedata:2.0.0"
),
)
self.assertEqual(
MavenDependency("javax.inject", "javax.inject", "1", ""),
gd.extract_dependency("| | \\--- javax.inject:javax.inject:1"),
)
self.assertEqual(
MavenDependency("com.google.dagger", "dagger", "2.41", ""),
gd.extract_dependency("| | +--- com.google.dagger:dagger:2.41 (*)"),
)
self.assertEqual(
MavenDependency("com.google.android.material", "material", "1.5.0", ""),
gd.extract_dependency(
"+--- com.google.android.material:material:{strictly 1.5.0} -> 1.5.0 (c)"
),
)
if __name__ == "__main__":
unittest.main()
# 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 logging
import os
import unittest
from dependency.buck import Buck
from dependency.gradle import GradleDependencies
from dependency.manager import Manager
from dependency.pom import MavenDependency, PomXmlAdapter
from dependency.repo import ArtifactFlavor
from tests.test_base import BaseTestCase
class TestManager(BaseTestCase):
def test_import_maven_dep(self):
gradle_dep = "com.google.dagger:hilt-android:2.41"
manager = Manager(self.repo)
pd = manager.import_maven_dep(gradle_dep)
self.assertEqual(
MavenDependency("com.google.dagger", "hilt-android", "2.41", ""), pd
)
self.assertFalse(
os.path.exists(
f"{self.repo.root}/com/google/dagger/hilt-android/2.41/hilt-android-2.41.jar"
)
)
self.assertTrue(
os.path.exists(
f"{self.repo.root}/com/google/dagger/hilt-android/2.41/hilt-android-2.41.pom"
)
)
self.assertTrue(
os.path.exists(
f"{self.repo.root}/com/google/dagger/hilt-android/2.41/hilt-android-2.41.aar"
)
)
def test_toPomDependency(self):
gradle_dep = "com.google.dagger:hilt-android:2.41"
manager = Manager(self.repo)
pd = manager.to_maven_dependency(gradle_dep)
self.assertEqual(
MavenDependency("com.google.dagger", "hilt-android", "2.41", ""), pd
)
def test_import_dep_shallow(self):
self.assertTrue(os.path.exists(self.deps_file))
gd = GradleDependencies()
deps = gd.import_gradle_dependencies(self.deps_file)
self.assertEqual(47, len(deps))
manager = Manager(self.repo)
for d in deps:
downloaded_files = manager.import_dep_shallow(d)
self.assertTrue(len(downloaded_files) > 0, f"Unable to download: {d}")
def test_import_dep(self):
gradle_dep = "com.google.dagger:hilt-android:2.41"
manager = Manager(self.repo)
pd = manager.to_maven_dependency(gradle_dep)
all_dependencies = manager.import_dep(pd)
for n in all_dependencies:
print(n)
self.assertEqual(52, len(all_dependencies))
def test_check_missing_dependencies_partial_list(self):
manager = Manager(self.repo)
pom = MavenDependency("androidx.viewpager", "viewpager", "1.0.0", "")
manager.import_dep_shallow(pom)
core = MavenDependency("androidx.core", "core", "1.0.0", "compile")
pom.add_dependency(core)
missing_deps = manager.check_missing_dependencies(pom)
self.assertEqual(2, len(missing_deps))
def test_check_missing_dependencies_empty_list(self):
manager = Manager(self.repo)
dependency = MavenDependency(
"androidx.concurrent", "concurrent-futures", "1.0.0", "runtime"
)
manager.import_dep_shallow(dependency)
missing_deps = manager.check_missing_dependencies(dependency)
logging.debug("missing dependencies:")
dependency.print_dependency_tree()
for d in missing_deps:
logging.debug(d)
self.assertEqual(2, len(missing_deps))
annotation = MavenDependency(
"androidx.annotation", "annotation", "1.1.0", "compile"
)
listenablefuture = MavenDependency(
"com.google.guava", "listenablefuture", "1.0", "compile"
)
self.assertEqual({annotation, listenablefuture}, missing_deps)
def test_import_missing_dependencies_single_dep(self):
manager = Manager(self.repo)
dependency = MavenDependency("androidx.viewpager", "viewpager", "1.0.0", "")
manager.import_dep_shallow(dependency)
versions = dict()
annotations = MavenDependency(
"androidx.annotation", "annotation", "1.3.0", "compile"
)
versions[annotations.get_group_and_artifactid()] = annotations
manager.import_missing_dependencies({dependency}, versions)
self.assertEqual(3, len(dependency.get_dependencies()))
self.assertTrue(annotations in dependency.get_dependencies())
all_dependencies = dependency.get_all_dependencies()
self.assertEqual(9, len(all_dependencies))
print("\n\n*** all dependencies")
for d in all_dependencies:
print(d)
print("\n\n*** dependencies tree")
dependency.print_dependency_tree()
print("\n\n*** versions")
for k, v in versions.items():
print(f"{k}:{v}")
self.assertEqual(9, len(versions))
def test_import_missing_dependencies_multiple_deps(self):
manager = Manager(self.repo)
dependency = MavenDependency(
"androidx.concurrent", "concurrent-futures", "1.0.0", "runtime"
)
manager.import_dep_shallow(dependency)
versions = dict()
annotations = MavenDependency(
"androidx.annotation", "annotation", "1.3.0", "compile"
)
versions[annotations.get_group_and_artifactid()] = annotations
manager.import_missing_dependencies({dependency, annotations}, versions)
self.assertEqual(2, len(dependency.get_dependencies()))
self.assertTrue(annotations in dependency.get_dependencies())
all_dependencies = dependency.get_all_dependencies()
self.assertEqual(3, len(all_dependencies))
print("\n\n*** all dependencies")
for d in all_dependencies:
print(d)
print("\n\n*** dependencies tree")
dependency.print_dependency_tree()
print("\n\n*** versions")
for k, v in versions.items():
print(f"{k}:{v}")
self.assertEqual(3, len(versions))
def test_import_missing_dependencies_updates_versions(self):
manager = Manager(self.repo)
dependency = MavenDependency("androidx.appcompat", "appcompat", "1.4.1", "")
manager.import_dep_shallow(dependency)
versions = dict()
annotations = MavenDependency(
"androidx.annotation", "annotation", "1.3.0", "compile"
)
versions[annotations.get_group_and_artifactid()] = annotations
manager.import_missing_dependencies({dependency, annotations}, versions)
self.assertEqual(14, len(dependency.get_dependencies()))
self.assertTrue(annotations in dependency.get_dependencies())
all_dependencies = dependency.get_all_dependencies()
print("\n\n*** all dependencies")
keys = set()
for d in all_dependencies:
print(d)
keys.add(d.get_group_and_artifactid())
print("\n\n*** dependencies tree")
dependency.print_dependency_tree()
self.assertEqual(34, len(all_dependencies))
print("\n\n*** versions")
for k, v in versions.items():
print(f"{k}:{v}")
self.assertEqual(set(versions.keys()), keys)
def test_import_missing_dependencies_all_gradle_deps(self):
file = "data/deps_with_missing.txt"
self.assertTrue(os.path.exists(file))
gd = GradleDependencies()
deps = gd.import_gradle_dependencies(file)
buck = Buck(self.repo)
# Remove ./libs/BUCK file if it exists
buck_file = buck.get_path()
if os.path.exists(buck_file):
os.remove(buck_file)
manager = Manager(self.repo)
versions = dict()
print("\n\n*** all dependencies")
for dep in deps:
manager.import_dep_shallow(dep)
versions[dep.get_group_and_artifactid()] = dep
print(dep)
self.assertEqual(4, len(versions))
# Check for missing dependencies
manager.import_missing_dependencies(deps, versions)
self.assertTrue("androidx.annotation_annotation-experimental" in versions)
self.assertEqual(
"androidx.annotation:annotation-experimental:1.1.0:compile",
versions["androidx.annotation_annotation-experimental"].__str__(),
)
# self.assertEqual("androidx.emoji2:emoji2:1.0.0:runtime",
# versions["androidx.emoji2_emoji2"].__str__())
print("\n\n*** versions")
for k, v in versions.items():
print(f"{k}:{v}")
packages = set()
print("\n\n*** dependencies tree")
appcompat = versions["androidx.appcompat_appcompat"]
appcompat.print_dependency_tree()
for dep in deps:
packages.add(dep)
dep_tree = dep.get_all_dependencies()
packages = packages.union(dep_tree)
# dep.print_dependency_tree()
# print("\n")
unique_package_keys = set()
for p in packages:
unique_package_keys.add(p.get_group_and_artifactid())
self.assertEqual(set(versions.keys()), unique_package_keys)
# find dup packages
for k in unique_package_keys:
dups = []
for p in packages:
if p.get_group_and_artifactid() == k:
dups.append(p)
if len(dups) > 1:
print(f"dups: {dups}")
self.assertEqual(len(versions), len(packages))
self.assertEqual(34, len(versions))
def test_import_missing_dependencies_resove_missing_versions(self):
dependency = MavenDependency("com.google.guava", "guava", "31.0.1-jre", "")
versions = dict()
manager = Manager(self.repo)
manager.import_dep_shallow(dependency)
jar_file = (
f"{self.repo.root}/com/google/guava/guava/31.0.1-jre/guava-31.0.1-jre.jar"
)
self.assertTrue(os.path.exists(jar_file))
visited = manager.import_missing_dependencies({dependency}, versions)
keys = set()
for d in visited:
keys.add(d.__str__())
self.assertSetEqual(
{
"com.google.guava:guava:31.0.1-jre:",
"com.google.guava:failureaccess:1.0.1:",
"com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava:",
"com.google.code.findbugs:jsr305:3.0.2:",
"org.checkerframework:checker-qual:3.22.0:",
"com.google.errorprone:error_prone_annotations:2.13.1:",
"com.google.j2objc:j2objc-annotations:1.3:",
},
keys,
)
def test_resolve_deps_versions(self):
dependency = MavenDependency("com.google.guava", "guava", "31.0.1-jre", "")
manager = Manager(self.repo)
manager.import_dep_shallow(dependency)
pom_file = self.repo.download_maven_dep(dependency, ArtifactFlavor.POM)
self.assertTrue(os.path.exists(pom_file))
pom_xml_adapter = PomXmlAdapter(pom_file)
pom_deps = pom_xml_adapter.get_deps()
manager.resolve_deps_versions(dependency, pom_deps)
keys = set()
for d in pom_deps:
keys.add(d.__str__())
self.assertSetEqual(
{
"com.google.guava:failureaccess:1.0.1:",
"com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava:",
"com.google.code.findbugs:jsr305:3.0.2:",
"org.checkerframework:checker-qual:3.22.0:",
"com.google.errorprone:error_prone_annotations:2.13.1:",
"com.google.j2objc:j2objc-annotations:1.3:",
},
keys,
)
if __name__ == "__main__":
unittest.main()
# 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 os
import unittest
from dependency.pom import MavenDependency, PomXmlAdapter
from dependency.repo import Repo, ArtifactFlavor
from tests.test_base import BaseTestCase
class TestPomDependency(BaseTestCase):
def test_str(self):
dependency = MavenDependency("com.google.dagger", "dagger", "2.41", "")
self.assertEqual("com.google.dagger:dagger:2.41:", dependency.__str__())
def test_get_libs_path(self):
dependency = MavenDependency("com.google.dagger", "dagger", "2.41", "")
path = dependency.get_libs_path()
self.assertEqual(["com", "google", "dagger", "dagger", "2.41"], path)
def test_parse_pom(self):
dependency = MavenDependency("com.google.dagger", "dagger", "2.41", "")
self.repo.download_maven_dep(dependency, ArtifactFlavor.POM)
file = self.repo.download_maven_dep(dependency, ArtifactFlavor.POM)
self.assertTrue(os.path.exists(file))
self.assertEqual(
f"{self.repo.get_libs_name()}/com/google/dagger/dagger/2.41/dagger-2.41.pom",
file,
)
pom_xml_adapter = PomXmlAdapter(file)
deps = pom_xml_adapter.get_deps()
self.assertEqual(1, len(deps))
self.assertEqual(
MavenDependency("javax.inject", "javax.inject", "1", "").__str__(),
deps[0].__str__(),
)
def test_resolve_pom_variables(self):
"""
Verifies that the method replaces variables in <dependency>
<groupId>${project.groupId}</groupId>
<artifactId>guava</artifactId>
<version>${project.version}</version>
with the project groupId and version: "com.google.guava:guava:31.1-jre:".
"""
dependency = MavenDependency(
"com.google.guava", "guava-testlib", "31.1-jre", ""
)
self.repo.download_maven_dep(dependency, ArtifactFlavor.POM)
file = self.repo.download_maven_dep(dependency, ArtifactFlavor.POM)
self.assertTrue(os.path.exists(file))
self.assertEqual(
f"{self.repo.get_libs_name()}/com/google/guava/guava-testlib/31.1-jre/guava-testlib-31.1-jre.pom",
file,
)
pom_xml_adapter = PomXmlAdapter(file)
deps = pom_xml_adapter.get_deps()
keys = set()
for d in deps:
s = d.__str__()
print(f"dep: {s}")
keys.add(s)
self.assertSetEqual(
{
"com.google.code.findbugs:jsr305:None:",
"org.checkerframework:checker-qual:None:",
"com.google.errorprone:error_prone_annotations:None:",
"com.google.j2objc:j2objc-annotations:None:",
"com.google.guava:guava:31.1-jre:",
"junit:junit:None:compile",
# "com.google.truth:javax.truth:None:test", - the test is excluded for now
},
keys,
)
def test_resolve_pom_properties(self):
dependency = MavenDependency("junit", "junit", "4.13.2", "")
self.repo.download_maven_dep(dependency, ArtifactFlavor.POM)
file = self.repo.download_maven_dep(dependency, ArtifactFlavor.POM)
self.assertTrue(os.path.exists(file))
self.assertEqual(
f"{self.repo.get_libs_name()}/junit/junit/4.13.2/junit-4.13.2.pom",
file,
)
pom_xml_adapter = PomXmlAdapter(file)
deps = pom_xml_adapter.get_deps()
keys = set()
for d in deps:
s = d.__str__()
print(f"dep: {s}")
keys.add(s)
self.assertSetEqual(
{
"org.hamcrest:hamcrest-core:1.3:",
# TODO: check about the test scope
},
keys,
)
def test_resolve_pom_complex_properties(self):
dependency = MavenDependency("com.squareup", "javapoet", "1.13.0", "")
self.repo.download_maven_dep(dependency, ArtifactFlavor.POM)
file = self.repo.download_maven_dep(dependency, ArtifactFlavor.POM)
self.assertTrue(os.path.exists(file))
self.assertEqual(
f"{self.repo.get_libs_name()}/com/squareup/javapoet/1.13.0/javapoet-1.13.0.pom",
file,
)
pom_xml_adapter = PomXmlAdapter(file)
deps = pom_xml_adapter.get_deps()
self.assertEqual(0, len(deps))
# TODO: update for "test" scope
def test_get_deps(self):
dependency = MavenDependency("com.google.dagger", "hilt-core", "2.41", "")
self.repo.download_maven_dep(dependency, ArtifactFlavor.POM)
pom_file = f"{self.repo.get_libs_name()}/com/google/dagger/hilt-core/2.41/hilt-core-2.41.pom"
self.assertTrue(os.path.exists(pom_file))
pa = PomXmlAdapter(pom_file)
deps = pa.get_deps()
self.assertEqual(3, len(deps))
keys = set()
for d in deps:
keys.add(d.__str__())
self.assertSetEqual(
{
"com.google.dagger:dagger:2.41:",
"com.google.code.findbugs:jsr305:3.0.2:",
"javax.inject:javax.inject:1:",
},
keys,
)
def test_add_dependency_version_upgrade(self):
dependency = MavenDependency("com.google.dagger", "hilt-core", "2.41", "")
d1 = MavenDependency("androidx.core", "core", "1.7.0", "")
d2 = MavenDependency("androidx.core", "core", "1.1.0", "")
d3 = MavenDependency("androidx.core", "core", "1.8.0", "")
dependency.add_dependency(d1)
dependency.add_dependency(d2)
dependency.add_dependency(d3)
deps = dependency.get_dependencies()
self.assertEqual(1, len(deps))
self.assertEqual(deps.pop(), d3)
def test_add_dependency_excludes_cycles(self):
dependency = MavenDependency("com.google.dagger", "hilt-core", "2.41", "")
d1 = MavenDependency("androidx.core", "core", "1.7.0", "")
d2 = MavenDependency("androidx.core", "core", "1.1.0", "")
d3 = MavenDependency("com.google.dagger", "hilt-core", "2.41", "")
dependency.add_dependency(d1)
dependency.add_dependency(d2)
dependency.add_dependency(d3)
deps = dependency.get_dependencies()
self.assertEqual(1, len(deps))
self.assertEqual(deps.pop(), d1)
if __name__ == "__main__":
unittest.main()
# 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 os
import time
import unittest
from dependency.pom import MavenDependency
from dependency.repo import Repo, ArtifactFlavor
from tests.test_base import BaseTestCase
class TestRepo(BaseTestCase):
def test_mkdirs(self):
dirs = ["one", "two", "three"]
self.assertTrue(self.repo.mkdirs(dirs))
timeout = time.time() + 5
while time.time() < timeout:
if os.path.exists("../libs/one/two/three"):
break
time.sleep(1)
self.assertTrue(os.path.exists(f"{self.repo.root}/one"))
self.assertTrue(os.path.exists(f"{self.repo.root}/one/two"))
self.assertTrue(os.path.exists(f"{self.repo.root}/one/two/three"))
os.rmdir(f"{self.repo.root}/one/two/three")
os.rmdir(f"{self.repo.root}/one/two")
os.rmdir(f"{self.repo.root}/one")
def test_get_dependency_dir(self):
pom = MavenDependency("com.google.dagger", "dagger", "2.41", "")
path = self.repo.get_dependency_dir(pom)
self.assertEqual(
f"{self.repo.get_libs_name()}/com/google/dagger/dagger/2.41", path.__str__()
)
def test_get_dependency_path(self):
pom = MavenDependency("com.google.dagger", "dagger", "2.41", "")
path = self.repo.get_dependency_path(pom, ArtifactFlavor.JAR)
self.assertEqual(
f"{self.repo.get_libs_name()}/com/google/dagger/dagger/2.41/dagger-2.41.jar",
path.__str__(),
)
def test_get_base_url(self):
pom = MavenDependency("com.google.dagger", "dagger", "2.41", "")
url = self.repo.get_base_url(pom)
self.assertEqual(
"https://repo1.maven.org/maven2/com/google/dagger/dagger/2.41/dagger-2.41",
url,
)
def test_get_url(self):
pom = MavenDependency("com.google.dagger", "dagger", "2.41", "")
url = self.repo.get_url(pom, ArtifactFlavor.JAR)
self.assertEqual(
"https://repo1.maven.org/maven2/com/google/dagger/dagger/2.41/dagger-2.41.jar",
url,
)
url = self.repo.get_url(pom, ArtifactFlavor.POM)
self.assertEqual(
"https://repo1.maven.org/maven2/com/google/dagger/dagger/2.41/dagger-2.41.pom",
url,
)
def test_download_maven_dep(self):
pom = MavenDependency("com.google.dagger", "dagger", "2.41", "")
self.repo.download_maven_dep(pom, ArtifactFlavor.JAR)
self.assertTrue(
os.path.exists(
f"{self.repo.get_libs_name()}/com/google/dagger/dagger/2.41/dagger-2.41.jar"
)
)
file = self.repo.download_maven_dep(pom, ArtifactFlavor.POM)
self.assertTrue(os.path.exists(file))
self.assertEqual(
f"{self.repo.get_libs_name()}/com/google/dagger/dagger/2.41/dagger-2.41.pom",
file,
)
self.repo.download_maven_dep(pom, ArtifactFlavor.AAR)
self.assertFalse(
os.path.exists(
f"{self.repo.get_libs_name()}/com/google/dagger/dagger/2.41/dagger-2.41.aar"
)
)
def test_get_release_version(self):
dep = MavenDependency("com.google.dagger", "dagger", "2.41", "")
result = self.repo.get_release_version(dep)
self.assertTrue(
os.path.exists(
f"{self.repo.get_libs_name()}/com/google/dagger/dagger/maven-metadata.xml"
)
)
self.assertEqual("2.42", result)
if __name__ == "__main__":
unittest.main()
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment