2
0

bootstrap.bmx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. '
  2. ' Copyright 2018 Bruce A Henderson
  3. '
  4. ' Licensed under the Apache License, Version 2.0 (the "License"); you may not
  5. ' use this file except in compliance with the License. You may obtain a copy of
  6. ' 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, WITHOUT
  12. ' WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. ' License for the specific language governing permissions and limitations under
  14. ' the License.
  15. '
  16. '
  17. ' bootstrap starter
  18. '
  19. '
  20. '
  21. SuperStrict
  22. Framework brl.standardio
  23. Import brl.filesystem
  24. Import brl.stringbuilder
  25. Import bah.libarchive
  26. Import bah.libcurl
  27. Import bah.format
  28. Const APP_TITLE:String = "BlitzMax bootstrap"
  29. Const VERSION:String = "1.0.0"
  30. Const BLOCK_SIZE:Int = 65536
  31. Print APP_TITLE + " v" + VERSION
  32. Print ""
  33. Local options:TOptions = New TOptions()
  34. options.ParseArgs(AppArgs[1..])
  35. Local config:TConfig = TConfig.Load(options)
  36. If options.verbose Then
  37. Print options.ToString()
  38. End If
  39. config.ProcessDownloads()
  40. End
  41. Type TOptions
  42. Field target:String
  43. Field config:String
  44. Field force:Int
  45. Field verbose:Int
  46. Field canShowProgress:Int
  47. Method New()
  48. canShowProgress = _isatty(0)
  49. config = "boot_files.txt"
  50. InitTarget()
  51. End Method
  52. Method ParseArgs(args:String[])
  53. Local i:Int
  54. For i = 0 Until args.length
  55. Local arg:String = args[i]
  56. If arg[..1] <> "-" Then
  57. Exit
  58. End If
  59. Select arg[1..]
  60. Case "t"
  61. i:+1
  62. CheckArg(i, args.length, "t")
  63. target = args[i].ToLower()
  64. Case "c"
  65. i:+1
  66. CheckArg(i, args.length, "c")
  67. config = args[i].ToLower()
  68. Case "f"
  69. force = True
  70. Case "v"
  71. verbose = True
  72. Case "h"
  73. ShowUsage()
  74. End
  75. End Select
  76. Next
  77. End Method
  78. Method ShowUsage()
  79. Print "usage: bootstrap [options]"
  80. Print " -c <filename> Specific configuration to load"
  81. Print " -f Force install if assets already exist"
  82. Print " -h Help"
  83. Print " -t Set bootstrap target"
  84. Print " Default : " + target
  85. Print " -v Verbose output"
  86. Print ""
  87. End Method
  88. Method CheckArg(index:Int, length:Int, option:String)
  89. If index = length Then
  90. Throw "Missing arg for '-" + option + "'"
  91. End If
  92. End Method
  93. Method ToString:String()
  94. Local sb:TStringBuilder = New TStringBuilder()
  95. sb.Append("Target = ").Append(target).AppendNewLine()
  96. sb.Append("Force = ").AppendInt(force).AppendNewLine()
  97. sb.Append("TTY = ").AppendInt(canShowProgress).AppendNewLine()
  98. Return sb.ToString()
  99. End Method
  100. Method InitTarget()
  101. ?win32
  102. target = "win32"
  103. ?linux
  104. target = "linux"
  105. ?macos
  106. target = "macos"
  107. ?x86
  108. target :+ "x86"
  109. ?x64
  110. target :+ "x64"
  111. ?arm
  112. target :+ "arm"
  113. ?arm64
  114. target :+ "arm64"
  115. ?
  116. End Method
  117. Method MatchesTarget:Int(otherTarget:String)
  118. Return target = otherTarget Or otherTarget = "any" Or target.StartsWith(otherTarget)
  119. End Method
  120. End Type
  121. Type TConfig
  122. Field downloads:TList = New TList
  123. Field options:TOptions
  124. Function Load:TConfig(options:TOptions)
  125. Local this:TConfig = New TConfig()
  126. this.options = options
  127. Local config:String = LoadText(options.config)
  128. For Local line:String = EachIn config.Split("~n")
  129. line = line.Trim()
  130. If line Then
  131. this.downloads.AddLast(TDownLoad.Create(line))
  132. End If
  133. Next
  134. Return this
  135. End Function
  136. Method ProcessDownloads()
  137. For Local download:TDownload = EachIn downloads
  138. If download.Download(options) Then
  139. download.Unpack(options)
  140. End If
  141. Next
  142. End Method
  143. End Type
  144. Type TDownload
  145. Field target:String
  146. Field filename:String
  147. Field uri:String
  148. Field extractFolder:String
  149. Field finalFolder:String
  150. Function Create:TDownload(line:String)
  151. Local download:TDownload = New TDownload
  152. Local fields:String[] = line.Split("~t")
  153. If fields.length < 3 Then
  154. Return Null
  155. End If
  156. download.target = fields[0]
  157. download.filename = fields[1]
  158. download.uri = fields[2]
  159. If fields.length >= 5 Then
  160. download.extractFolder = fields[3]
  161. download.finalFolder = fields[4]
  162. End If
  163. Return download
  164. End Function
  165. Method Download:Int(options:TOptions)
  166. Clear()
  167. If target And (Not options.MatchesTarget(target)) Then
  168. Return False
  169. End If
  170. If FileType(finalFolder) = FILETYPE_DIR And Not options.force Then
  171. Print "Folder '" + finalFolder + "' already exists. Skipping..."
  172. Return True
  173. End If
  174. If FileType(filename) And Not options.force Then
  175. Print "File '" + filename + "' already exists. Skipping download..."
  176. Return True
  177. End If
  178. Print "Downloading '" + filename + "'"
  179. Local stream:TStream = WriteStream(filename)
  180. Local curl:TCurlEasy = TCurlEasy.Create()
  181. curl.setOptInt(CURLOPT_FOLLOWLOCATION, 1)
  182. curl.setOptInt(CURLOPT_SSL_VERIFYPEER, False)
  183. Local progress:TProgress
  184. If options.canShowProgress Then
  185. progress = New TProgress
  186. curl.setProgressCallback(ShowProgress, progress)
  187. End If
  188. curl.setOptString(CURLOPT_URL, uri)
  189. ' redirect data to stream
  190. curl.setWriteStream(stream)
  191. Local result:Int = curl.perform()
  192. If result Then
  193. Throw "Error downloading file : " + CurlError(result)
  194. End If
  195. curl.cleanup()
  196. stream.Close()
  197. Return True
  198. End Method
  199. Method Unpack(options:TOptions)
  200. Clear()
  201. If FileType(finalFolder) = FILETYPE_DIR And Not options.force Then
  202. Return
  203. End If
  204. ' if the folder exists, then we are here by force. Attempt to delete it:
  205. If FileType(finalFolder) = FILETYPE_DIR Then
  206. If options.verbose Then
  207. Print "Removing folder '" + finalFolder + "'"
  208. End If
  209. If Not DeleteDir(finalFolder, True)
  210. Throw "Cannot delete folder '" + finalFolder + "'"
  211. End If
  212. End If
  213. Local unpacker:TUnPacker = New TUnPacker(Self)
  214. unpacker.Unpack(options)
  215. If Not FileType(extractFolder) Then
  216. Throw "Cannot find unpacked folder '" + extractFolder + "'"
  217. End If
  218. If FileType(extractFolder) = FILETYPE_DIR Then
  219. If options.verbose Then
  220. Print "Renaming '" + extractFolder + "'"
  221. End If
  222. If Not RenameFile(extractFolder, finalFolder) Then
  223. Throw "Unable to rename folder '" + extractFolder + "' to '" + finalFolder + "'"
  224. End If
  225. End If
  226. End Method
  227. Function ShowProgress:Int(data:Object, dltotal:Long, dlnow:Long, ultotal:Long, ulnow:Long)
  228. Local progress:TProgress = TProgress(data)
  229. progress.ShowProgress(dltotal, dlnow)
  230. End Function
  231. End Type
  232. Type TUnPacker
  233. Field download:TDownload
  234. Field archive:TReadArchive
  235. Field entry:TArchiveEntry
  236. Method New(download:TDownload)
  237. Self.download = download
  238. entry = New TArchiveEntry.Create()
  239. End Method
  240. Method UnPack(options:TOptions)
  241. Clear()
  242. Print "Unpacking '" + download.filename + "'"
  243. Local total:Int = EntryCount()
  244. Open()
  245. Local progress:TProgress
  246. If options.canShowProgress Then
  247. progress = New TProgress
  248. End If
  249. Local count:Int
  250. While archive.ReadNextHeader(entry) = ARCHIVE_OK
  251. Local path:String = entry.Pathname()
  252. Local dir:String = ExtractDir(path)
  253. CreateDir(dir, True)
  254. If Not path.EndsWith("/") Then
  255. Local stream:TStream = WriteStream(path)
  256. CopyStream(archive.DataStream(), stream, BLOCK_SIZE)
  257. stream.Close()
  258. End If
  259. count :+ 1
  260. If progress Then
  261. progress.ShowProgress(total, count)
  262. End If
  263. Wend
  264. archive.Free()
  265. If options.canShowProgress Then
  266. Clear()
  267. End If
  268. End Method
  269. Method Open()
  270. archive = New TReadArchive.Create()
  271. archive.SupportFilterAll()
  272. archive.SupportFormatAll()
  273. Local result:Int = archive.OpenFilename(download.filename, BLOCK_SIZE)
  274. If result <> ARCHIVE_OK Then
  275. Throw "Error opening " + download.filename
  276. End If
  277. End Method
  278. Method EntryCount:Int()
  279. Local count:Int
  280. Open()
  281. While archive.ReadNextHeader(entry) = ARCHIVE_OK
  282. archive.DataSkip()
  283. count :+ 1
  284. Wend
  285. archive.Free()
  286. Return count
  287. End Method
  288. End Type
  289. Type TProgress
  290. Const full:String = " "
  291. Const space:String = " "
  292. Const bar:String = "##########################################################"
  293. Field length:Int
  294. Field progress:Int
  295. Field spin:Int
  296. Field spinText:String = "\|/-"
  297. Field lastUpdate:Int
  298. Field startTime:Int
  299. Global formatter:TFormatter = TFormatter.Create(" %02d:%02d:%02d")
  300. Method New()
  301. length = bar.length
  302. Clear()
  303. Put "~r[" + space + "] --:--:--~r"
  304. startTime = MilliSecs()
  305. End Method
  306. Method ShowProgress(dltotal:Long, dlnow:Long)
  307. Local sofar:Float = Float(dlnow) / dltotal
  308. Local count:Int = length * sofar
  309. Local now:Int = MilliSecs()
  310. If dltotal <> 0 Then
  311. Local timeTaken:Int = now - startTime
  312. Local timeLeft:Int = 0
  313. If dlnow > 0 Then
  314. timeLeft = (timeTaken / Double(dlnow)) * (dltotal - dlnow)
  315. Else
  316. timeLeft = 9999999
  317. End If
  318. If progress <> count Or now > lastUpdate + 1000 Then
  319. Put "~r["
  320. Local sofar:Float = Float(dlnow) / dltotal
  321. Local s:String = bar[..length * sofar]
  322. s :+ space[..length - (length * sofar)]
  323. s :+ " "
  324. Put s[..length]
  325. Put "] " + FormatTime(timeLeft / 1000) + "~r"
  326. progress = count
  327. lastUpdate = now
  328. End If
  329. Else
  330. ' spinner
  331. If now > lastUpdate + 500 Then
  332. Put "~r[ " + spinText[spin..spin+1] + "~r"
  333. spin :+ 1
  334. If spin = spinText.Length Then
  335. spin = 0
  336. End If
  337. lastUpdate = now
  338. End If
  339. End If
  340. End Method
  341. Method FormatTime:String(time:Int)
  342. Local seconds:Int = time Mod 60
  343. Local minutes:Int = (time / 60) Mod 60
  344. Local hours:Int = time / 3600
  345. formatter.Clear()
  346. If hours > 99 Then
  347. formatter.IntArg(99).IntArg(59).IntArg(59)
  348. Else
  349. formatter.IntArg(hours).IntArg(minutes).IntArg(seconds)
  350. End If
  351. Return formatter.format()
  352. End Method
  353. End Type
  354. Function Put( str$="" )
  355. StandardIOStream.WriteString str
  356. StandardIOStream.Flush
  357. End Function
  358. Function Clear()
  359. Put "~r " + TProgress.full + "~r"
  360. End Function
  361. Extern
  362. Function _isatty:Int(fd:Int)
  363. End Extern