123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495 |
- '
- ' Copyright 2018 Bruce A Henderson
- '
- ' Licensed under the Apache License, Version 2.0 (the "License"); you may not
- ' use this file except in compliance with the License. You may obtain a copy of
- ' the License at
- '
- ' http://www.apache.org/licenses/LICENSE-2.0
- '
- ' Unless required by applicable law or agreed to in writing, software
- ' distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- ' WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- ' License for the specific language governing permissions and limitations under
- ' the License.
- '
- '
- ' bootstrap starter
- '
- '
- '
- SuperStrict
- Framework brl.standardio
- Import brl.filesystem
- Import brl.stringbuilder
- Import bah.libarchive
- Import bah.libcurl
- Import bah.format
- Const APP_TITLE:String = "BlitzMax bootstrap"
- Const VERSION:String = "1.0.0"
- Const BLOCK_SIZE:Int = 65536
- Print APP_TITLE + " v" + VERSION
- Print ""
- Local options:TOptions = New TOptions()
- options.ParseArgs(AppArgs[1..])
- Local config:TConfig = TConfig.Load(options)
- If options.verbose Then
- Print options.ToString()
- End If
- config.ProcessDownloads()
- End
- Type TOptions
- Field target:String
- Field config:String
- Field force:Int
- Field verbose:Int
-
- Field canShowProgress:Int
- Method New()
- canShowProgress = _isatty(0)
- config = "boot_files.txt"
- InitTarget()
- End Method
-
- Method ParseArgs(args:String[])
- Local i:Int
- For i = 0 Until args.length
- Local arg:String = args[i]
- If arg[..1] <> "-" Then
- Exit
- End If
- Select arg[1..]
- Case "t"
- i:+1
- CheckArg(i, args.length, "t")
- target = args[i].ToLower()
- Case "c"
- i:+1
- CheckArg(i, args.length, "c")
- config = args[i].ToLower()
- Case "f"
- force = True
- Case "v"
- verbose = True
- Case "h"
- ShowUsage()
- End
- End Select
- Next
- End Method
-
- Method ShowUsage()
- Print "usage: bootstrap [options]"
- Print " -c <filename> Specific configuration to load"
- Print " -f Force install if assets already exist"
- Print " -h Help"
- Print " -t Set bootstrap target"
- Print " Default : " + target
- Print " -v Verbose output"
- Print ""
- End Method
- Method CheckArg(index:Int, length:Int, option:String)
- If index = length Then
- Throw "Missing arg for '-" + option + "'"
- End If
- End Method
-
- Method ToString:String()
- Local sb:TStringBuilder = New TStringBuilder()
- sb.Append("Target = ").Append(target).AppendNewLine()
- sb.Append("Force = ").AppendInt(force).AppendNewLine()
- sb.Append("TTY = ").AppendInt(canShowProgress).AppendNewLine()
- Return sb.ToString()
- End Method
-
- Method InitTarget()
- ?win32
- target = "win32"
- ?linux
- target = "linux"
- ?macos
- target = "macos"
- ?x86
- target :+ "x86"
- ?x64
- target :+ "x64"
- ?arm
- target :+ "arm"
- ?arm64
- target :+ "arm64"
- ?
- End Method
-
- Method MatchesTarget:Int(otherTarget:String)
- Return target = otherTarget Or otherTarget = "any" Or target.StartsWith(otherTarget)
- End Method
-
- End Type
- Type TConfig
- Field downloads:TList = New TList
- Field options:TOptions
-
- Function Load:TConfig(options:TOptions)
- Local this:TConfig = New TConfig()
- this.options = options
-
- Local config:String = LoadText(options.config)
- For Local line:String = EachIn config.Split("~n")
- line = line.Trim()
- If line Then
- this.downloads.AddLast(TDownLoad.Create(line))
- End If
- Next
-
- Return this
- End Function
- Method ProcessDownloads()
- For Local download:TDownload = EachIn downloads
- If download.Download(options) Then
- download.Unpack(options)
- End If
- Next
- End Method
-
- End Type
- Type TDownload
- Field target:String
- Field filename:String
- Field uri:String
- Field extractFolder:String
- Field finalFolder:String
- Function Create:TDownload(line:String)
- Local download:TDownload = New TDownload
- Local fields:String[] = line.Split("~t")
- If fields.length < 3 Then
- Return Null
- End If
-
- download.target = fields[0]
- download.filename = fields[1]
- download.uri = fields[2]
-
- If fields.length >= 5 Then
- download.extractFolder = fields[3]
- download.finalFolder = fields[4]
- End If
-
- Return download
- End Function
- Method Download:Int(options:TOptions)
- Clear()
-
- If target And (Not options.MatchesTarget(target)) Then
- Return False
- End If
-
- If FileType(finalFolder) = FILETYPE_DIR And Not options.force Then
- Print "Folder '" + finalFolder + "' already exists. Skipping..."
- Return True
- End If
-
- If FileType(filename) And Not options.force Then
- Print "File '" + filename + "' already exists. Skipping download..."
- Return True
- End If
-
- Print "Downloading '" + filename + "'"
- Local stream:TStream = WriteStream(filename)
- Local curl:TCurlEasy = TCurlEasy.Create()
-
- curl.setOptInt(CURLOPT_FOLLOWLOCATION, 1)
- curl.setOptInt(CURLOPT_SSL_VERIFYPEER, False)
-
- Local progress:TProgress
-
- If options.canShowProgress Then
- progress = New TProgress
- curl.setProgressCallback(ShowProgress, progress)
- End If
-
- curl.setOptString(CURLOPT_URL, uri)
-
- ' redirect data to stream
- curl.setWriteStream(stream)
-
- Local result:Int = curl.perform()
- If result Then
- Throw "Error downloading file : " + CurlError(result)
- End If
-
- curl.cleanup()
-
- stream.Close()
-
- Return True
- End Method
-
- Method Unpack(options:TOptions)
- Clear()
- If FileType(finalFolder) = FILETYPE_DIR And Not options.force Then
- Return
- End If
-
- ' if the folder exists, then we are here by force. Attempt to delete it:
- If FileType(finalFolder) = FILETYPE_DIR Then
- If options.verbose Then
- Print "Removing folder '" + finalFolder + "'"
- End If
- If Not DeleteDir(finalFolder, True)
- Throw "Cannot delete folder '" + finalFolder + "'"
- End If
- End If
-
- Local unpacker:TUnPacker = New TUnPacker(Self)
- unpacker.Unpack(options)
-
- If Not FileType(extractFolder) Then
- Throw "Cannot find unpacked folder '" + extractFolder + "'"
- End If
-
- If FileType(extractFolder) = FILETYPE_DIR Then
- If options.verbose Then
- Print "Renaming '" + extractFolder + "'"
- End If
- If Not RenameFile(extractFolder, finalFolder) Then
- Throw "Unable to rename folder '" + extractFolder + "' to '" + finalFolder + "'"
- End If
- End If
-
- End Method
- Function ShowProgress:Int(data:Object, dltotal:Long, dlnow:Long, ultotal:Long, ulnow:Long)
- Local progress:TProgress = TProgress(data)
- progress.ShowProgress(dltotal, dlnow)
- End Function
-
- End Type
- Type TUnPacker
- Field download:TDownload
-
- Field archive:TReadArchive
- Field entry:TArchiveEntry
- Method New(download:TDownload)
- Self.download = download
-
- entry = New TArchiveEntry.Create()
- End Method
- Method UnPack(options:TOptions)
- Clear()
-
- Print "Unpacking '" + download.filename + "'"
- Local total:Int = EntryCount()
-
- Open()
-
- Local progress:TProgress
-
- If options.canShowProgress Then
- progress = New TProgress
- End If
-
- Local count:Int
-
- While archive.ReadNextHeader(entry) = ARCHIVE_OK
-
- Local path:String = entry.Pathname()
- Local dir:String = ExtractDir(path)
-
- CreateDir(dir, True)
-
- If Not path.EndsWith("/") Then
- Local stream:TStream = WriteStream(path)
-
- CopyStream(archive.DataStream(), stream, BLOCK_SIZE)
-
- stream.Close()
- End If
-
- count :+ 1
- If progress Then
- progress.ShowProgress(total, count)
- End If
- Wend
- archive.Free()
- If options.canShowProgress Then
- Clear()
- End If
- End Method
-
- Method Open()
- archive = New TReadArchive.Create()
- archive.SupportFilterAll()
- archive.SupportFormatAll()
- Local result:Int = archive.OpenFilename(download.filename, BLOCK_SIZE)
-
- If result <> ARCHIVE_OK Then
- Throw "Error opening " + download.filename
- End If
- End Method
-
- Method EntryCount:Int()
- Local count:Int
-
- Open()
- While archive.ReadNextHeader(entry) = ARCHIVE_OK
- archive.DataSkip()
- count :+ 1
- Wend
- archive.Free()
- Return count
- End Method
- End Type
- Type TProgress
- Const full:String = " "
- Const space:String = " "
- Const bar:String = "##########################################################"
-
- Field length:Int
- Field progress:Int
-
- Field spin:Int
- Field spinText:String = "\|/-"
-
- Field lastUpdate:Int
- Field startTime:Int
-
- Global formatter:TFormatter = TFormatter.Create(" %02d:%02d:%02d")
-
- Method New()
- length = bar.length
- Clear()
- Put "~r[" + space + "] --:--:--~r"
-
- startTime = MilliSecs()
- End Method
- Method ShowProgress(dltotal:Long, dlnow:Long)
- Local sofar:Float = Float(dlnow) / dltotal
- Local count:Int = length * sofar
-
- Local now:Int = MilliSecs()
-
- If dltotal <> 0 Then
- Local timeTaken:Int = now - startTime
- Local timeLeft:Int = 0
- If dlnow > 0 Then
- timeLeft = (timeTaken / Double(dlnow)) * (dltotal - dlnow)
- Else
- timeLeft = 9999999
- End If
- If progress <> count Or now > lastUpdate + 1000 Then
- Put "~r["
- Local sofar:Float = Float(dlnow) / dltotal
-
- Local s:String = bar[..length * sofar]
- s :+ space[..length - (length * sofar)]
- s :+ " "
- Put s[..length]
- Put "] " + FormatTime(timeLeft / 1000) + "~r"
-
- progress = count
- lastUpdate = now
- End If
- Else
- ' spinner
-
- If now > lastUpdate + 500 Then
- Put "~r[ " + spinText[spin..spin+1] + "~r"
- spin :+ 1
- If spin = spinText.Length Then
- spin = 0
- End If
-
- lastUpdate = now
- End If
- End If
- End Method
-
- Method FormatTime:String(time:Int)
- Local seconds:Int = time Mod 60
- Local minutes:Int = (time / 60) Mod 60
- Local hours:Int = time / 3600
-
- formatter.Clear()
-
- If hours > 99 Then
- formatter.IntArg(99).IntArg(59).IntArg(59)
- Else
- formatter.IntArg(hours).IntArg(minutes).IntArg(seconds)
- End If
-
- Return formatter.format()
- End Method
-
- End Type
- Function Put( str$="" )
- StandardIOStream.WriteString str
- StandardIOStream.Flush
- End Function
- Function Clear()
- Put "~r " + TProgress.full + "~r"
- End Function
- Extern
- Function _isatty:Int(fd:Int)
- End Extern
|