-
Notifications
You must be signed in to change notification settings - Fork 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
1fa8efc
commit f93c36f
Showing
2 changed files
with
271 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,226 @@ | ||
#!/usr/bin/env python3 | ||
|
||
__license__ = 'MIT' | ||
|
||
import argparse | ||
import hashlib | ||
import json | ||
import logging | ||
import shutil | ||
import sys | ||
import tempfile | ||
from typing import List, Dict, Optional | ||
import urllib.request | ||
import xml.etree.ElementTree as ET | ||
|
||
parser = argparse.ArgumentParser() | ||
parser.add_argument('packages', nargs='*') | ||
parser.add_argument('--output', '-o', | ||
help='Specify output file name', default="maven-sources.json") | ||
parser.add_argument('--repo', '-r', action="append") | ||
parser.add_argument('--verbose', '-v', action='store_true') | ||
|
||
def assembleUri(repo: str, groupId: str, artifactId: str, version: str, extension: str) -> str: | ||
groupId = groupId.replace(".", "/") | ||
return f'{repo}{groupId}/{artifactId}/{version}/{artifactId}-{version}.{extension}' | ||
|
||
def getFileHash(file) -> str: | ||
file.seek(0) | ||
byteData = file.read() # read entire file as bytes | ||
return hashlib.sha256(byteData).hexdigest() | ||
|
||
def parsePomDeps(parsed_pom) -> List[Dict[str, str]]: | ||
ns = {'POM': 'http://maven.apache.org/POM/4.0.0'} | ||
|
||
result = [] | ||
|
||
parent = parsed_pom.find("POM:parent", ns) | ||
if parent is not None: | ||
groupId = parent.find("POM:groupId", ns).text | ||
artifactId = parent.find("POM:artifactId", ns).text | ||
version = parent.find("POM:version", ns).text | ||
|
||
result.append({ | ||
"groupId": groupId, | ||
"artifactId": artifactId, | ||
"version": version | ||
}) | ||
|
||
deps = parsed_pom.findall("POM:dependencies/POM:dependency", ns) | ||
for dep in deps: | ||
groupId = dep.find("POM:groupId", ns).text | ||
artifactId = dep.find("POM:artifactId", ns).text | ||
version = dep.find("POM:version", ns) | ||
|
||
if(version is None): | ||
continue | ||
|
||
version = version.text | ||
|
||
result.append({ | ||
"groupId": groupId, | ||
"artifactId": artifactId, | ||
"version": version | ||
}) | ||
|
||
return result | ||
|
||
def getPackagingType(parsed_pom) -> Optional[str]: | ||
ns = {'POM': 'http://maven.apache.org/POM/4.0.0'} | ||
|
||
packaging = parsed_pom.find("POM:packaging", ns) | ||
if packaging is not None and packaging.text == "pom": | ||
return None # Nothing to download for this | ||
|
||
if packaging is None: | ||
# jar is default if nothing is specified | ||
return "jar" | ||
|
||
return packaging.text | ||
|
||
modules = [] | ||
addedModules = [] | ||
|
||
def addModule(groupId: str, artifactId: str, version: str): | ||
addedModules.append({ | ||
"groupId": groupId, | ||
"artifactId": artifactId, | ||
"version": version | ||
}) | ||
|
||
def downloadAndAdd(repo: str, groupId: str, artifactId: str, version: str, binaryType: str) -> bool: | ||
url = assembleUri(repo, groupId, artifactId, version, binaryType) | ||
groupId = groupId.replace(".", "/") | ||
try: | ||
with urllib.request.urlopen(url) as response: | ||
with tempfile.NamedTemporaryFile(delete=True) as tmp_file: | ||
shutil.copyfileobj(response, tmp_file) | ||
|
||
modules.append({ | ||
"type": "file", | ||
"url": url, | ||
"sha256": getFileHash(tmp_file), | ||
"dest": f"maven-local/{groupId}/{artifactId}/{version}" | ||
}) | ||
|
||
return True | ||
except urllib.error.HTTPError: | ||
logging.warning("Unable to download %s file for %s", binaryType, artifactId) | ||
return False | ||
|
||
def parseGradleMetadata(repo: str, groupId: str, artifactId: str, version: str) -> bool: | ||
url = assembleUri(repo, groupId, artifactId, version, "module") | ||
groupId = groupId.replace(".", "/") | ||
try: | ||
with urllib.request.urlopen(url) as response: | ||
with tempfile.NamedTemporaryFile(delete=True) as tmp_file: | ||
shutil.copyfileobj(response, tmp_file) | ||
|
||
tmp_file.seek(0) | ||
gradle_meta = json.loads(tmp_file.read().decode('utf-8')) | ||
for variant in gradle_meta["variants"]: | ||
if "files" not in variant: | ||
continue | ||
for file in variant["files"]: | ||
modules.append({ | ||
"type": "file", | ||
"url": f'{repo}{groupId}/{artifactId}/{version}/{file["url"]}', | ||
"sha256": file["sha256"], | ||
"dest": f'maven-local/{groupId}/{artifactId}/{version}' | ||
}) | ||
|
||
modules.append({ | ||
"type": "file", | ||
"url": url, | ||
"sha256": getFileHash(tmp_file), | ||
"dest": f"maven-local/{groupId}/{artifactId}/{version}" | ||
}) | ||
|
||
return True | ||
except urllib.error.HTTPError: | ||
logging.warning("Unable to get the extended Gradle module metadata for %s", artifactId) | ||
return False | ||
|
||
def parsePomTree(repos: list[str], groupId: str, artifactId: str, version: str) -> bool: | ||
for module in addedModules: | ||
if module["groupId"] == groupId and module["artifactId"] == artifactId and module["version"] == version: | ||
return True | ||
|
||
for repo in repos: | ||
url = assembleUri(repo, groupId, artifactId, version, "pom") | ||
|
||
try: | ||
logging.debug("Looking for %s:%s at %s", artifactId, version, url) | ||
with urllib.request.urlopen(url) as response: | ||
with tempfile.NamedTemporaryFile(delete=True) as tmp_file: | ||
addModule(groupId, artifactId, version) | ||
|
||
shutil.copyfileobj(response, tmp_file) | ||
tmp_file.seek(0) | ||
file_content = tmp_file.read().decode('utf-8') | ||
parsed_file = ET.fromstring(file_content) | ||
|
||
if (binaryType := getPackagingType(parsed_file)) is not None: | ||
downloadAndAdd(repo, groupId, artifactId, version, binaryType) | ||
|
||
if("do_not_remove: published-with-gradle-metadata" in file_content): | ||
# This module has extended Gradle metadata, download that (and its dependencies) | ||
parseGradleMetadata(repo, groupId, artifactId, version) | ||
|
||
deps = parsePomDeps(parsed_file) | ||
for dep in deps: | ||
parsePomTree(repos, dep["groupId"], dep["artifactId"], dep["version"]) | ||
|
||
groupId = groupId.replace(".", "/") | ||
modules.append({ | ||
"type": "file", | ||
"url": url, | ||
"sha256": getFileHash(tmp_file), | ||
"dest": f"maven-local/{groupId}/{artifactId}/{version}" | ||
}) | ||
|
||
return True | ||
|
||
except urllib.error.HTTPError: | ||
pass | ||
|
||
logging.warning("%s:%s not found in any source", artifactId, version) | ||
return False | ||
|
||
def main(): | ||
opts = parser.parse_args() | ||
|
||
repos = [] | ||
if(opts.repo is not None): | ||
repos.extend(opts.repo) | ||
else: | ||
repos.append("https://repo.maven.apache.org/maven2/") | ||
|
||
if len(opts.packages) < 1: | ||
parser.print_help() | ||
sys.exit(1) | ||
|
||
if opts.verbose: | ||
loglevel = logging.DEBUG | ||
else: | ||
loglevel = logging.INFO | ||
logging.basicConfig(level=loglevel) | ||
|
||
for package in opts.packages: | ||
package_parts = package.split(":") | ||
if len(package_parts) != 3: | ||
print("Package names must be in the format groupId:artifactId:version") | ||
sys.exit(1) | ||
|
||
groupId = package_parts[0] | ||
artifactId = package_parts[1] | ||
version = package_parts[2] | ||
|
||
parsePomTree(repos, groupId, artifactId, version) | ||
|
||
with open(opts.output, 'w') as output: | ||
output.write(json.dumps(modules, indent=4)) | ||
logging.info('Output saved to %s', opts.output) | ||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# Flatpak Maven Generator | ||
|
||
Tool to automatically generate `flatpak-builder` manifest json from maven artifact names. | ||
|
||
## Usage | ||
|
||
`flatpak-maven-generator groupId:artifactId:version` which generates `maven-sources.json` and can be included in a manifest like: | ||
|
||
```json | ||
"sources": [ | ||
"maven-sources.json" | ||
] | ||
``` | ||
|
||
You can also list multiple space separated artifacts in single command, for example: | ||
``` | ||
flatpak-maven-generator org.foo.bar:artifact1:1.0.0 org.foo.baz:artifact2:1.5.21 | ||
``` | ||
|
||
By default, artifacts are looked up on [Maven Central](https://search.maven.org/), but different or additional repositories can be specified with the `-r`/`--repo` flag. For example: | ||
``` | ||
flatpak-maven-generator --repo https://plugins.gradle.org/m2/ --repo https://repo.maven.apache.org/maven2/ org.foo.bar:artifact1:1.0.0 | ||
``` | ||
|
||
Repositories will be searched in the order they are specified on the command line. | ||
|
||
When included in a manifest, the JSON file will instruct `flatpak-builder` to download the necessary files to mirror the requested artifacts (and their recursive dependencies) into a local maven repository. This is created in a folder called `maven-local`, so the build configuration of the software being built will have to be modified to search for its dependencies there. For example, in a `build.gradle.kts` file, you would add the following: | ||
``` | ||
allprojects { | ||
repositories { | ||
maven(url = "./maven-local") | ||
} | ||
} | ||
``` | ||
|
||
If you are intending to mirror Gradle plugins inside your Flatpak build sandbox, you may additionally have to specify the following in `settings.gradle.kts` (or equivalent): | ||
``` | ||
pluginManagement { | ||
repositories { | ||
maven(url = "./maven-local") | ||
} | ||
} | ||
``` | ||
|
||
If you are building multiple modules that all depend on the local maven mirror, you may wish to move the mirrored `maven-local` folder to `$FLATPAK_DEST`, where it can be shared between modules and then cleaned up afterwards. |