Merge branch 'catalog' into 'master'

scanner: support libs.versions.toml

Closes #1168

See merge request fdroid/fdroidserver!1526
This commit is contained in:
Hans-Christoph Steiner 2024-09-23 15:03:25 +00:00
commit 8c4583b04e
17 changed files with 744 additions and 52 deletions

View file

@ -741,6 +741,11 @@ include tests/signindex/guardianproject-v1.jar
include tests/signindex/testy.jar
include tests/signindex/unsigned.jar
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/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/app/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.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
@ -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/settings.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/build.gradle
include tests/source-files/org.tasks/build.gradle.kts

View file

@ -34,6 +34,11 @@ from enum import IntEnum
from pathlib import Path
from tempfile import TemporaryDirectory
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
from . import _, common, metadata, scanner
from .exception import BuildException, ConfigurationException, VCSException
@ -58,13 +63,177 @@ DEPFILE = {
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):
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):
compileCommands = [
'alias',
'api',
'apk',
'classpath',
@ -80,15 +249,25 @@ def get_gradle_compile_commands(build):
if build.gradle and build.gradle != ['yes']:
flavors += build.gradle
commands = [
''.join(c) for c in itertools.product(flavors, buildTypes, compileCommands)
return [''.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):
"""
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
"""
@ -183,8 +362,7 @@ class SignatureDataController:
raise SignatureDataVersionMismatchException()
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
inaccessible or not parse-able
@ -259,8 +437,7 @@ class SignatureDataController:
logging.debug("write '{}' to cache".format(self.filename))
def verify_data(self):
"""
Clean and validate `self.data`.
"""Clean and validate `self.data`.
Right now this function does just a basic key sanitation.
"""
@ -451,8 +628,7 @@ _SCANNER_TOOL = None
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.
"""
@ -496,6 +672,7 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
Returns
-------
the number of fatal problems encountered.
"""
count = 0
@ -571,6 +748,7 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
Returns
-------
0 as we explicitly ignore the file, so don't count an error
"""
msg = 'Ignoring %s at %s' % (what, path_in_build_dir)
logging.info(msg)
@ -593,6 +771,7 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
Returns
-------
0 as we deleted the offending file
"""
msg = 'Removing %s at %s' % (what, path_in_build_dir)
logging.info(msg)
@ -620,6 +799,7 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
Returns
-------
0, as warnings don't count as errors
"""
if toignore(path_in_build_dir):
return 0
@ -645,6 +825,7 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
Returns
-------
0 if the problem was ignored/deleted/is only a warning, 1 otherwise
"""
options = common.get_options()
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 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):
return any(command.match(line) for command in gradle_compile_commands)
def is_used_by_gradle_with_catalog(line, prefix):
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
for root, dirs, files in os.walk(build_dir, topdown=True):
# 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:
dirs.remove(ignoredir)
if "settings.gradle" in files or "settings.gradle.kts" in files:
catalogs = get_catalogs(root)
for curfile in files:
if curfile in ['.DS_Store']:
continue
@ -789,14 +983,28 @@ def scan_source(build_dir, build=metadata.Build(), json_per_build=None):
with open(filepath, 'r', errors='replace') as f:
lines = f.readlines()
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):
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,
filepath,
json_per_build,
)
noncomment_lines = [
line for line in lines if not common.gradle_comment.match(line)
]

View file

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

View file

@ -1,9 +1,11 @@
#!/usr/bin/env python3
import collections
import glob
import inspect
import logging
import os
import pathlib
import re
import shutil
import sys
@ -11,13 +13,16 @@ import tempfile
import textwrap
import unittest
import uuid
import yaml
import zipfile
import collections
import pathlib
from unittest import mock
from dataclasses import asdict
from datetime import datetime, timedelta
from unittest import mock
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
import yaml
localmodule = os.path.realpath(
os.path.join(os.path.dirname(inspect.getfile(inspect.currentframe())), '..')
@ -26,11 +31,12 @@ print('localmodule: ' + localmodule)
if localmodule not in sys.path:
sys.path.insert(0, localmodule)
from testcommon import TmpCwd, mkdtemp, mock_open_to_str, parse_args_for_test
import fdroidserver.build
import fdroidserver.common
import fdroidserver.metadata
import fdroidserver.scanner
from testcommon import TmpCwd, mkdtemp, mock_open_to_str, parse_args_for_test
# Always use built-in default rules so changes in downloaded rules don't break tests.
@ -68,6 +74,8 @@ class ScannerTest(unittest.TestCase):
'realm': 1,
'se.manyver': 3,
'lockfile.test': 1,
'com.lolo.io.onelist': 6,
'catalog.test': 10,
}
for d in glob.glob(os.path.join(source_files, '*')):
build = fdroidserver.metadata.Build()
@ -77,26 +85,28 @@ class ScannerTest(unittest.TestCase):
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 = [
('source-files/fdroid/fdroidclient/build.gradle', 'yes', 18),
('source-files/com.nextcloud.client/build.gradle', 'generic', 28),
('source-files/com.kunzisoft.testcase/build.gradle', 'libre', 4),
('source-files/cn.wildfirechat.chat/chat/build.gradle', 'yes', 33),
('source-files/org.tasks/app/build.gradle.kts', 'generic', 43),
('source-files/at.bitfire.davdroid/build.gradle', 'standard', 16),
('source-files/se.manyver/android/app/build.gradle', 'indie', 29),
('source-files/osmandapp/osmand/build.gradle', 'free', 5),
('source-files/eu.siacs.conversations/build.gradle', 'free', 24),
('source-files/org.mozilla.rocket/app/build.gradle', 'focus', 42),
('source-files/com.jens.automation2/app/build.gradle', 'fdroidFlavor', 9),
('source-files/fdroid/fdroidclient/build.gradle', 'yes', 15),
('source-files/com.nextcloud.client/build.gradle', 'generic', 24),
('source-files/com.kunzisoft.testcase/build.gradle', 'libre', 3),
('source-files/cn.wildfirechat.chat/chat/build.gradle', 'yes', 30),
('source-files/org.tasks/app/build.gradle.kts', 'generic', 41),
('source-files/at.bitfire.davdroid/build.gradle', 'standard', 15),
('source-files/se.manyver/android/app/build.gradle', 'indie', 26),
('source-files/osmandapp/osmand/build.gradle', 'free', 2),
('source-files/eu.siacs.conversations/build.gradle', 'free', 21),
('source-files/org.mozilla.rocket/app/build.gradle', 'focus', 40),
('source-files/com.jens.automation2/app/build.gradle', 'fdroidFlavor', 5),
]
for f, flavor, count in test_files:
i = 0
build = fdroidserver.metadata.Build()
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:
for line in fp.readlines():
for regex in regexs:
@ -105,6 +115,56 @@ class ScannerTest(unittest.TestCase):
i += 1
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):
"""Check for sneaking in banned maven repos"""
os.chdir(self.testdir)
@ -778,17 +838,17 @@ class Test_main(unittest.TestCase):
self.scan_binary_func = mock.Mock(return_value=0)
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)
"""
self.args = ["com.example.app"]
with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir), mock.patch(
"sys.exit", self.exit_func
), mock.patch("sys.argv", ["fdroid scanner", *self.args]), mock.patch(
"fdroidserver.common.read_app_args", self.read_app_args_func
), mock.patch(
"fdroidserver.scanner.scan_binary", self.scan_binary_func
with (
tempfile.TemporaryDirectory() as tmpdir,
TmpCwd(tmpdir),
mock.patch("sys.exit", self.exit_func),
mock.patch("sys.argv", ["fdroid scanner", *self.args]),
mock.patch("fdroidserver.common.read_app_args", self.read_app_args_func),
mock.patch("fdroidserver.scanner.scan_binary", self.scan_binary_func),
):
fdroidserver.scanner.main()
@ -799,17 +859,17 @@ class Test_main(unittest.TestCase):
self.scan_binary_func.assert_not_called()
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)
"""
self.args = ["local.application.apk"]
with tempfile.TemporaryDirectory() as tmpdir, TmpCwd(tmpdir), mock.patch(
"sys.exit", self.exit_func
), mock.patch("sys.argv", ["fdroid scanner", *self.args]), mock.patch(
"fdroidserver.common.read_app_args", self.read_app_args_func
), mock.patch(
"fdroidserver.scanner.scan_binary", self.scan_binary_func
with (
tempfile.TemporaryDirectory() as tmpdir,
TmpCwd(tmpdir),
mock.patch("sys.exit", self.exit_func),
mock.patch("sys.argv", ["fdroid scanner", *self.args]),
mock.patch("fdroidserver.common.read_app_args", self.read_app_args_func),
mock.patch("fdroidserver.scanner.scan_binary", self.scan_binary_func),
):
pathlib.Path(self.args[0]).touch()
fdroidserver.scanner.main()

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")