1/* 2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import androidx.build.ProjectLayoutType 18import org.gradle.api.DefaultTask 19import org.gradle.api.file.DirectoryProperty 20import org.gradle.api.provider.Property 21import org.gradle.api.tasks.Copy 22import org.gradle.api.tasks.PathSensitivity 23 24import javax.inject.Inject 25 26/** 27 * Finds the sqlite sources and puts them into the `destinationDirectory`. 28 * 29 * This task is setup differently between AOSP and Github (Playground), hence use the helper 30 * `registerPrepareSqliteSourcesTask` method to create an instance of it. 31 * 32 * On AOSP, the sources are in an external prebuilts repository and simply get copied into the given 33 * [destinationDirectory]. 34 * On Github, they are downloaded from SQLite servers and copied into the `destinationDirectory` 35 * from there. 36 * 37 * To ensure each version is consistent, we use the `sqliteVersion` parameter and check the Sqlite 38 * source code for them. 39 */ 40abstract class PrepareSqliteSourcesTask extends DefaultTask { 41 // defined in https://github.com/sqlite/sqlite/blob/master/src/sqlite.h.in#L149 42 private static String VERSION_PREFIX = "#define SQLITE_VERSION" 43 private FileSystemOperations fileSystemOperations 44 45 /** 46 * The Sqlite version to prepare 47 */ 48 @Input 49 abstract Property<String> getSqliteVersion() 50 51 /** 52 * The target directory where the Sqlite source will be put 53 */ 54 @OutputDirectory 55 abstract DirectoryProperty getDestinationDirectory() 56 57 /** 58 * The source directory which includes the original Sqlite amalgamation distribution 59 */ 60 @InputDirectory 61 @PathSensitive(PathSensitivity.NONE) 62 abstract DirectoryProperty getSources() 63 64 @Inject 65 PrepareSqliteSourcesTask(FileSystemOperations fileSystemOperations) { 66 this.fileSystemOperations = fileSystemOperations 67 description = "Create a directory containing Sqlite sources." 68 group = "build" 69 } 70 71 @TaskAction 72 void prepareSources() { 73 File originalSqliteSources = sources.asFile.get() 74 validateSqliteVersion(originalSqliteSources) 75 File targetSourceDirectory = destinationDirectory.asFile.get() 76 targetSourceDirectory.deleteDir() 77 targetSourceDirectory.mkdirs() 78 79 fileSystemOperations.copy { CopySpec copySpec -> 80 copySpec.from(originalSqliteSources) 81 copySpec.into(targetSourceDirectory) 82 copySpec.include("sqlite3.c", "sqlite3.h") 83 } 84 } 85 86 /** 87 * Finds the sqlite version definition in the source file and ensures it is the same 88 * version as [sqliteVersion] to ensure they never go out of sync. 89 */ 90 private void validateSqliteVersion(File sourceDir) { 91 File headerFile = new File(sourceDir, "sqlite3.h") 92 if (!headerFile.isFile() || !headerFile.canRead()) { 93 throw new IllegalStateException("Cannot find header file at location: ${headerFile}") 94 } 95 String versionLine = headerFile.text.split('\n').find { it.contains(VERSION_PREFIX) } 96 if (versionLine == null) { 97 throw new IllegalStateException("Cannot find the version line in sqlite.") 98 } 99 String strippedVersion = versionLine.takeAfter(VERSION_PREFIX).trim() 100 .takeBetween("\"", "\"") 101 if (strippedVersion != sqliteVersion.get()) { 102 throw new IllegalStateException(""" 103 Expected ${sqliteVersion.get()}, found $strippedVersion. Please update the 104 sqliteVersion parameter if this was intentional. 105 """.trim()) 106 } 107 } 108} 109 110/** 111 * Downloads the sqlite amalgamation for the given version. 112 * See: https://sqlite.org/amalgamation.html and https://www.sqlite.org/download.html for 113 * details. 114 */ 115@CacheableTask 116abstract class DownloadSQLiteAmalgamationTask extends DefaultTask { 117 /** 118 * The Sqlite version to download 119 */ 120 @Input 121 abstract Property<String> getReleaseVersion() 122 123 /** 124 * The year which Sqlite version was released. It is necessary because the download 125 * URL includes the year. 126 */ 127 @Input 128 abstract Property<Integer> getReleaseYear() 129 130 /** 131 * Target file where the downloaded amalgamation zip file will be written. 132 */ 133 @OutputFile 134 abstract RegularFileProperty getDownloadTargetFile() 135 136 DownloadSQLiteAmalgamationTask() { 137 description = "Downloads the Sqlite amalgamation build from sqlite servers" 138 group = "build" 139 } 140 141 @TaskAction 142 void download() { 143 File downloadTarget = downloadTargetFile.asFile.get() 144 downloadTarget.delete() 145 downloadTarget.parentFile.mkdirs() 146 String downloadUrl = buildDownloadUrl(releaseYear.get(), releaseVersion.get()) 147 downloadTarget.withOutputStream { outputStream -> 148 new URL(downloadUrl).withInputStream { inputStream -> 149 inputStream.transferTo(outputStream) 150 } 151 } 152 } 153 154 /** 155 * Computes the download URL from the sqlite version and release year inputs. 156 */ 157 private static String buildDownloadUrl(int releaseYear, String releaseVersion) { 158 // see https://www.sqlite.org/download.html 159 // The version is encoded so that filenames sort in order of increasing version number 160 // when viewed using "ls". 161 // For version 3.X.Y the filename encoding is 3XXYY00. 162 // For branch version 3.X.Y.Z, the encoding is 3XXYYZZ. 163 def sections = releaseVersion.split('\\.') 164 if (sections.size() < 3) { 165 throw new IllegalArgumentException("Invalid sqlite version $releaseVersion") 166 } 167 int major = sections[0].toInteger() 168 int minor = sections[1].toInteger() 169 int patch = sections[2].toInteger() 170 int branch = sections.size() >= 4 ? sections[3].toInteger() : 0 171 String fileName = String.format("%d%02d%02d%02d.zip", major, minor, patch, branch) 172 return "https://www.sqlite.org/${releaseYear}/sqlite-amalgamation-${fileName}" 173 } 174} 175 176/** 177 * Configuration object for preparing relevant sqlite sources. 178 */ 179abstract class Configuration { 180 /** 181 * The Sqlite version to be prepared. 182 */ 183 abstract Property<String> getSqliteVersion() 184 185 /** 186 * The release year of the requested Sqlite version. 187 * It is necessary because the download URL for sqlite amalgamation includes the 188 * release year. 189 */ 190 abstract Property<Integer> getSqliteReleaseYear() 191 192 /** 193 * The location to put prepared sqlite sources. 194 */ 195 abstract DirectoryProperty getDestinationDirectory() 196 197 /** 198 * Set when sqlite is downloaded from prebuilts rather than from Sqlite servers (used in AOSP). 199 */ 200 abstract DirectoryProperty getSqlitePrebuiltsDirectory() 201} 202 203/** 204 * Utility method to create an instance of [PrepareSqliteSourcesTask] that is compatible 205 * with both AOSP and GitHub builds. 206 * This is exported into the build script via ext properties. 207 */ 208TaskProvider<PrepareSqliteSourcesTask> registerPrepareSqliteSourcesTask( 209 Project project, 210 String name, 211 Action<Configuration> configure 212) { 213 def configuration = project.objects.newInstance(Configuration.class) 214 configure.execute(configuration) 215 216 def distDirectory = project.objects.directoryProperty() 217 if (ProjectLayoutType.isPlayground(project)) { 218 def downloadTaskProvider = project.tasks.register( 219 name.capitalize() + "DownloadAmalgamation", 220 DownloadSQLiteAmalgamationTask 221 ) { 222 it.releaseVersion.set(configuration.sqliteVersion) 223 it.releaseYear.set(configuration.sqliteReleaseYear) 224 it.downloadTargetFile.set( 225 project.layout.buildDirectory.file("sqlite3/download/amalgamation.zip") 226 ) 227 } 228 229 def unzipTaskProvider = project.tasks.register( 230 name.capitalize() + "UnzipAmalgamation", 231 Copy 232 ) { 233 it.from( 234 project.zipTree(downloadTaskProvider.map { it.downloadTargetFile }) 235 ) 236 it.into( 237 project.layout.buildDirectory.dir("sqlite3/download/unzipped") 238 ) 239 it.eachFile { 240 it.path = it.path.replaceFirst(/sqlite-amalgamation-\d+\//, '') 241 } 242 } 243 distDirectory.set( 244 project.objects.directoryProperty().fileProvider( 245 unzipTaskProvider.map { it.destinationDir } 246 ) 247 ) 248 } else { 249 distDirectory.set(configuration.sqlitePrebuiltsDirectory) 250 } 251 252 def prepareSourcesTaskProvider = project.tasks.register( 253 name, 254 PrepareSqliteSourcesTask 255 ) { 256 it.sources.set(distDirectory) 257 it.sqliteVersion.set(configuration.sqliteVersion) 258 it.destinationDirectory.set( 259 project.layout.buildDirectory.dir("sqlite/selected-sources") 260 ) 261 } 262 return prepareSourcesTaskProvider 263} 264 265// export a function to register the task 266ext.registerPrepareSqliteSourcesTask = this.®isterPrepareSqliteSourcesTask 267