1+ /* This Source Code Form is subject to the terms of the Mozilla Public
2+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+ package org.mozilla.tooling.nimbus
6+
7+ import org.gradle.api.Action
8+ import org.gradle.api.DefaultTask
9+ import org.gradle.api.GradleException
10+ import org.gradle.api.file.ArchiveOperations
11+ import org.gradle.api.file.FileVisitDetails
12+ import org.gradle.api.file.RegularFileProperty
13+ import org.gradle.api.model.ObjectFactory
14+ import org.gradle.api.provider.ListProperty
15+ import org.gradle.api.provider.Property
16+ import org.gradle.api.tasks.CacheableTask
17+ import org.gradle.api.tasks.Input
18+ import org.gradle.api.tasks.LocalState
19+ import org.gradle.api.tasks.Nested
20+ import org.gradle.api.tasks.OutputFile
21+ import org.gradle.api.tasks.TaskAction
22+
23+ import javax.inject.Inject
24+
25+ import groovy.transform.Immutable
26+
27+ /**
28+ * A task that fetches a prebuilt `nimbus-fml` binary for the current platform.
29+ *
30+ * Prebuilt binaries for all platforms are packaged into ZIP archives, and
31+ * published to sources like `archive.mozilla.org` (for releases) or
32+ * TaskCluster (for nightly builds).
33+ *
34+ * This task takes a variable number of inputs: a list of archive sources,
35+ * and a list of glob patterns to find the binary for the current platform
36+ * in the archive.
37+ *
38+ * The unzipped binary is this task's only output. This output is then used as
39+ * an optional input to the `NimbusFmlCommandTask`s.
40+ */
41+ @CacheableTask
42+ abstract class NimbusAssembleToolsTask extends DefaultTask {
43+ @Inject
44+ abstract ArchiveOperations getArchiveOperations ()
45+
46+ @Nested
47+ abstract FetchSpec getFetchSpec ()
48+
49+ @Nested
50+ abstract UnzipSpec getUnzipSpec ()
51+
52+ /* * The location of the fetched ZIP archive. */
53+ @LocalState
54+ abstract RegularFileProperty getArchiveFile ()
55+
56+ /**
57+ * The location of the fetched hash file, which contains the
58+ * archive's checksum.
59+ */
60+ @LocalState
61+ abstract RegularFileProperty getHashFile ()
62+
63+ /* * The location of the unzipped binary. */
64+ @OutputFile
65+ abstract RegularFileProperty getFmlBinary ()
66+
67+ /**
68+ * Configures the task to download the archive.
69+ *
70+ * @param action The configuration action.
71+ */
72+ void fetch (Action<FetchSpec > action ) {
73+ action. execute(fetchSpec)
74+ }
75+
76+ /**
77+ * Configures the task to extract the binary from the archive.
78+ *
79+ * @param action The configuration action.
80+ */
81+ void unzip (Action<UnzipSpec > action ) {
82+ action. execute(unzipSpec)
83+ }
84+
85+ @TaskAction
86+ void assembleTools () {
87+ def sources = [fetchSpec, * fetchSpec. fallbackSources. get()]. collect {
88+ new Source (new URI (it. archive. get()), new URI (it. hash. get()))
89+ }
90+
91+ def successfulSource = sources. find { it. trySaveArchiveTo(archiveFile. get(). asFile) }
92+ if (successfulSource == null ) {
93+ throw new GradleException (" Couldn't fetch archive from any of: ${ sources*.archiveURI.collect { "`$it`" }.join(', ')} " )
94+ }
95+
96+ // We get the checksum, although don't do anything with it yet;
97+ // Checking it here would be able to detect if the zip file was tampered with
98+ // in transit between here and the server.
99+ // It won't detect compromise of the CI server.
100+ try {
101+ successfulSource. saveHashTo(hashFile. get(). asFile)
102+ } catch (IOException e) {
103+ throw new GradleException (" Couldn't fetch hash from `${ successfulSource.hashURI} `" , e)
104+ }
105+
106+ def zipTree = archiveOperations. zipTree(archiveFile. get())
107+ def visitedFilePaths = []
108+ zipTree. matching {
109+ include unzipSpec. includePatterns. get()
110+ }. visit { FileVisitDetails details ->
111+ if (! details. directory) {
112+ if (visitedFilePaths. empty) {
113+ details. copyTo(fmlBinary. get(). asFile)
114+ fmlBinary. get(). asFile. setExecutable(true )
115+ }
116+ visitedFilePaths. add(details. relativePath)
117+ }
118+ }
119+
120+ if (visitedFilePaths. empty) {
121+ throw new GradleException (" Couldn't find any files in archive matching unzip spec: (${ unzipSpec.includePatterns.get().collect { "`$it`" }.join(' | ')} )" )
122+ }
123+
124+ if (visitedFilePaths. size() > 1 ) {
125+ throw new GradleException (" Ambiguous unzip spec matched ${ visitedFilePaths.size()} files in archive: ${ visitedFilePaths.collect { "`$it`" }.join(', ')} " )
126+ }
127+ }
128+
129+ /**
130+ * Specifies the source from which to fetch the archive and
131+ * its hash file.
132+ */
133+ static abstract class FetchSpec extends SourceSpec {
134+ @Inject
135+ abstract ObjectFactory getObjectFactory ()
136+
137+ @Nested
138+ abstract ListProperty<SourceSpec > getFallbackSources ()
139+
140+ /**
141+ * Configures a fallback to try if the archive can't be fetched
142+ * from this source.
143+ *
144+ * The task will try fallbacks in the order in which they're
145+ * configured.
146+ *
147+ * @param action The configuration action.
148+ */
149+ void fallback (Action<SourceSpec > action ) {
150+ def spec = objectFactory. newInstance(SourceSpec )
151+ action(spec)
152+ fallbackSources. add(spec)
153+ }
154+ }
155+
156+ /* * Specifies the URL of an archive and its hash file. */
157+ static abstract class SourceSpec {
158+ @Input
159+ abstract Property<String > getArchive ()
160+
161+ @Input
162+ abstract Property<String > getHash ()
163+ }
164+
165+ /**
166+ * Specifies which binary to extract from the fetched archive.
167+ *
168+ * The spec should only match one file in the archive. If the spec
169+ * matches multiple files in the archive, the task will fail.
170+ */
171+ static abstract class UnzipSpec {
172+ @Input
173+ abstract ListProperty<String > getIncludePatterns ()
174+
175+ /**
176+ * Includes all files whose paths match the pattern.
177+ *
178+ * @param pattern An Ant-style glob pattern.
179+ * @see org.gradle.api.tasks.util.PatternFilterable#include
180+ */
181+ void include (String pattern ) {
182+ includePatterns. add(pattern)
183+ }
184+ }
185+
186+ /* * A helper to fetch an archive and its hash file. */
187+ @Immutable
188+ static class Source {
189+ URI archiveURI
190+ URI hashURI
191+
192+ boolean trySaveArchiveTo (File destination ) {
193+ try {
194+ saveURITo(archiveURI, destination)
195+ true
196+ } catch (IOException ignored) {
197+ false
198+ }
199+ }
200+
201+ void saveHashTo (File destination ) {
202+ saveURITo(hashURI, destination)
203+ }
204+
205+ private static void saveURITo (URI source , File destination ) {
206+ source. toURL(). withInputStream { from ->
207+ destination. withOutputStream { out ->
208+ out << from
209+ }
210+ }
211+ }
212+ }
213+ }
0 commit comments