diff --git a/MANIFEST.in b/MANIFEST.in index ff54de79..b9e1d722 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 diff --git a/fdroidserver/scanner.py b/fdroidserver/scanner.py index ee26fce4..0e877f07 100644 --- a/fdroidserver/scanner.py +++ b/fdroidserver/scanner.py @@ -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) ] diff --git a/setup.py b/setup.py index 6b9be802..444a70be 100755 --- a/setup.py +++ b/setup.py @@ -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 diff --git a/tests/scanner.TestCase b/tests/scanner.TestCase index 98bab924..701c0b2e 100755 --- a/tests/scanner.TestCase +++ b/tests/scanner.TestCase @@ -18,6 +18,10 @@ 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( @@ -70,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() @@ -79,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: @@ -107,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) @@ -780,8 +838,7 @@ 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"] @@ -802,8 +859,7 @@ 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"] diff --git a/tests/source-files/catalog.test/app/build.gradle b/tests/source-files/catalog.test/app/build.gradle new file mode 100644 index 00000000..72c9d184 --- /dev/null +++ b/tests/source-files/catalog.test/app/build.gradle @@ -0,0 +1,2 @@ +implementation libs.bundles.firebase +implementation libs.play.service.ads diff --git a/tests/source-files/catalog.test/build.gradle.kts b/tests/source-files/catalog.test/build.gradle.kts new file mode 100644 index 00000000..5572706f --- /dev/null +++ b/tests/source-files/catalog.test/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + alias(libs.plugins.google.services) + alias(libs.plugins.firebase.crashlytics) + alias(projectLibs.plugins.firebase.crashlytics) +} diff --git a/tests/source-files/catalog.test/gradle/libs.versions.toml b/tests/source-files/catalog.test/gradle/libs.versions.toml new file mode 100644 index 00000000..666a0f7f --- /dev/null +++ b/tests/source-files/catalog.test/gradle/libs.versions.toml @@ -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"] diff --git a/tests/source-files/catalog.test/libs.versions.toml b/tests/source-files/catalog.test/libs.versions.toml new file mode 100644 index 00000000..666a0f7f --- /dev/null +++ b/tests/source-files/catalog.test/libs.versions.toml @@ -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"] diff --git a/tests/source-files/catalog.test/settings.gradle.kts b/tests/source-files/catalog.test/settings.gradle.kts new file mode 100644 index 00000000..4ea352c8 --- /dev/null +++ b/tests/source-files/catalog.test/settings.gradle.kts @@ -0,0 +1,11 @@ +dependencyResolutionManagement { + defaultLibrariesExtensionName = "projectLibs" + versionCatalogs { + create("libs") { + from(files("./libs.versions.toml")) + } + create("anotherLibs") { + from(files("$rootDir/libs.versions.toml")) + } + } +} diff --git a/tests/source-files/com.lolo.io.onelist/app/build.gradle.kts b/tests/source-files/com.lolo.io.onelist/app/build.gradle.kts new file mode 100644 index 00000000..261cfe2f --- /dev/null +++ b/tests/source-files/com.lolo.io.onelist/app/build.gradle.kts @@ -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) +} diff --git a/tests/source-files/com.lolo.io.onelist/build.gradle.kts b/tests/source-files/com.lolo.io.onelist/build.gradle.kts new file mode 100644 index 00000000..baad9726 --- /dev/null +++ b/tests/source-files/com.lolo.io.onelist/build.gradle.kts @@ -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) +} \ No newline at end of file diff --git a/tests/source-files/com.lolo.io.onelist/gradle/libs.versions.toml b/tests/source-files/com.lolo.io.onelist/gradle/libs.versions.toml new file mode 100644 index 00000000..d6cf9869 --- /dev/null +++ b/tests/source-files/com.lolo.io.onelist/gradle/libs.versions.toml @@ -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" } \ No newline at end of file diff --git a/tests/source-files/com.lolo.io.onelist/gradle/wrapper/gradle-wrapper.properties b/tests/source-files/com.lolo.io.onelist/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..e411586a --- /dev/null +++ b/tests/source-files/com.lolo.io.onelist/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/tests/source-files/com.lolo.io.onelist/settings.gradle b/tests/source-files/com.lolo.io.onelist/settings.gradle new file mode 100644 index 00000000..533aeeeb --- /dev/null +++ b/tests/source-files/com.lolo.io.onelist/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +include 'app' diff --git a/tests/source-files/org.piepmeyer.gauguin/build.gradle.kts b/tests/source-files/org.piepmeyer.gauguin/build.gradle.kts new file mode 100644 index 00000000..cb7d1d02 --- /dev/null +++ b/tests/source-files/org.piepmeyer.gauguin/build.gradle.kts @@ -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") +} diff --git a/tests/source-files/org.piepmeyer.gauguin/libs.versions.toml b/tests/source-files/org.piepmeyer.gauguin/libs.versions.toml new file mode 100644 index 00000000..7159985c --- /dev/null +++ b/tests/source-files/org.piepmeyer.gauguin/libs.versions.toml @@ -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"] + diff --git a/tests/source-files/org.piepmeyer.gauguin/settings.gradle.kts b/tests/source-files/org.piepmeyer.gauguin/settings.gradle.kts new file mode 100644 index 00000000..46f4acda --- /dev/null +++ b/tests/source-files/org.piepmeyer.gauguin/settings.gradle.kts @@ -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")