<lambda>null1 import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp
2 import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver
3 import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
4 import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
5 import com.google.api.client.http.HttpTransport
6 import com.google.api.client.json.gson.GsonFactory
7 import com.google.api.client.util.store.DataStore
8 import com.google.api.client.util.store.DataStoreFactory
9 import com.google.api.client.util.store.FileDataStoreFactory
10 import com.google.api.services.androidbuildinternal.v3.Androidbuildinternal
11 import com.google.api.services.androidbuildinternal.v3.AndroidbuildinternalScopes
12 import org.apache.commons.compress.archivers.zip.ZipFile
13 import org.apache.commons.compress.utils.IOUtils
14 import org.gradle.api.DefaultTask
15 import org.gradle.api.tasks.Input
16 import org.gradle.api.tasks.TaskAction
17 import org.xml.sax.SAXException
18 import java.io.File
19 import java.io.FileOutputStream
20 import java.io.IOException
21 import java.net.HttpURLConnection
22 import java.net.URL
23 import java.nio.ByteBuffer
24 import java.nio.channels.Channels
25 import java.nio.channels.ReadableByteChannel
26 import java.nio.channels.SeekableByteChannel
27 import java.nio.file.Path
28 import java.security.GeneralSecurityException
29 import java.util.Collections
30 import java.util.logging.Logger
31 import javax.xml.parsers.DocumentBuilderFactory
32 import javax.xml.parsers.ParserConfigurationException
33
34 /**
35 * Task that grabs the most recent build from the branch under development (git_main, etc)
36 * This task uses the androidbuildinternal api jar to access build servers.
37 *
38 * The jar is found here:
39 * vendor/unbundled_google/libraries/androidbuildinternal/
40 *
41 * Their are several assumptions baked in to this code.
42 *
43 *
44 * * "sdk-trunk_staging" is a build target
45 * * "sdk-trunk_staging" containing a test artifact called: "android-all-robolectric.jar"
46 *
47 * To see builds and artifacts look here:
48 * https://android-build.corp.google.com/build_explorer/branch/git_main/?gridSize=20&activeTarget=sdk-trunk_staging&selectionType=START_BUILD_WINDOW&numBuilds=20
49 *
50 * The API called to find the latest build can be played with here:
51 * https://apis-explorer-internal.corp.google.com/?discoveryUrl=https:%2F%2Fwww.googleapis.com%2Fdiscovery%2Fv1%2Fapis%2Fandroidbuildinternal%2Fv3%2Frest&creds=public&methodId=androidbuildinternal.build.list
52 *
53 * If we want to try to be more precise a pull the current checkout's build the best option today
54 * is to guess which git project is the latest and use the API behind: go/wimcl to find the buildId
55 * that most closely matches the buildId the user is on. Unclear if this is necessary.
56 *
57 */
58 abstract class RoboJarFetcherTask : DefaultTask() {
59
60 @get:Input
61 abstract var rootPath: String
62
63 @get:Input
64 abstract var outPath: String
65
66 @get:Input
67 abstract var suggestedGitBranch: String
68
69 @get:Input
70 abstract var buildId: Long
71
72 @TaskAction
73 fun taskAction() {
74 println("Fetching android_all jar")
75 // Setting this property is needed in the gradle jvm to allow
76 // this task to start a web browser on its own rather than
77 // begging the user to do so in standard out.
78 val originalSysProp = System.getProperty("java.awt.headless")
79 System.setProperty("java.awt.headless", "false")
80 val generator = RoboJarFetcher(rootPath, outPath, suggestedGitBranch, buildId)
81 val path = generator.downloadAndroidJarFromServer()
82 System.setProperty("java.awt.headless", originalSysProp)
83 println("Jar downloaded at $path")
84 }
85 }
86
87 private class RoboJarFetcher(
88 private val rootPath: String,
89 private val outPath: String,
90 private val suggestedGitBranch: String,
91 private val buildId: Long
92 ) {
93
94 private val dataStoreFactory: DataStoreFactory
95 private val localProps: DataStore<Long>
96 private var client: Androidbuildinternal? = null
97 private var lastFetchedBuildId: Long = -1
98
99 init {
100 val dataDir = File(outPath, "gapi")
101 dataDir.mkdirs()
102 dataStoreFactory = FileDataStoreFactory(dataDir)
103 localProps = dataStoreFactory.getDataStore(LOCAL_PROPS)
104 }
105
106 /**
107 * Downloads and returns the jar for latest robolectric system image
108 */
109 @Throws(IOException::class)
downloadAndroidJarFromServernull110 fun downloadAndroidJarFromServer(): Path {
111 val jar = cachedJar()
112 if (jar != null) {
113 return jar
114 }
115
116 // Download from server
117 val buildId = latestBuildId()
118 LOGGER.info("Downloading jar for buildId $buildId")
119 val downloadUrl = buildClient().buildartifact()
120 .getdownloadurl(buildId.toString(), TARGET, "latest", "android-all-robolectric.jar")
121 .set("redirect", false)
122 .execute()
123 .signedUrl
124 LOGGER.info("Download url $downloadUrl")
125 val out = getFileForBuildNumber(buildId)
126 val tempFile = File(out.parentFile, out.name + ".tmp")
127 FileOutputStream(tempFile).use { o -> IOUtils.copy(URL(downloadUrl).openStream(), o) }
128 tempFile.renameTo(out)
129 localProps[KEY_BUILD_NUMBER] = buildId
130 localProps[KEY_EXPIRY_TIME] = System.currentTimeMillis() + EXPIRY_TIMEOUT
131
132 // Cleanup, delete all other files.
133 for (f in out.parentFile.listFiles()) {
134 if (f != out) {
135 f.delete()
136 }
137 }
138 return out.toPath()
139 }
140
141 @Throws(IOException::class)
cachedJarnull142 private fun cachedJar(): Path? {
143 val buildNumber = localProps[KEY_BUILD_NUMBER] ?: return null
144 val targetFile = getFileForBuildNumber(buildNumber)
145 if (!targetFile.exists()) {
146 return null
147 }
148 if (buildId != -1L) {
149 // If we want a fixed build number, ignore expiry check
150 return if (buildNumber == buildId) targetFile.toPath() else null
151 }
152
153 // Verify if this is still valid
154 var expiryTime = localProps[KEY_EXPIRY_TIME] ?: return null
155 if (expiryTime < System.currentTimeMillis()) {
156 // Check if we are still valid.
157 val latestBuildId = try {
158 latestBuildId()
159 } catch (e: Exception) {
160 LOGGER.warning("Error fetching buildId from build server, using existing jar")
161 return targetFile.toPath()
162 }
163 if (buildNumber != latestBuildId) {
164 // New build available, download and return that
165 return null
166 }
167 // Since we just verified, update the expiry
168 expiryTime = System.currentTimeMillis() + EXPIRY_TIMEOUT
169 localProps[KEY_EXPIRY_TIME] = expiryTime
170 }
171 return targetFile.toPath()
172 }
173
174 @Throws(IOException::class)
latestBuildIdnull175 private fun latestBuildId() : Long {
176 if (buildId >= 0) {
177 return buildId
178 }
179 if (lastFetchedBuildId > -1) {
180 return lastFetchedBuildId
181 }
182 val gitBranch = currentGitBranch()
183 val result = buildClient()
184 .build()
185 .list()
186 .setSortingType("buildId")
187 .setBuildType("submitted")
188 .setBranch("git_$gitBranch")
189 .setTarget(TARGET)
190 .setBuildAttemptStatus("complete")
191 .setSuccessful(true)
192 .setMaxResults(1L)
193 .execute()
194 val buildId = result.builds[0].buildId
195 LOGGER.info("Latest build id: $buildId")
196 lastFetchedBuildId = buildId.toLong()
197 return lastFetchedBuildId
198 }
199
200 @Throws(IOException::class)
buildClientnull201 private fun buildClient() : Androidbuildinternal {
202 if (client != null) {
203 return client as Androidbuildinternal
204 }
205 try {
206 val transport: HttpTransport = GoogleNetHttpTransport.newTrustedTransport()
207 val flow = GoogleAuthorizationCodeFlow.Builder(
208 transport,
209 GsonFactory.getDefaultInstance(),
210 CLIENT_ID, CLIENT_SECRET,
211 AndroidbuildinternalScopes.all())
212 .setDataStoreFactory(dataStoreFactory)
213 .setAccessType("offline")
214 .setApprovalPrompt("force")
215 .build()
216 val credential = AuthorizationCodeInstalledApp(flow,
217 LocalServerReceiver())
218 .authorize("user")
219 return Androidbuildinternal.Builder(
220 transport, GsonFactory.getDefaultInstance(), credential)
221 .build().also { client = it }
222 } catch (gse: GeneralSecurityException) {
223 throw IOException(gse)
224 }
225 }
226
227 @Throws(IOException::class)
currentGitBranchnull228 fun currentGitBranch(): String {
229 if (suggestedGitBranch.isNotEmpty()) {
230 return suggestedGitBranch
231 }
232
233 // Try to find from repo manifest
234 val manifest = File("$rootPath/.repo/manifests/default.xml")
235 return try {
236 DocumentBuilderFactory.newInstance()
237 .newDocumentBuilder()
238 .parse(manifest)
239 .getElementsByTagName("default")
240 .item(0)
241 .attributes
242 .getNamedItem("revision")
243 .nodeValue
244 } catch (ex: ParserConfigurationException) {
245 throw IOException(ex)
246 } catch (ex: SAXException) {
247 throw IOException(ex)
248 }
249 }
250
getFileForBuildNumbernull251 private fun getFileForBuildNumber(buildNumber: Long): File {
252 val dir = File(outPath, "android_all")
253 dir.mkdirs()
254 return File(dir, "android-all-robolectric-$buildNumber.jar")
255 }
256
257 companion object {
258 private val LOGGER = Logger.getLogger("RoboJarFetcher")
259 private const val CLIENT_ID =
260 "547163898880-gm920odpvl47ba6cpehjsna4ef978739.apps.googleusercontent.com"
261 private const val CLIENT_SECRET = "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"
262 private const val TARGET = "sdk-trunk_staging"
263 private const val LOCAL_PROPS = "local_props"
264 private const val KEY_EXPIRY_TIME = "expiry_time"
265 private const val KEY_BUILD_NUMBER = "build_number"
266 private const val EXPIRY_TIMEOUT = (60 * 60 * 1000 * 48).toLong() // 2 days
267 }
268 }
269
270 private class RemoteByteChannel(urlString: String?) : SeekableByteChannel {
271 private val url: URL
272 private val size: Long
273 private var requestedPos: Long? = null
274 private var currentPos: Long = 0
275 private var currentConn: HttpURLConnection? = null
276 private var currentChannel: ReadableByteChannel? = null
277
278 init {
279 url = URL(urlString)
280 val conn = newConn()
281 conn.requestMethod = "HEAD"
282 conn.connect()
283 conn.responseCode
284 size = conn.getHeaderField("content-length").toLong()
285 conn.disconnect()
286 }
287
closeCurrentConnnull288 private fun closeCurrentConn() {
289 currentChannel?.let (IOUtils::closeQuietly)
290 currentChannel = null
291
292 currentConn?.let(HttpURLConnection::disconnect)
293 currentConn = null
294 }
295
296 @Throws(IOException::class)
newConnnull297 private fun newConn(): HttpURLConnection {
298 closeCurrentConn()
299 return (url.openConnection() as HttpURLConnection).also { currentConn = it }
300 }
301
302 @Throws(IOException::class)
readnull303 override fun read(byteBuffer: ByteBuffer): Int {
304 requestedPos?.let {
305 if (it != currentPos) {
306 currentPos = it
307 closeCurrentConn()
308 }
309 }
310 requestedPos = null
311
312 if (currentChannel == null) {
313 val conn = newConn()
314 conn.setRequestProperty("Range", "bytes=$currentPos-")
315 conn.connect()
316 currentChannel = Channels.newChannel(conn.inputStream)
317 }
318 val expected = byteBuffer.remaining()
319 IOUtils.readFully(currentChannel, byteBuffer)
320 val remaining = byteBuffer.remaining()
321 currentPos += (expected - remaining).toLong()
322 return expected - remaining
323 }
324
325 @Throws(IOException::class)
writenull326 override fun write(byteBuffer: ByteBuffer): Int {
327 throw IOException("Not supported")
328 }
329
positionnull330 override fun position(): Long {
331 return requestedPos ?: currentPos
332 }
333
positionnull334 override fun position(l: Long): SeekableByteChannel {
335 requestedPos = l
336 return this
337 }
338
sizenull339 override fun size(): Long {
340 return size
341 }
342
343 @Throws(IOException::class)
truncatenull344 override fun truncate(l: Long): SeekableByteChannel {
345 throw IOException("Not supported")
346 }
347
isOpennull348 override fun isOpen(): Boolean {
349 return currentChannel != null
350 }
351
closenull352 override fun close() {
353 closeCurrentConn()
354 }
355 }
356