feat(cli): show download progress and copy setup links

This commit is contained in:
Lemon-miaow committed 2026-10-04 15:49:21 +08:00
1 parent e4a2a6339a
commit e67896979e
12 files changed
+230 -17

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
import org.gradle.api.logging.Logging
import org.gradle.internal.operations.BuildOperationListener
import org.gradle.internal.operations.BuildOperationListenerManager
import org.gradle.internal.operations.OperationProgressDetails
import org.gradle.internal.resource.ExternalResourceReadBuildOperationType
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
// The Docker images pin Gradle. Observe its download events without changing requests,
// dependency verification or caching; plain console output otherwise hides these events.
def logger = Logging.getLogger('felis.download')
def downloads = new ConcurrentHashMap()
def bytes = { long value ->
value >= 1024 * 1024 ? String.format(Locale.ROOT, '%.1f MiB', value / (1024.0 * 1024)) :
String.format(Locale.ROOT, '%.1f KiB', value / 1024.0)
}
def seconds = { long since -> TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - since) }
def listener = [
started: { operation, event ->
if (!(operation.details instanceof ExternalResourceReadBuildOperationType.Details)) return
def uri = URI.create(operation.details.location)
if (!(uri.scheme in ['http', 'https'])) return
def now = System.nanoTime()
def name = "${uri.path.tokenize('/').last()} (${uri.host})"
downloads[operation.id] = [name: name, bytes: 0L, total: 0L, started: now,
changed: now, sampled: now, sampledBytes: 0L]
logger.lifecycle("[felis] Downloading ${name}")
},
progress: { id, event ->
def download = downloads[id]
if (download == null || !(event.details instanceof OperationProgressDetails)) return
if (event.details.units != 'bytes') return
synchronized (download) {
if (event.details.progress > download.bytes) download.changed = System.nanoTime()
download.bytes = event.details.progress
download.total = event.details.total
}
},
finished: { operation, event ->
def download = downloads.remove(operation.id)
if (download == null) return
def outcome = event.failure != null ? 'Download failed' :
event.result?.missing ? 'Not found' : 'Downloaded'
logger.lifecycle("[felis] ${outcome} ${download.name} (${seconds(download.started)}s)")
}
] as BuildOperationListener
def manager = gradle.services.get(BuildOperationListenerManager)
manager.addListener(listener)
def timer = Executors.newSingleThreadScheduledExecutor({ task ->
def thread = new Thread(task, 'felis-download-progress')
thread.daemon = true
thread
} as java.util.concurrent.ThreadFactory)
timer.scheduleAtFixedRate({
downloads.values().each { download ->
synchronized (download) {
def now = System.nanoTime()
def rate = (long) ((download.bytes - download.sampledBytes) * 1e9 / (now - download.sampled))
def total = download.total > 0 ? bytes(download.total) : 'unknown size'
def percent = download.total > 0 ? " (${Math.min(100, (100L * download.bytes).intdiv(download.total))}%)" : ''
def idle = seconds(download.changed)
def waiting = idle >= 30 ? " | no new data for ${idle}s; waiting for the download" : ''
logger.lifecycle("[felis] ${download.name}: ${bytes(download.bytes)} / ${total}${percent} | ${bytes(rate)}/s | ${seconds(download.started)}s elapsed${waiting}")
download.sampled = now
download.sampledBytes = download.bytes
}
}
} as Runnable, 10, 10, TimeUnit.SECONDS)
gradle.buildFinished {
timer.shutdownNow()
manager.removeListener(listener)
}