scanner: support libs.versions.toml

This commit is contained in:
linsui 2024-09-15 02:03:26 +08:00
parent 528760acc8
commit eff0ef48f4
17 changed files with 723 additions and 35 deletions

View file

@ -741,6 +741,11 @@ include tests/signindex/guardianproject-v1.jar
include tests/signindex/testy.jar include tests/signindex/testy.jar
include tests/signindex/unsigned.jar include tests/signindex/unsigned.jar
include tests/source-files/at.bitfire.davdroid/build.gradle include tests/source-files/at.bitfire.davdroid/build.gradle
include tests/source-files/catalog.test/app/build.gradle
include tests/source-files/catalog.test/build.gradle.kts
include tests/source-files/catalog.test/libs.versions.toml
include tests/source-files/catalog.test/gradle/libs.versions.toml
include tests/source-files/catalog.test/settings.gradle.kts
include tests/source-files/cn.wildfirechat.chat/avenginekit/build.gradle include tests/source-files/cn.wildfirechat.chat/avenginekit/build.gradle
include tests/source-files/cn.wildfirechat.chat/build.gradle include tests/source-files/cn.wildfirechat.chat/build.gradle
include tests/source-files/cn.wildfirechat.chat/chat/build.gradle include tests/source-files/cn.wildfirechat.chat/chat/build.gradle
@ -775,6 +780,11 @@ include tests/source-files/com.integreight.onesheeld/settings.gradle
include tests/source-files/com.jens.automation2/build.gradle include tests/source-files/com.jens.automation2/build.gradle
include tests/source-files/com.jens.automation2/app/build.gradle include tests/source-files/com.jens.automation2/app/build.gradle
include tests/source-files/com.kunzisoft.testcase/build.gradle include tests/source-files/com.kunzisoft.testcase/build.gradle
include tests/source-files/com.lolo.io.onelist/app/build.gradle.kts
include tests/source-files/com.lolo.io.onelist/build.gradle.kts
include tests/source-files/com.lolo.io.onelist/gradle/libs.versions.toml
include tests/source-files/com.lolo.io.onelist/gradle/wrapper/gradle-wrapper.properties
include tests/source-files/com.lolo.io.onelist/settings.gradle
include tests/source-files/com.nextcloud.client/build.gradle include tests/source-files/com.nextcloud.client/build.gradle
include tests/source-files/com.nextcloud.client.dev/src/generic/fastlane/metadata/android/en-US/full_description.txt include tests/source-files/com.nextcloud.client.dev/src/generic/fastlane/metadata/android/en-US/full_description.txt
include tests/source-files/com.nextcloud.client.dev/src/generic/fastlane/metadata/android/en-US/short_description.txt include tests/source-files/com.nextcloud.client.dev/src/generic/fastlane/metadata/android/en-US/short_description.txt
@ -817,6 +827,9 @@ include tests/source-files/org.mozilla.rocket/app/build.gradle
include tests/source-files/org.noise_planet.noisecapture/app/build.gradle include tests/source-files/org.noise_planet.noisecapture/app/build.gradle
include tests/source-files/org.noise_planet.noisecapture/settings.gradle include tests/source-files/org.noise_planet.noisecapture/settings.gradle
include tests/source-files/org.noise_planet.noisecapture/sosfilter/build.gradle include tests/source-files/org.noise_planet.noisecapture/sosfilter/build.gradle
include tests/source-files/org.piepmeyer.gauguin/build.gradle.kts
include tests/source-files/org.piepmeyer.gauguin/libs.versions.toml
include tests/source-files/org.piepmeyer.gauguin/settings.gradle.kts
include tests/source-files/org.tasks/app/build.gradle.kts include tests/source-files/org.tasks/app/build.gradle.kts
include tests/source-files/org.tasks/build.gradle include tests/source-files/org.tasks/build.gradle
include tests/source-files/org.tasks/build.gradle.kts include tests/source-files/org.tasks/build.gradle.kts

View file

