• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<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