diff --git a/README.md b/README.md index b8a393ad..fb2be802 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ TweetDuck uses **Inno Setup** for installers and updates. First, download and in Next, add the Inno Setup installation folder (usually `C:\Program Files (x86)\Inno Setup 5`) into your **PATH** environment variable. You may need to restart File Explorer for the change to take place. -Now you can generate installers after a build by running `bld/RUN BUILD.bat`. Note that this will only package the files, you still need to run the [release build](#release) in Visual Studio! +Now you can generate installers after a build by running `bld/GEN EVERYTHING.bat`. Note that this will only package the files, you still need to run the [release build](#release) in Visual Studio! After the window closes, three installers will be generated inside the `bld/Output` folder: * **TweetDuck.exe** @@ -71,6 +71,6 @@ After the window closes, three installers will be generated inside the `bld/Outp > When opening **Batch Build**, you will also see `x64` and `AnyCPU` configurations. These are visible due to what I consider a Visual Studio bug, and will not work without significant changes to the project. Manually running the `Resources/PostCefUpdate.ps1` PowerShell script modifies the downloaded CefSharp packages, and removes the invalid configurations. -> There is a small chance running `RUN BUILD.bat` immediately shows a resource error. If that happens, close the console window (which terminates all Inno Setup processes and leaves corrupted installers in the output folder), and run it again. +> There is a small chance running `GEN EVERYTHING.bat` immediately shows a resource error. If that happens, close the console window (which terminates all Inno Setup processes and leaves corrupted installers in the output folder), and run it again. -> Running `RUN BUILD.bat` uses about 400 MB of RAM due to high compression. You can lower this to about 140 MB by opening `gen_full.iss` and `gen_port.iss`, and changing `LZMADictionarySize=15360` to `LZMADictionarySize=4096`. +> Running `GEN EVERYTHING.bat` uses about 400 MB of RAM due to high compression. You can lower this to about 140 MB by opening `gen_full.iss` and `gen_port.iss`, and changing `LZMADictionarySize=15360` to `LZMADictionarySize=4096`. diff --git a/Resources/PostBuild.fsx b/Resources/PostBuild.fsx new file mode 100644 index 00000000..f1d80987 --- /dev/null +++ b/Resources/PostBuild.fsx @@ -0,0 +1,249 @@ +open System +open System.Collections.Generic +open System.Diagnostics +open System.IO +open System.Text.RegularExpressions +open System.Threading.Tasks + +// "$(DevEnvDir)CommonExtensions\Microsoft\FSharp\fsi.exe" "$(ProjectDir)Resources\PostBuild.fsx" --exec --nologo -- "$(TargetDir)\" "$(ProjectDir)\" "$(ConfigurationName)" +// "$(ProjectDir)bld\post_build.exe" "$(TargetDir)\" "$(ProjectDir)\" "$(ConfigurationName)" + +exception ArgumentUsage of string + +#if !INTERACTIVE +[<EntryPoint>] +#endif +let main (argv: string[]) = + try + if argv.Length < 2 then + #if INTERACTIVE + raise (ArgumentUsage "fsi.exe PostBuild.fsx --exec --nologo -- <TargetDir> <ProjectDir> [ConfigurationName] [VersionTag]") + #else + raise (ArgumentUsage "PostBuild.exe <TargetDir> <ProjectDir> [ConfigurationName] [VersionTag]") + #endif + + let _time name func = + let sw = Stopwatch.StartNew() + func() + sw.Stop() + printfn "[%s took %i ms]" name (int (Math.Round(sw.Elapsed.TotalMilliseconds))) + + let (+/) path1 path2 = + Path.Combine(path1, path2) + + let sw = Stopwatch.StartNew() + + // Setup + + let targetDir = argv.[0] + let projectDir = argv.[1] + + let configuration = if argv.Length >= 3 then argv.[2] + else "Release" + + let version = if argv.Length >= 4 then argv.[3] + else ((targetDir +/ "TweetDuck.exe") |> FileVersionInfo.GetVersionInfo).FileVersion + + printfn "--------------------------" + printfn "TweetDuck version %s" version + printfn "--------------------------" + + let localesDir = targetDir +/ "locales" + let scriptsDir = targetDir +/ "scripts" + let pluginsDir = targetDir +/ "plugins" + let importsDir = scriptsDir +/ "imports" + + // Functions (Strings) + + let filterNotEmpty = + Seq.filter (not << String.IsNullOrEmpty) + + let replaceRegex (pattern: string) (replacement: string) input = + Regex.Replace(input, pattern, replacement) + + let collapseLines separator (sequence: string seq) = + String.Join(separator, sequence) + + let trimStart (line: string) = + line.TrimStart() + + // Functions (File Management) + let copyFile source target = + File.Copy(source, target, true) + + let createDirectory path = + Directory.CreateDirectory(path) |> ignore + + let rec copyDirectoryContentsFiltered source target (filter: string -> bool) = + if not (Directory.Exists(target)) then + Directory.CreateDirectory(target) |> ignore + + let src = DirectoryInfo(source) + + for file in src.EnumerateFiles() do + if filter file.Name then + file.CopyTo(target +/ file.Name) |> ignore + + for dir in src.EnumerateDirectories() do + if filter dir.Name then + copyDirectoryContentsFiltered dir.FullName (target +/ dir.Name) filter + + let copyDirectoryContents source target = + copyDirectoryContentsFiltered source target (fun _ -> true) + + // Functions (File Processing) + + let byPattern path pattern = + Directory.EnumerateFiles(path, pattern, SearchOption.AllDirectories) + + let exceptEndingWith name = + Seq.filter (fun (file: string) -> not (file.EndsWith(name))) + + let iterateFiles (files: string seq) (func: string -> unit) = + Parallel.ForEach(files, func) |> ignore + + let readFile file = seq { + use stream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 0x1000, FileOptions.SequentialScan) + use reader = new StreamReader(stream) + let mutable cont = true + + while cont do + let line = reader.ReadLine() + + if line = null then + cont <- false + else + yield line + } + + let writeFile (fullPath: string) (lines: string seq) = + let relativePath = fullPath.[(targetDir.Length)..] + let includeVersion = relativePath.StartsWith(@"scripts\") && not (relativePath.StartsWith(@"scripts\imports\")) + let finalLines = if includeVersion then seq { yield "#" + version; yield! lines } else lines + + File.WriteAllLines(fullPath, finalLines |> filterNotEmpty |> Seq.toArray) + printfn "Processed %s" relativePath + + let processFiles (files: string seq) (extProcessors: IDictionary<string, (string seq -> string seq)>) = + let rec processFileContents file = + readFile file + |> extProcessors.[Path.GetExtension(file)] + |> Seq.map (fun line -> + Regex.Replace(line, @"#import ""(.*?)""", (fun matchInfo -> + processFileContents(importsDir +/ matchInfo.Groups.[1].Value.Trim()) + |> collapseLines (Environment.NewLine) + |> (fun contents -> contents.TrimEnd()) + )) + ) + + iterateFiles files (fun file -> + processFileContents file + |> (writeFile file) + ) + + // Build + + copyFile (projectDir +/ "bld/Resources/LICENSES.txt") (targetDir +/ "LICENSES.txt") + + copyDirectoryContents (projectDir +/ "Resources/Scripts") scriptsDir + + createDirectory (pluginsDir +/ "official") + createDirectory (pluginsDir +/ "user") + + copyDirectoryContentsFiltered + (projectDir +/ "Resources/Plugins") + (pluginsDir +/ "official") + (fun name -> name <> ".debug" && name <> "emoji-instructions.txt") + + if configuration = "Debug" then + copyDirectoryContents + (projectDir +/ "Resources/Plugins/.debug") + (pluginsDir +/ "user/.debug") + + if Directory.Exists(localesDir) || configuration = "Release" then + Directory.EnumerateFiles(localesDir, "*.pak") + |> exceptEndingWith @"\en-US.pak" + |> Seq.iter (File.Delete) + + // Validation + + if File.ReadAllText(pluginsDir +/ "official/emoji-keyboard/emoji-ordering.txt").IndexOf('\r') <> -1 then + raise (FormatException("emoji-ordering.txt must not have any carriage return characters")) + else + printfn "Verified emoji-ordering.txt" + + // Processing + + let fileProcessors = + dict [ + ".js", (fun (lines: string seq) -> + lines + |> Seq.map (fun line -> + line + |> trimStart + |> replaceRegex @"^(.*?)((?<=^|[;{}()])\s?//(?:\s.*|$))?$" "$1" + |> replaceRegex @"(?<!\w)(return|throw)(\s.*?)? if (.*?);" "if ($3)$1$2;" + ) + ); + + ".css", (fun (lines: string seq) -> + lines + |> Seq.map (fun line -> + line + |> replaceRegex @"\s*/\*.*?\*/" "" + |> replaceRegex @"^(\S.*) {$" "$1{" + |> replaceRegex @"^\s+(.+?):\s*(.+?)(?:\s*(!important))?;$" "$1:$2$3;" + ) + |> filterNotEmpty + |> collapseLines " " + |> replaceRegex @"([{};])\s" "$1" + |> replaceRegex @";}" "}" + |> Seq.singleton + ); + + ".html", (fun (lines: string seq) -> + lines + |> Seq.map trimStart + ); + + ".meta", (fun (lines: string seq) -> + lines + |> Seq.map (fun line -> line.Replace("{version}", version)) + ); + ] + + processFiles ((byPattern targetDir "*.js") |> exceptEndingWith @"\configuration.default.js") fileProcessors + processFiles (byPattern targetDir "*.css") fileProcessors + processFiles (byPattern targetDir "*.html") fileProcessors + processFiles (byPattern pluginsDir "*.meta") fileProcessors + + // Cleanup + + Directory.Delete(importsDir, true) + + // Finished + + sw.Stop() + printfn "------------------" + printfn "Finished in %i ms" (int (Math.Round(sw.Elapsed.TotalMilliseconds))) + printfn "------------------" + 0 + + with + | ArgumentUsage message -> + printfn "" + printfn "Build script usage:" + printfn "%s" message + printfn "" + 1 + | ex -> + printfn "" + printfn "Encountered an error while running PostBuild:" + printfn "%A" ex + printfn "" + 1 + +#if INTERACTIVE +printfn "Running PostBuild in interpreter..." +main (fsi.CommandLineArgs |> Array.skip (1 + (fsi.CommandLineArgs |> Array.findIndex (fun arg -> arg = "--")))) +#endif diff --git a/Resources/PostBuild.ps1 b/Resources/PostBuild.ps1 deleted file mode 100644 index ca109295..00000000 --- a/Resources/PostBuild.ps1 +++ /dev/null @@ -1,168 +0,0 @@ -Param( - [Parameter(Mandatory = $True, Position = 1)][string] $targetDir, - [Parameter(Mandatory = $True, Position = 2)][string] $projectDir, - [Parameter(Position = 3)][string] $configuration = "Release", - [Parameter(Position = 4)][string] $version = "" -) - -$ErrorActionPreference = "Stop" - -try{ - $sw = [Diagnostics.Stopwatch]::StartNew() - - if ($version.Equals("")){ - $version = (Get-Item (Join-Path $targetDir "TweetDuck.exe")).VersionInfo.FileVersion - } - - Write-Host "--------------------------" - Write-Host "TweetDuck version" $version - Write-Host "--------------------------" - - # Cleanup - - if (Test-Path (Join-Path $targetDir "locales")){ - Remove-Item -Path (Join-Path $targetDir "locales\*.pak") -Exclude "en-US.pak" - } - - # Copy resources - - Copy-Item (Join-Path $projectDir "bld\Resources\LICENSES.txt") -Destination $targetDir -Force - - New-Item -ItemType directory -Path $targetDir -Name "scripts" | Out-Null - New-Item -ItemType directory -Path $targetDir -Name "plugins" | Out-Null - New-Item -ItemType directory -Path $targetDir -Name "plugins\official" | Out-Null - New-Item -ItemType directory -Path $targetDir -Name "plugins\user" | Out-Null - - Copy-Item (Join-Path $projectDir "Resources\Scripts\*") -Recurse -Destination (Join-Path $targetDir "scripts") - Copy-Item (Join-Path $projectDir "Resources\Plugins\*") -Recurse -Destination (Join-Path $targetDir "plugins\official") -Exclude ".debug" - - Remove-Item (Join-Path $targetDir "plugins\official\emoji-keyboard\emoji-instructions.txt") - - if ($configuration -eq "Debug"){ - New-Item -ItemType directory -Path $targetDir -Name "plugins\user\.debug" | Out-Null - Copy-Item (Join-Path $projectDir "Resources\Plugins\.debug\*") -Recurse -Destination (Join-Path $targetDir "plugins\user\.debug") - } - - # Helper functions - - function Remove-Empty-Lines{ - Param([Parameter(Mandatory = $True, Position = 1)] $lines) - - foreach($line in $lines){ - if ($line -ne ''){ - $line - } - } - } - - function Check-Carriage-Return{ - Param([Parameter(Mandatory = $True, Position = 1)] $file) - - if (!(Test-Path $file)){ - Throw "$file does not exist" - } - - if ((Get-Content -Path $file -Raw).Contains("`r")){ - Throw "$file must not have any carriage return characters" - } - - Write-Host "Verified" $file.Substring($targetDir.Length) - } - - function Rewrite-File{ - Param([Parameter(Mandatory = $True, Position = 1)] $file, - [Parameter(Mandatory = $True, Position = 2)] $lines, - [Parameter(Mandatory = $True, Position = 3)] $imports) - - $lines = Remove-Empty-Lines($lines) - $relativePath = $file.FullName.Substring($targetDir.Length) - - foreach($line in $lines){ - if ($line.Contains('#import ')){ - $imports.Add($file.FullName) - break - } - } - - if ($relativePath.StartsWith("scripts\")){ - $lines = (,("#" + $version) + $lines) - } - - [IO.File]::WriteAllLines($file.FullName, $lines) - Write-Host "Processed" $relativePath - } - - # Validation - - Check-Carriage-Return(Join-Path $targetDir "plugins\official\emoji-keyboard\emoji-ordering.txt") - - # Processing - - $imports = New-Object "System.Collections.Generic.List[string]" - - foreach($file in Get-ChildItem -Path $targetDir -Filter "*.js" -Exclude "configuration.default.js" -Recurse){ - $lines = [IO.File]::ReadLines($file.FullName) - $lines = $lines | ForEach-Object { $_.TrimStart() } - $lines = $lines -Replace '^(.*?)((?<=^|[;{}()])\s?//(?:\s.*|$))?$', '$1' - $lines = $lines -Replace '(?<!\w)(return|throw)(\s.*?)? if (.*?);', 'if ($3)$1$2;' - Rewrite-File $file $lines $imports - } - - foreach($file in Get-ChildItem -Path $targetDir -Filter "*.css" -Recurse){ - $lines = [IO.File]::ReadLines($file.FullName) - $lines = $lines -Replace '\s*/\*.*?\*/', '' - $lines = $lines -Replace '^(\S.*) {$', '$1{' - $lines = $lines -Replace '^\s+(.+?):\s*(.+?)(?:\s*(!important))?;$', '$1:$2$3;' - $lines = @((Remove-Empty-Lines($lines)) -Join ' ') - $lines = $lines -Replace '([{};])\s', '$1' - $lines = $lines -Replace ';}', '}' - Rewrite-File $file $lines $imports - } - - foreach($file in Get-ChildItem -Path $targetDir -Filter "*.html" -Recurse){ - $lines = [IO.File]::ReadLines($file.FullName) - $lines = $lines | ForEach-Object { $_.TrimStart() } - Rewrite-File $file $lines $imports - } - - foreach($file in Get-ChildItem -Path (Join-Path $targetDir "plugins") -Filter "*.meta" -Recurse){ - $lines = [IO.File]::ReadLines($file.FullName) - $lines = $lines -Replace '\{version\}', $version - Rewrite-File $file $lines $imports - } - - # Imports - - $importFolder = Join-Path $targetDir "scripts\imports" - - foreach($path in $imports){ - $text = [IO.File]::ReadAllText($path) - $text = [Regex]::Replace($text, '#import "(.*?)"', { - $importPath = Join-Path $importFolder ($args[0].Groups[1].Value.Trim()) - $importStr = [IO.File]::ReadAllText($importPath).TrimEnd() - - if ($importStr[0] -eq '#'){ - $importStr = $importStr.Substring($importStr.IndexOf("`n") + 1) - } - - return $importStr - }, [System.Text.RegularExpressions.RegexOptions]::MultiLine) - - [IO.File]::WriteAllText($path, $text) - Write-Host "Resolved" $path.Substring($targetDir.Length) - } - - [IO.Directory]::Delete($importFolder, $True) - - # Finished - - $sw.Stop() - Write-Host "------------------" - Write-Host "Finished in" $([math]::Round($sw.Elapsed.TotalMilliseconds)) "ms" - Write-Host "------------------" - -}catch{ - Write-Host "Encountered an error while running PostBuild.ps1 on line" $_.InvocationInfo.ScriptLineNumber - Write-Host $_ - Exit 1 -} diff --git a/Resources/ScriptLoader.cs b/Resources/ScriptLoader.cs index f370c5ad..42a41959 100644 --- a/Resources/ScriptLoader.cs +++ b/Resources/ScriptLoader.cs @@ -87,7 +87,7 @@ private static void ShowLoadError(Control sync, string message){ #if DEBUG private static readonly string HotSwapProjectRoot = FixPathSlash(Path.GetFullPath(Path.Combine(Program.ProgramPath, "../../../"))); private static readonly string HotSwapTargetDir = FixPathSlash(Path.Combine(HotSwapProjectRoot, "bin", "tmp")); - private static readonly string HotSwapRebuildScript = Path.Combine(HotSwapProjectRoot, "Resources", "PostBuild.ps1"); + private static readonly string HotSwapRebuildScript = Path.Combine(HotSwapProjectRoot, "bld", "post_build.exe"); static ScriptLoader(){ if (File.Exists(HotSwapRebuildScript)){ @@ -110,8 +110,8 @@ public static void HotSwap(){ Stopwatch sw = Stopwatch.StartNew(); using(Process process = Process.Start(new ProcessStartInfo{ - FileName = "powershell", - Arguments = $"-ExecutionPolicy Unrestricted -File \"{HotSwapRebuildScript}\" \"{HotSwapTargetDir}\\\" \"{HotSwapProjectRoot}\\\" \"Debug\" \"{Program.VersionTag}\"", + FileName = HotSwapRebuildScript, + Arguments = $"\"{HotSwapTargetDir}\\\" \"{HotSwapProjectRoot}\\\" \"Debug\" \"{Program.VersionTag}\"", WindowStyle = ProcessWindowStyle.Hidden })){ // ReSharper disable once PossibleNullReferenceException diff --git a/TweetDuck.csproj b/TweetDuck.csproj index dc7699dd..6fcce94f 100644 --- a/TweetDuck.csproj +++ b/TweetDuck.csproj @@ -394,15 +394,22 @@ rmdir "$(ProjectDir)bin\Release" rmdir "$(TargetDir)scripts" /S /Q rmdir "$(TargetDir)plugins" /S /Q -powershell -ExecutionPolicy Unrestricted -File "$(ProjectDir)Resources\PostBuild.ps1" "$(TargetDir)\" "$(ProjectDir)\" "$(ConfigurationName)" +IF EXIST "$(ProjectDir)bld\post_build.exe" ( + "$(ProjectDir)bld\post_build.exe" "$(TargetDir)\" "$(ProjectDir)\" "$(ConfigurationName)" +) ELSE ( + "$(DevEnvDir)CommonExtensions\Microsoft\FSharp\fsi.exe" "$(ProjectDir)Resources\PostBuild.fsx" --exec --nologo -- "$(TargetDir)\" "$(ProjectDir)\" "$(ConfigurationName)" +) </PostBuildEvent> </PropertyGroup> + <Target Name="BeforeBuild" Condition="(!$([System.IO.File]::Exists("$(ProjectDir)\bld\post_build.exe")) OR ($([System.IO.File]::GetLastWriteTime("$(ProjectDir)\Resources\PostBuild.fsx").Ticks) > $([System.IO.File]::GetLastWriteTime("$(ProjectDir)\bld\post_build.exe").Ticks)))"> + <Exec Command=""$(ProjectDir)bld\POST BUILD.bat"" WorkingDirectory="$(ProjectDir)bld\" IgnoreExitCode="true" /> + </Target> <Target Name="AfterBuild" Condition="$(ConfigurationName) == Release"> <Exec Command="del "$(TargetDir)*.pdb"" /> <Exec Command="del "$(TargetDir)*.xml"" /> <Delete Files="$(TargetDir)CefSharp.BrowserSubprocess.exe" /> <Delete Files="$(TargetDir)widevinecdmadapter.dll" /> - <Exec Command=""$(ProjectDir)bld\UPDATE ONLY.bat"" WorkingDirectory="$(ProjectDir)bld\" IgnoreExitCode="true" /> + <Exec Command=""$(ProjectDir)bld\GEN UPDATE ONLY.bat"" WorkingDirectory="$(ProjectDir)bld\" IgnoreExitCode="true" /> </Target> <PropertyGroup> <PreBuildEvent>powershell Get-Process TweetDuck.Browser -ErrorAction SilentlyContinue ^| Where-Object {$_.Path -eq '$(TargetDir)TweetDuck.Browser.exe'} ^| Stop-Process; Exit 0</PreBuildEvent> diff --git a/bld/RUN BUILD.bat b/bld/GEN EVERYTHING.bat similarity index 100% rename from bld/RUN BUILD.bat rename to bld/GEN EVERYTHING.bat diff --git a/bld/UPDATE ONLY.bat b/bld/GEN UPDATE ONLY.bat similarity index 100% rename from bld/UPDATE ONLY.bat rename to bld/GEN UPDATE ONLY.bat diff --git a/bld/POST BUILD.bat b/bld/POST BUILD.bat new file mode 100644 index 00000000..ef6c7c4b --- /dev/null +++ b/bld/POST BUILD.bat @@ -0,0 +1,16 @@ +@ECHO OFF + +DEL "post_build.exe" + +SET fsc="%PROGRAMFILES(x86)%\Microsoft SDKs\F#\10.1\Framework\v4.0\fsc.exe" + +IF NOT EXIST %fsc% ( + SET fsc="%PROGRAMFILES%\Microsoft SDKs\F#\10.1\Framework\v4.0\fsc.exe" +) + +IF NOT EXIST %fsc% ( + ECHO fsc.exe not found + EXIT 1 +) + +%fsc% --standalone --deterministic --preferreduilang:en-US --platform:x86 --target:exe --out:post_build.exe "%~dp0..\Resources\PostBuild.fsx"