@ -34,6 +34,11 @@ from enum import IntEnum
from pathlib import Path from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
from . import _, common, metadata, scanner from . import _, common, metadata, scanner
from .exception import BuildException, ConfigurationException, VCSException from .exception import BuildException, ConfigurationException, VCSException
@ -58,13 +63,177 @@ DEPFILE = {
SCANNER_CACHE_VERSION = 1 SCANNER_CACHE_VERSION = 1
DEFAULT_CATALOG_PREFIX_REGEX = re.compile(
r'''defaultLibrariesExtensionName\s*=\s*['"](\w+)['"]'''
)
GRADLE_KTS_CATALOG_FILE_REGEX = re.compile(
r'''create\("(\w+)"\)\s*\{[^}]*from\(files\(['"]([^"]+)['"]\)\)'''
)
GRADLE_CATALOG_FILE_REGEX = re.compile(
r'''(\w+)\s*\{[^}]*from\(files\(['"]([^"]+)['"]\)\)'''
)
VERSION_CATALOG_REGEX = re.compile(
r'dependencyResolutionManagement\s*\{[^}]*versionCatalogs\s*\{'
)
class ExitCode(IntEnum): class ExitCode(IntEnum):
NONFREE_CODE = 1 NONFREE_CODE = 1
class GradleVersionCatalog:
"""Parse catalog from libs.versions.toml.
https://docs.gradle.org/current/userguide/platforms.html
"""
def __init__(self, catalog):
self.version = {
alias: self.get_version(version)
for alias, version in catalog.get("versions", {}).items()
}
self.libraries = {
self.alias_to_accessor(alias): self.library_to_coordinate(library)
for alias, library in catalog.get("libraries", {}).items()
}
self.plugins = {
self.alias_to_accessor(alias): self.plugin_to_coordinate(plugin)
for alias, plugin in catalog.get("plugins", {}).items()
}
self.bundles = {
self.alias_to_accessor(alias): self.bundle_to_coordinates(bundle)
for alias, bundle in catalog.get("bundles", {}).items()
}
@staticmethod
def alias_to_accessor(alias: str) -> str:
"""Covert alias to accessor.
https://docs.gradle.org/current/userguide/platforms.html#sub:mapping-aliases-to-accessors
Alias is used to define a lib in catalog. Accessor is used to access it.
"""
return alias.replace("-", ".").replace("_", ".")
def get_version(self, version: dict) -> str:
if isinstance(version, str):
return version
ref = version.get("ref")
if ref:
return self.version.get(ref, "")
return (
version.get("prefer", "")
or version.get("require", "")
or version.get("strictly", "")
)
def library_to_coordinate(self, library: dict) -> str:
"""Generate the Gradle dependency coordinate from catalog."""
module = library.get("module")
if not module:
group = library.get("group")
name = library.get("name")
if group and name:
module = f"{group}:{name}"
else:
return ""
version = library.get("version")
if version:
return f"{module}:{self.get_version(version)}"
else:
return module
def plugin_to_coordinate(self, plugin: dict) -> str:
"""Generate the Gradle plugin coordinate from catalog."""
id = plugin.get("id")
if not id:
return ""
version = plugin.get("version")
if version:
return f"{id}:{self.get_version(version)}"
else:
return id
def bundle_to_coordinates(self, bundle) -> list[str]:
"""Generate the Gradle dependency bundle coordinate from catalog."""
coordinates = []
for alias in bundle:
library = self.libraries.get(self.alias_to_accessor(alias))
if library:
coordinates.append(library)
return coordinates
def get_coordinate(self, accessor: str) -> list[str]:
"""Get the Gradle coordinate from the catalog with an accessor."""
if accessor.startswith("plugins."):
return [self.plugins.get(accessor[8:], "")]
if accessor.startswith("bundles."):
return self.bundles.get(accessor[8:], [])
return [self.libraries.get(accessor, "")]
def get_catalogs(root: str) -> dict[str, GradleVersionCatalog]:
"""Get all Gradle dependency catalogs from settings.gradle[.kts].
Returns a dict with the extension and the corresponding catalog.
The extension is used as the prefix of the accessor to access libs in the catalog.
"""
root = Path(root)
catalogs = {}
default_prefix = "libs"
catalog_files_m = []
def find_block_end(s, start):
pat = re.compile("[{}]")
depth = 1
for m in pat.finditer(s, pos=start):
if m.group() == "{":
depth += 1
else:
depth -= 1
if depth == 0:
return m.start()
else:
return -1
groovy_file = root / "settings.gradle"
kotlin_file = root / "settings.gradle.kts"
if groovy_file.is_file():
s = groovy_file.read_text(encoding="utf-8")
version_catalogs_m = VERSION_CATALOG_REGEX.search(s)
if version_catalogs_m:
start = version_catalogs_m.end()
end = find_block_end(s, start)
catalog_files_m = GRADLE_CATALOG_FILE_REGEX.finditer(s, start, end)
elif kotlin_file.is_file():
s = kotlin_file.read_text(encoding="utf-8")
version_catalogs_m = VERSION_CATALOG_REGEX.search(s)
if version_catalogs_m:
start = version_catalogs_m.end()
end = find_block_end(s, start)
catalog_files_m = GRADLE_KTS_CATALOG_FILE_REGEX.finditer(s, start, end)
else:
return {}
m_default = DEFAULT_CATALOG_PREFIX_REGEX.search(s)
if m_default:
default_prefix = m_default.group(1)
default_catalog_file = Path(root) / "gradle/libs.versions.toml"
if default_catalog_file.is_file():
with default_catalog_file.open("rb") as f:
catalogs[default_prefix] = GradleVersionCatalog(tomllib.load(f))
for m in catalog_files_m:
catalog_file = Path(root) / m.group(2).replace("$rootDir/", "")
if catalog_file.is_file():
with catalog_file.open("rb") as f:
catalogs[m.group(1)] = GradleVersionCatalog(tomllib.load(f))
return catalogs
def get_gradle_compile_commands(build): def get_gradle_compile_commands(build):
compileCommands = [ compileCommands = [
'alias',
'api', 'api',
'apk', 'apk',
'classpath', 'classpath',
@ -80,15 +249,25 @@ def get_gradle_compile_commands(build):
if build.gradle and build.gradle != ['yes']: if build.gradle and build.gradle != ['yes']:
flavors += build.gradle flavors += build.gradle
commands = [ return [''.join(c) for c in itertools.product(flavors, buildTypes, compileCommands)]
''.join(c) for c in itertools.product(flavors, buildTypes, compileCommands)
def get_gradle_compile_commands_without_catalog(build):
return [
re.compile(rf'''\s*{c}.*\s*\(?['"].*['"]''', re.IGNORECASE)
for c in get_gradle_compile_commands(build)
]
def get_gradle_compile_commands_with_catalog(build, prefix):
return [
re.compile(rf'\s*{c}.*\s*\(?{prefix}\.([a-z0-9.]+)', re.IGNORECASE)
for c in get_gradle_compile_commands(build)
] ]
return [re.compile(r'\s*' + c, re.IGNORECASE) for c in commands]
def get_embedded_classes(apkfile, depth=0): def get_embedded_classes(apkfile, depth=0):
""" """Get the list of Java classes embedded into all DEX files.
Get the list of Java classes embedded into all DEX files.
:return: set of Java classes names as string :return: set of Java classes names as string
""" """
@ -183,8 +362,7 @@ class SignatureDataController:
raise SignatureDataVersionMismatchException() raise SignatureDataVersionMismatchException()
def check_last_updated(self): def check_last_updated(self):
""" """Check if the last_updated value is ok and raise an exception if expired or inaccessible.
Check if the last_updated value is ok and raise an exception if expired or inaccessible.
:raises SignatureDataMalformedException: when timestamp value is :raises SignatureDataMalformedException: when timestamp value is
inaccessible or not parse-able inaccessible or not parse-able
@ -259,8 +437,7 @@ class SignatureDataController:
logging.debug("write '{}' to cache".format(self.filename)) logging.debug("write '{}' to cache".format(self.filename))
def verify_data(self): def verify_data(self):
""" """Clean and validate `self.data`.
Clean and validate `self.data`.
Right now this function does just a basic key sanitation. Right now this function does just a basic key sanitation.
""" """
@ -451,8 +628,7 @@ _SCANNER_TOOL = None
def _get_tool(): def _get_tool():
""" """Lazy loading function for getting a ScannerTool instance.
Lazy loading function for getting a ScannerTool instance.
ScannerTool initialization need to access `common.config` values. Those are only available after initialization through `common.read_config()`. So this factory assumes config was called at an erlier point in time. ScannerTool initialization need to access `common.config` values. Those are only available after initialization through `common.read_config()`. So this factory assumes config was called at an erlier point in time.
""" """
@ -496,6 +672,7 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
Returns Returns
------- -------
the number of fatal problems encountered. the number of fatal problems encountered.
""" """
count = 0 count = 0
@ -571,6 +748,7 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
Returns Returns
------- -------
0 as we explicitly ignore the file, so don't count an error 0 as we explicitly ignore the file, so don't count an error
""" """
msg = 'Ignoring %s at %s' % (what, path_in_build_dir) msg = 'Ignoring %s at %s' % (what, path_in_build_dir)
logging.info(msg) logging.info(msg)
@ -593,6 +771,7 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
Returns Returns
------- -------
0 as we deleted the offending file 0 as we deleted the offending file
""" """
msg = 'Removing %s at %s' % (what, path_in_build_dir) msg = 'Removing %s at %s' % (what, path_in_build_dir)
logging.info(msg) logging.info(msg)
@ -620,6 +799,7 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
Returns Returns
------- -------
0, as warnings don't count as errors 0, as warnings don't count as errors
""" """
if toignore(path_in_build_dir): if toignore(path_in_build_dir):
return 0 return 0
@ -645,6 +825,7 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
Returns Returns
------- -------
0 if the problem was ignored/deleted/is only a warning, 1 otherwise 0 if the problem was ignored/deleted/is only a warning, 1 otherwise
""" """
options = common.get_options() options = common.get_options()
if toignore(path_in_build_dir): if toignore(path_in_build_dir):
@ -691,11 +872,21 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
return True return True
return False return False
gradle_compile_commands = get_gradle_compile_commands(build) def is_used_by_gradle_without_catalog(line):
return any(
command.match(line)
for command in get_gradle_compile_commands_without_catalog(build)
)
def is_used_by_gradle(line): def is_used_by_gradle_with_catalog(line, prefix):
return any(command.match(line) for command in gradle_compile_commands) for m in (
command.match(line)
for command in get_gradle_compile_commands_with_catalog(build, prefix)
):
if m:
return m
catalogs = {}
# Iterate through all files in the source code # Iterate through all files in the source code
for root, dirs, files in os.walk(build_dir, topdown=True): for root, dirs, files in os.walk(build_dir, topdown=True):
# It's topdown, so checking the basename is enough # It's topdown, so checking the basename is enough
@ -703,6 +894,9 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
if ignoredir in dirs: if ignoredir in dirs:
dirs.remove(ignoredir) dirs.remove(ignoredir)
if "settings.gradle" in files or "settings.gradle.kts" in files:
catalogs = get_catalogs(root)
for curfile in files: for curfile in files:
if curfile in ['.DS_Store']: if curfile in ['.DS_Store']:
continue continue
@ -789,10 +983,24 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
with open(filepath, 'r', errors='replace') as f: with open(filepath, 'r', errors='replace') as f:
lines = f.readlines() lines = f.readlines()
for i, line in enumerate(lines): for i, line in enumerate(lines):
if is_used_by_gradle(line): if is_used_by_gradle_without_catalog(line):
for name in suspects_found(line): for name in suspects_found(line):
count += handleproblem( count += handleproblem(
"usual suspect '%s'" % (name), f"usual suspect '{name}'",
path_in_build_dir,
filepath,
json_per_build,
)
for prefix, catalog in catalogs.items():
m = is_used_by_gradle_with_catalog(line, prefix)
if not m:
continue
accessor = m[1]
coordinates = catalog.get_coordinate(accessor)
for coordinate in coordinates:
for name in suspects_found(coordinate):
count += handleproblem(
f"usual suspect '{prefix}.{accessor}: {name}'",
path_in_build_dir, path_in_build_dir,
filepath, filepath,
json_per_build, json_per_build,

View file

@ -4,8 +4,7 @@ import re
import subprocess import subprocess
import sys import sys
from setuptools import Command from setuptools import Command, setup
from setuptools import setup
from setuptools.command.install import install from setuptools.command.install import install
@ -108,6 +107,7 @@ setup(
'requests >= 2.5.2, != 2.11.0, != 2.12.2, != 2.18.0', 'requests >= 2.5.2, != 2.11.0, != 2.12.2, != 2.18.0',
'sdkmanager >= 0.6.4', 'sdkmanager >= 0.6.4',
'yamllint', 'yamllint',
'tomli >= 1.1.0; python_version < "3.11"',
], ],
# Some requires are only needed for very limited cases: # Some requires are only needed for very limited cases:
# * biplist is only used for parsing Apple .ipa files # * biplist is only used for parsing Apple .ipa files

View file

@ -18,6 +18,10 @@ from dataclasses import asdict
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest import mock from unittest import mock
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
import yaml import yaml
localmodule = os.path.realpath( localmodule = os.path.realpath(
@ -70,6 +74,8 @@ class ScannerTest(unittest.TestCase):
'realm': 1, 'realm': 1,
'se.manyver': 3, 'se.manyver': 3,
'lockfile.test': 1, 'lockfile.test': 1,
'com.lolo.io.onelist': 6,
'catalog.test': 10,
} }
for d in glob.glob(os.path.join(source_files, '*')): for d in glob.glob(os.path.join(source_files, '*')):
build = fdroidserver.metadata.Build() build = fdroidserver.metadata.Build()
@ -79,26 +85,28 @@ class ScannerTest(unittest.TestCase):
should, fatal_problems, "%s should have %d errors!" % (d, should) should, fatal_problems, "%s should have %d errors!" % (d, should)
) )
def test_get_gradle_compile_commands(self): def test_get_gradle_compile_commands_without_catalog(self):
test_files = [ test_files = [
('source-files/fdroid/fdroidclient/build.gradle', 'yes', 18), ('source-files/fdroid/fdroidclient/build.gradle', 'yes', 15),
('source-files/com.nextcloud.client/build.gradle', 'generic', 28), ('source-files/com.nextcloud.client/build.gradle', 'generic', 24),
('source-files/com.kunzisoft.testcase/build.gradle', 'libre', 4), ('source-files/com.kunzisoft.testcase/build.gradle', 'libre', 3),
('source-files/cn.wildfirechat.chat/chat/build.gradle', 'yes', 33), ('source-files/cn.wildfirechat.chat/chat/build.gradle', 'yes', 30),
('source-files/org.tasks/app/build.gradle.kts', 'generic', 43), ('source-files/org.tasks/app/build.gradle.kts', 'generic', 41),
('source-files/at.bitfire.davdroid/build.gradle', 'standard', 16), ('source-files/at.bitfire.davdroid/build.gradle', 'standard', 15),
('source-files/se.manyver/android/app/build.gradle', 'indie', 29), ('source-files/se.manyver/android/app/build.gradle', 'indie', 26),
('source-files/osmandapp/osmand/build.gradle', 'free', 5), ('source-files/osmandapp/osmand/build.gradle', 'free', 2),
('source-files/eu.siacs.conversations/build.gradle', 'free', 24), ('source-files/eu.siacs.conversations/build.gradle', 'free', 21),
('source-files/org.mozilla.rocket/app/build.gradle', 'focus', 42), ('source-files/org.mozilla.rocket/app/build.gradle', 'focus', 40),
('source-files/com.jens.automation2/app/build.gradle', 'fdroidFlavor', 9), ('source-files/com.jens.automation2/app/build.gradle', 'fdroidFlavor', 5),
] ]
for f, flavor, count in test_files: for f, flavor, count in test_files:
i = 0 i = 0
build = fdroidserver.metadata.Build() build = fdroidserver.metadata.Build()
build.gradle = [flavor] build.gradle = [flavor]
regexs = fdroidserver.scanner.get_gradle_compile_commands(build) regexs = fdroidserver.scanner.get_gradle_compile_commands_without_catalog(
build
)
with open(f, encoding='utf-8') as fp: with open(f, encoding='utf-8') as fp:
for line in fp.readlines(): for line in fp.readlines():
for regex in regexs: for regex in regexs:
@ -107,6 +115,56 @@ class ScannerTest(unittest.TestCase):
i += 1 i += 1
self.assertEqual(count, i) self.assertEqual(count, i)
def test_get_gradle_compile_commands_with_catalog(self):
test_files = [
('source-files/com.lolo.io.onelist/build.gradle.kts', 'yes', 5),
('source-files/com.lolo.io.onelist/app/build.gradle.kts', 'yes', 26),
('source-files/catalog.test/build.gradle.kts', 'yes', 3),
('source-files/catalog.test/app/build.gradle', 'yes', 2),
]
for f, flavor, count in test_files:
i = 0
build = fdroidserver.metadata.Build()
build.gradle = [flavor]
regexs = fdroidserver.scanner.get_gradle_compile_commands_with_catalog(
build, "libs"
)
with open(f, encoding='utf-8') as fp:
for line in fp.readlines():
for regex in regexs:
m = regex.match(line)
if m:
i += 1
self.assertEqual(count, i)
def test_catalog(self):
accessor_coordinate_pairs = {
'firebase.crash': ['com.google.firebase:firebase-crash:1.1.1'],
'firebase.core': ['com.google.firebase:firebase-core:2.2.2'],
'play.service.ads': ['com.google.android.gms:play-services-ads:1.2.1'],
'plugins.google.services': ['com.google.gms.google-services:1.2.1'],
'plugins.firebase.crashlytics': ['com.google.firebase.crashlytics:1.1.1'],
'bundles.firebase': [
'com.google.firebase:firebase-crash:1.1.1',
'com.google.firebase:firebase-core:2.2.2',
],
}
with open('source-files/catalog.test/gradle/libs.versions.toml', 'rb') as f:
catalog = fdroidserver.scanner.GradleVersionCatalog(tomllib.load(f))
for accessor, coordinate in accessor_coordinate_pairs.items():
self.assertEqual(catalog.get_coordinate(accessor), coordinate)
def test_get_catalogs(self):
test_files = [
('source-files/com.lolo.io.onelist/', 1),
('source-files/catalog.test/', 3),
('source-files/org.piepmeyer.gauguin/', 1),
]
for root, count in test_files:
self.assertEqual(count, len(fdroidserver.scanner.get_catalogs(root)))
def test_scan_source_files_sneaky_maven(self): def test_scan_source_files_sneaky_maven(self):
"""Check for sneaking in banned maven repos""" """Check for sneaking in banned maven repos"""
os.chdir(self.testdir) os.chdir(self.testdir)
@ -780,8 +838,7 @@ class Test_main(unittest.TestCase):
self.scan_binary_func = mock.Mock(return_value=0) self.scan_binary_func = mock.Mock(return_value=0)
def test_parsing_appid(self): def test_parsing_appid(self):
""" """This test verifies that app id get parsed correctly
This test verifies that app id get parsed correctly
(doesn't test how they get processed) (doesn't test how they get processed)
""" """
self.args = ["com.example.app"] self.args = ["com.example.app"]
@ -802,8 +859,7 @@ class Test_main(unittest.TestCase):
self.scan_binary_func.assert_not_called() self.scan_binary_func.assert_not_called()
def test_parsing_apkpath(self): def test_parsing_apkpath(self):
""" """This test verifies that apk paths get parsed correctly
This test verifies that apk paths get parsed correctly
(doesn't test how they get processed) (doesn't test how they get processed)
""" """
self.args = ["local.application.apk"] self.args = ["local.application.apk"]

View file

@ -0,0 +1,2 @@
implementation libs.bundles.firebase
implementation libs.play.service.ads

View file

@ -0,0 +1,5 @@
plugins {
alias(libs.plugins.google.services)
alias(libs.plugins.firebase.crashlytics)
alias(projectLibs.plugins.firebase.crashlytics)
}

View file

@ -0,0 +1,15 @@
[versions]
firebase = "1.1.1"
gms = "1.2.1"
[libraries]
firebase-crash = { module = "com.google.firebase:firebase-crash", version.ref = "firebase" }
firebase_core = { module = "com.google.firebase:firebase-core", version = "2.2.2" }
"play.service.ads" = { module = "com.google.android.gms:play-services-ads", version.ref = "gms"}
[plugins]
google-services = { id = "com.google.gms.google-services", version.ref = "gms" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase" }
[bundles]
firebase = ["firebase-crash", "firebase_core"]

View file

@ -0,0 +1,15 @@
[versions]
firebase = "1.1.1"
gms = "1.2.1"
[libraries]
firebase-crash = { module = "com.google.firebase:firebase-crash", version.ref = "firebase" }
firebase_core = { module = "com.google.firebase:firebase-core", version = "2.2.2" }
"play.service.ads" = { module = "com.google.android.gms:play-services-ads", version.ref = "gms"}
[plugins]
google-services = { id = "com.google.gms.google-services", version.ref = "gms" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase" }
[bundles]
firebase = ["firebase-crash", "firebase_core"]

View file

@ -0,0 +1,11 @@
dependencyResolutionManagement {
defaultLibrariesExtensionName = "projectLibs"
versionCatalogs {
create("libs") {
from(files("./libs.versions.toml"))
}
create("anotherLibs") {
from(files("$rootDir/libs.versions.toml"))
}
}
}

View file

@ -0,0 +1,118 @@
import java.io.FileInputStream
import java.util.Properties
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.google.services)
alias(libs.plugins.firebase.crashlytics)
alias(libs.plugins.ksp)
}
android {
namespace = "com.lolo.io.onelist"
val versionPropsFile = file("../version.properties")
var versionCodeCI: Int? = null
if (versionPropsFile.canRead()) {
val versionProps = Properties()
versionProps.load(FileInputStream(versionPropsFile))
val v = versionProps["VERSION_CODE"]
versionCodeCI = (versionProps["VERSION_CODE"] as String).toInt()
}
defaultConfig {
multiDexEnabled = true
applicationId = "com.lolo.io.onelist"
compileSdk = 34
minSdk = 23
targetSdk = 34
versionCode = versionCodeCI ?: 19
versionName = "1.4.2"
vectorDrawables.useSupportLibrary = true
}
androidResources {
generateLocaleConfig = true
}
buildFeatures {
viewBinding = true
buildConfig = true
}
ksp {
arg("room.schemaLocation", "$projectDir/schemas")
}
buildTypes {
getByName("debug") {
applicationIdSuffix = ".debug"
versionNameSuffix = "-DEBUG"
resValue("string", "app_name", "1ListDev")
}
getByName("release") {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
resValue("string", "app_name", "1List")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
dependencies {
// android
implementation(libs.androidx.core.splashscreen)
implementation(libs.androidx.preference.ktx)
implementation(libs.androidx.lifecycle.extensions)
implementation(libs.androidx.legacy.support.v4)
implementation(libs.androidx.appcompat)
// android - design
implementation(libs.constraint.layout)
implementation(libs.androidx.recyclerview)
implementation(libs.flexbox)
implementation(libs.material)
implementation(libs.androidx.swiperefreshlayout)
// kotlin
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlin.stdlib.jdk7)
// firebase
implementation(libs.firebase.crashlytics)
// koin di
implementation(libs.koin.android)
implementation(libs.koin.androidx.navigation)
// room
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
ksp(libs.androidx.room.compiler)
// json
implementation(libs.gson)
// other libs
implementation(libs.whatsnew)
implementation(libs.storage)
implementation(libs.advrecyclerview)
}

View file

@ -0,0 +1,11 @@
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.google.services) apply false
alias(libs.plugins.firebase.crashlytics) apply false
alias(libs.plugins.ksp) apply false
}
tasks.register("clean", Delete::class) {
delete(rootProject.layout.buildDirectory)
}

View file

@ -0,0 +1,58 @@
[versions]
advrecyclerview = "1.0.0"
appcompat = "1.6.1"
constraint-layout = "2.0.4"
crashlytics = "18.6.2"
firebase-crashlytics-gradle-plugin = "2.9.9"
flexbox = "3.0.0"
gson = "2.5.6"
kotlin = "1.9.20"
kotlin-coroutines = "1.6.4"
legacy-support-v4 = "1.0.0"
lifecycle-extensions = "2.2.0"
material = "1.11.0"
preference-ktx = "1.2.1"
recyclerview = "1.3.2"
splashscreen ="1.0.1"
koin ="3.5.0"
room="2.6.1"
storage = "1.5.5"
swiperefreshlayout = "1.1.0"
whatsnew = "0.1.7"
ksp-plugin="1.9.20-1.0.14"
# plugins versions
android-application-plugin="8.3.0"
kotlin-android-plugin="1.9.22"
google-services-plugin = "4.4.1"
[libraries]
advrecyclerview = { module = "com.h6ah4i.android.widget.advrecyclerview:advrecyclerview", version.ref = "advrecyclerview" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "splashscreen" }
androidx-legacy-support-v4 = { module = "androidx.legacy:legacy-support-v4", version.ref = "legacy-support-v4" }
androidx-lifecycle-extensions = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycle-extensions" }
androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preference-ktx" }
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "swiperefreshlayout" }
constraint-layout = { module = "com.android.support.constraint:constraint-layout", version.ref = "constraint-layout" }
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics", version.ref = "crashlytics" }
flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" }
gson = { module = "org.immutables:gson", version.ref = "gson" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-androidx-navigation = { module = "io.insert-koin:koin-androidx-navigation", version.ref = "koin" }
kotlin-stdlib-jdk7 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk7", version.ref = "kotlin" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin-coroutines" }
material = { module = "com.google.android.material:material", version.ref = "material" }
storage = { module = "com.anggrayudi:storage", version.ref = "storage" }
whatsnew = { module = "io.github.tonnyl:whatsnew", version.ref = "whatsnew" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp-plugin" }
android-application = { id = "com.android.application", version.ref = "android-application-plugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin-android-plugin" }
google-services = { id = "com.google.gms.google-services", version.ref = "google-services-plugin" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebase-crashlytics-gradle-plugin" }

View file

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View file

@ -0,0 +1,9 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
include 'app'

View file

@ -0,0 +1,47 @@
import java.net.URI
buildscript {
dependencies {
classpath("com.android.tools.build:gradle:8.6.0")
}
}
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.jvm) apply false
alias(libs.plugins.sonarqube)
alias(libs.plugins.ktlint)
alias(libs.plugins.ksp)
alias(libs.plugins.roborazzi) apply false
alias(libs.plugins.gms) apply false
}
sonarqube {
properties {
property("sonar.projectKey", "org.piepmeyer.gauguin")
property("sonar.organization", "meikpiep")
property("sonar.verbose", "true")
property("sonar.host.url", "https://sonarcloud.io")
}
}
tasks.sonar {
onlyIf("There is no property 'buildserver'") {
project.hasProperty("buildserver")
}
dependsOn(":gauguin-app:lint")
}
allprojects {
repositories {
mavenCentral()
google()
maven { url = URI("https://jitpack.io") }
}
}
subprojects {
apply(plugin = "org.jlleitschuh.gradle.ktlint")
}

View file

@ -0,0 +1,91 @@
[versions]
kotlin = "1.9.23"
koin = "3.5.6"
koin-annotations="1.3.1"
kotest = "5.9.1"
kotest-extensions = "1.3.0"
kotlin-coroutines = "1.8.1"
android-gradle-plugin = "8.6.0"
androidUiTestingUtils = "2.3.3"
roborazzi = "1.26.0"
[libraries]
kotlin-coroutines-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "kotlin-coroutines" }
kotlin-coroutines-debug = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-debug", version.ref = "kotlin-coroutines" }
kotlin-serialization = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.6.3" }
logging-kotlin = { group = "io.github.oshai", name = "kotlin-logging-jvm", version = "6.0.9" }
logging-slf = { group = "org.slf4j", name = "slf4j-api", version = "2.0.13" }
logging-logback-android = { group = "com.github.tony19", name = "logback-android", version = "3.0.0" }
logging-logback-kotlin = { group = "ch.qos.logback", name = "logback-classic", version = "1.5.6" }
android-material = { group = "com.google.android.material", name = "material", version = "1.12.0" }
androidx-annotation = { group = "androidx.annotation", name = "annotation", version = "1.8.2" }
androidx-ktx = { group = "androidx.core", name = "core-ktx", version = "1.13.1" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version = "2.1.4" }
androidx-drawerlayout = { group = "androidx.drawerlayout", name = "drawerlayout", version = "1.2.0" }
androidx-fragment = { group = "androidx.fragment", name = "fragment-ktx", version = "1.8.3" }
androidx-gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version = "1.0.0" }
androidx-lifecycle-runtime = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version = "2.8.5" }
androidx-lifecycle-viewmodel = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version = "2.8.5" }
androidx-preference = { group = "androidx.preference", name = "preference-ktx", version = "1.2.1" }
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version = "1.3.2" }
androidx-transition = { group = "androidx.transition", name = "transition", version = "1.5.1" }
androidx-window = { group = "androidx.window", name = "window", version = "1.3.0" }
androidx-window-core = { group = "androidx.window", name = "window-core", version = "1.3.0" }
androidx-test-junit-ktx = { group = "androidx.test.ext", name = "junit-ktx", version = "1.2.1" }
androidx-test-rules = { group = "androidx.test", name = "rules", version = "1.6.1" }
androidx-test-runner = { group = "androidx.test", name = "runner", version = "1.6.2" }
koin-core = { group = "io.insert-koin", name = "koin-core", version.ref = "koin" }
koin-annotations = { group = "io.insert-koin", name = "koin-annotations", version.ref = "koin-annotations" }
koin-ksp-compiler = { group = "io.insert-koin", name = "koin-ksp-compiler", version.ref = "koin-annotations" }
koin-test = { group = "io.insert-koin", name = "koin-test", version.ref = "koin" }
koin-android = { group = "io.insert-koin", name = "koin-android", version.ref = "koin" }
kotest-runner = { group = "io.kotest", name = "kotest-runner-junit5", version.ref = "kotest" }
kotest-assertions = { group = "io.kotest", name = "kotest-assertions-core", version.ref = "kotest" }
kotest-parametrizedtests = { group = "io.kotest", name = "kotest-framework-datatest", version.ref = "kotest" }
kotest-koin = { group = "io.kotest.extensions", name = "kotest-extensions-koin", version.ref = "kotest-extensions" }
test-mockk = { group = "io.mockk", name = "mockk", version = "1.13.11" }
androiduitestingutils-utils = { group = "com.github.sergio-sastre.AndroidUiTestingUtils", name = "utils", version.ref = "androidUiTestingUtils" }
androiduitestingutils-robolectric = { group = "com.github.sergio-sastre.AndroidUiTestingUtils", name = "robolectric", version.ref = "androidUiTestingUtils" }
roboelectric = { group = "org.robolectric", name = "robolectric", version = "4.13" }
roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" }
roborazzi-junit = { group = "io.github.takahirom.roborazzi", name = "roborazzi-junit-rule", version.ref = "roborazzi" }
junit-vintage-engine = { group = "org.junit.vintage", name = "junit-vintage-engine", version = "5.10.3" }
thirdparty-konfetti = { group = "nl.dionsegijn", name = "konfetti-xml", version = "2.0.4" }
#thirdparty-ferriswheel = { group = "ru.github.igla", name = "ferriswheel", version = "1.2" }
thirdparty-navigationdrawer = { group = "com.mikepenz", name = "materialdrawer", version = "9.0.2" }
thirdparty-balloon = { group = "com.github.skydoves", name = "balloon", version = "1.6.7" }
thirdparty-vico = { group = "com.patrykandpatrick.vico", name = "views", version = "2.0.0-alpha.25" }
thirdparty-androidplot = { group = "com.androidplot", name = "androidplot-core", version = "1.5.11" }
thirdparty-leakcanary = { group = "com.squareup.leakcanary", name = "leakcanary-android", version = "2.14" }
[plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
android-library = { id = "com.android.library", version.ref = "android-gradle-plugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
sonarqube = { id = "org.sonarqube", version = "5.0.0.4638" }
ktlint = { id = "org.jlleitschuh.gradle.ktlint", version = "12.1.1" }
ksp = { id = "com.google.devtools.ksp", version = "1.9.23-1.0.20" }
roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" }
gms = { id = "com.google.gms.google-services", version = "1" }
[bundles]
logging = ["logging-kotlin", "logging-slf"]
kotest = ["kotest-runner", "kotest-assertions", "kotest-parametrizedtests", "kotest-koin"]
koin = ["koin-core", "koin-annotations", "koin-ksp-compiler"]
androidx-test = ["androidx-test-junit-ktx", "androidx-test-rules", "androidx-test-runner"]
screenshotTests = ["androiduitestingutils-utils", "androiduitestingutils-robolectric", "roboelectric", "roborazzi", "roborazzi-junit", "junit-vintage-engine"]

View file

@ -0,0 +1,24 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("libs.versions.toml"))
}
}
}
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version ("0.8.0")
}
rootProject.name = "gauguin"
include(":gauguin-app")
include(":gauguin-core")