|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "flag" |
| 6 | + "fmt" |
| 7 | + "io/fs" |
| 8 | + "os" |
| 9 | + "os/exec" |
| 10 | + "path/filepath" |
| 11 | + "runtime" |
| 12 | + "strings" |
| 13 | + |
| 14 | + "github.com/mholt/archives" |
| 15 | + "github.com/otiai10/copy" |
| 16 | +) |
| 17 | + |
| 18 | +func build(baseDir string, releaseMode string, targetOS, targetArch string) { |
| 19 | + |
| 20 | + fmt.Println(`< Beginning build to "` + baseDir + `" for ` + targetOS + `. >`) |
| 21 | + |
| 22 | + forWin := strings.Contains(targetOS, "windows") |
| 23 | + forMac := strings.Contains(targetOS, "darwin") |
| 24 | + |
| 25 | + copyTo := func(src, dest string) { |
| 26 | + if err := copy.Copy(src, dest); err != nil { |
| 27 | + panic(err) |
| 28 | + } |
| 29 | + } |
| 30 | + |
| 31 | + // Note that this script is meant to be run from a terminal at the project root. |
| 32 | + // It is specifically NOT meant to be built into an executable and run by double-clicking in |
| 33 | + // Finder, on Mac OS. |
| 34 | + |
| 35 | + // We always remove any pre-existing platform directory before building to ensure it's fresh. |
| 36 | + if err := os.RemoveAll(baseDir); err != nil { |
| 37 | + panic(err) |
| 38 | + } |
| 39 | + |
| 40 | + copyTo(filepath.Join("..", "changelog.txt"), filepath.Join(baseDir, "changelog.txt")) |
| 41 | + |
| 42 | + if forMac { |
| 43 | + baseDir = filepath.Join(baseDir, "MasterPlan.app", "Contents", "MacOS") |
| 44 | + } |
| 45 | + |
| 46 | + // Copy the assets folder to the bin directory |
| 47 | + |
| 48 | + copyTo(filepath.Join("..", "assets"), filepath.Join(baseDir, "assets")) |
| 49 | + |
| 50 | + fmt.Println("<Assets copied.>") |
| 51 | + |
| 52 | + outputFilepath := filepath.Join(baseDir, "MasterPlan") |
| 53 | + |
| 54 | + if forWin { |
| 55 | + |
| 56 | + outputFilepath += ".exe" |
| 57 | + |
| 58 | + // Copy the resources.syso so the executable has the generated icon and executable properties compiled in. |
| 59 | + // This is done using go generate with goversioninfo downloaded and "// go:generate goversioninfo -64=true" in main.go. |
| 60 | + copyTo(filepath.Join("..", "other_sources", "resource.syso"), filepath.Join("..", "resource.syso")) |
| 61 | + |
| 62 | + // Copy in the SDL requirements (.dll files) |
| 63 | + filepath.Walk(filepath.Join("other_sources"), func(path string, info fs.FileInfo, err error) error { |
| 64 | + _, filename := filepath.Split(path) |
| 65 | + if filepath.Ext(path) == ".dll" { |
| 66 | + copyTo(path, filepath.Join(baseDir, filename)) |
| 67 | + } |
| 68 | + return nil |
| 69 | + }) |
| 70 | + |
| 71 | + } |
| 72 | + |
| 73 | + // We should compile statically at some point, but it's broken currently, it seems? See: https://github.com/veandco/go-sdl2/issues/507 |
| 74 | + // So for the meantime, we'll just build dynamically and bundle the dependencies on Windows and Mac. On Linux, usually users have the dependencies (SDL2, basically) installed already. |
| 75 | + |
| 76 | + // The below string cross-compiles by setting CC to an 64-bit Windows version of MinGW |
| 77 | + // CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=` + targetOS + ` GOARCH=amd64 CGO_LDFLAGS="-lSDL2 -lSDL2_gfx" go build -tags ` + releaseMode + ` -ldflags "-s -w -H=windowsgui" -o ` + filename + ` ./` |
| 78 | + |
| 79 | + var c *exec.Cmd |
| 80 | + var err error |
| 81 | + |
| 82 | + os.Setenv(`GOOS`, targetOS) |
| 83 | + os.Setenv(`GOARCH`, targetArch) |
| 84 | + |
| 85 | + // cross-compile: |
| 86 | + if forWin && runtime.GOOS == "linux" { |
| 87 | + os.Setenv("CC", "x86_64-w64-mingw32-gcc") |
| 88 | + } else { |
| 89 | + os.Setenv("CC", "gcc") |
| 90 | + } |
| 91 | + |
| 92 | + // Default building for the current OS |
| 93 | + if forWin { |
| 94 | + // No static flag for Windows because we're redistributing the DLLs for simplicity, and I have yet to figure out where to put the static libraries on Linux to cross-compile successfully. |
| 95 | + // When a command is more than a single word and is separated by spaces, it has to be in a single "space" (i.e. "static " + releaseMode) |
| 96 | + |
| 97 | + // Also note that I know it's weird that I'm joining the build script directory here because baseDir is already a folder up; this makes it so |
| 98 | + // bin is the running directory |
| 99 | + c = exec.Command(`go`, `build`, `-C`, `..`, `-ldflags`, `-s -w -H windowsgui`, `-tags`, releaseMode, `-o`, filepath.Join("build_script", outputFilepath), `.`) |
| 100 | + } else { |
| 101 | + c = exec.Command(`go`, `build`, `-C`, `..`, `-ldflags`, `-s -w`, `-tags`, releaseMode, `-o`, filepath.Join("build_script", outputFilepath), `.`) |
| 102 | + } |
| 103 | + |
| 104 | + fmt.Println("<Building binary with args: ", c.Args, ".>") |
| 105 | + |
| 106 | + text, err := c.CombinedOutput() |
| 107 | + |
| 108 | + if err != nil { |
| 109 | + fmt.Println("<ERROR: ", string(err.Error())+">") |
| 110 | + fmt.Println(string(text)) |
| 111 | + } |
| 112 | + |
| 113 | + // Add the stuff for Mac |
| 114 | + if forMac { |
| 115 | + baseDir = filepath.Clean(filepath.Join(baseDir, "..")) |
| 116 | + copyTo(filepath.Join("..", "other_sources", "Info.plist"), filepath.Join(baseDir, "Info.plist")) |
| 117 | + copyTo(filepath.Join("..", "other_sources", "macicons.icns"), filepath.Join(baseDir, "Resources", "macicons.icns")) |
| 118 | + } |
| 119 | + |
| 120 | + // The final executable should be, well, executable for everybody. 0777 should do it for Mac and Linux. |
| 121 | + os.Chmod(outputFilepath, 0777) |
| 122 | + |
| 123 | + if err == nil { |
| 124 | + fmt.Println("<Build complete!>") |
| 125 | + fmt.Println("") |
| 126 | + } |
| 127 | + |
| 128 | + if forWin { |
| 129 | + // Remove Resources; we don't need it in the root directory anymore after building. |
| 130 | + os.Remove(filepath.Join("..", "resource.syso")) |
| 131 | + } |
| 132 | + |
| 133 | +} |
| 134 | + |
| 135 | +// Compress the build output in bin. This is a separate step to ensure that any dependencies that need to be copied in from build |
| 136 | +// services (like Appveyor) can be done after building in the build service's configuration. |
| 137 | +func compress() { |
| 138 | + |
| 139 | + fmt.Println("<Compressing build...>") |
| 140 | + |
| 141 | + // Archive in .tar.gz because AppVeyor doesn't handle execution bits properly and I don't want to add a ton to the source code |
| 142 | + // just to box the output up into a .tar.gz. |
| 143 | + |
| 144 | + os.Chdir("./bin") // Switch to the bin folder and then archive the contents |
| 145 | + |
| 146 | + versions := []string{} |
| 147 | + |
| 148 | + filepath.Walk(filepath.Clean("."), func(path string, info os.FileInfo, err error) error { |
| 149 | + |
| 150 | + dirCount := len(strings.Split(path, string(os.PathSeparator))) |
| 151 | + |
| 152 | + if info.IsDir() && dirCount == 1 && path != "." { |
| 153 | + |
| 154 | + ending := ".tar.gz" |
| 155 | + if strings.Contains(path, "windows") { |
| 156 | + ending = ".zip" |
| 157 | + } |
| 158 | + |
| 159 | + versions = append(versions, path, ending) |
| 160 | + } |
| 161 | + |
| 162 | + return nil |
| 163 | + |
| 164 | + }) |
| 165 | + |
| 166 | + ctx := context.Background() |
| 167 | + for i := 0; i < len(versions); i += 2 { |
| 168 | + |
| 169 | + version := versions[i] |
| 170 | + ending := versions[i+1] |
| 171 | + |
| 172 | + // We want to create separate archives for each version (e.g. release and demo) |
| 173 | + files, err := archives.FilesFromDisk(ctx, nil, map[string]string{ |
| 174 | + version: version + ending, |
| 175 | + }) |
| 176 | + |
| 177 | + if err != nil { |
| 178 | + panic(err) |
| 179 | + } |
| 180 | + |
| 181 | + format := archives.CompressedArchive{ |
| 182 | + Compression: archives.Gz{}, |
| 183 | + Archival: archives.Tar{}, |
| 184 | + } |
| 185 | + |
| 186 | + out, err := os.Create(version + ending) |
| 187 | + if err != nil { |
| 188 | + panic(err) |
| 189 | + } |
| 190 | + |
| 191 | + defer out.Close() |
| 192 | + |
| 193 | + err = format.Archive(ctx, out, files) |
| 194 | + if err != nil { |
| 195 | + panic(err) |
| 196 | + } |
| 197 | + |
| 198 | + } |
| 199 | + |
| 200 | + fmt.Println("<Build successfully compressed!>") |
| 201 | + |
| 202 | +} |
| 203 | + |
| 204 | +func publishToItch() { |
| 205 | + |
| 206 | + buildNames := []string{} |
| 207 | + |
| 208 | + filepath.Walk(filepath.Join("./", "bin"), func(path string, info os.FileInfo, err error) error { |
| 209 | + |
| 210 | + dirCount := len(strings.Split(path, string(os.PathSeparator))) |
| 211 | + |
| 212 | + if info.IsDir() && dirCount == 2 { |
| 213 | + buildNames = append(buildNames, path) // We want to upload the build directories |
| 214 | + } |
| 215 | + |
| 216 | + return nil |
| 217 | + |
| 218 | + }) |
| 219 | + |
| 220 | + fmt.Println("Builds found:", buildNames) |
| 221 | + |
| 222 | + for _, build := range buildNames { |
| 223 | + |
| 224 | + buildName := strings.Split(build, string(os.PathSeparator))[1] |
| 225 | + |
| 226 | + result, err := exec.Command("butler", "push", build, "solarlune/masterplan:"+buildName).CombinedOutput() |
| 227 | + |
| 228 | + if err == nil { |
| 229 | + fmt.Println("Published", build, "to itch!") |
| 230 | + } else { |
| 231 | + fmt.Println(string(result), string(err.Error())) |
| 232 | + } |
| 233 | + |
| 234 | + } |
| 235 | + |
| 236 | +} |
| 237 | + |
| 238 | +func main() { |
| 239 | + |
| 240 | + buildMP := flag.Bool("b", false, "Build MasterPlan into the bin directory.") |
| 241 | + osFlag := flag.String("os", "", "What target OS to build MasterPlan for. Omitting this flag will build MasterPlan for the current operating system.") |
| 242 | + archFlag := flag.String("arch", "", "What target arch to build MasterPlan for (either 'amd64' or 'arm64'). Omitting this flag will build MasterPlan for the current architecture.") |
| 243 | + compressMP := flag.Bool("c", false, "Compress build output.") |
| 244 | + itch := flag.Bool("i", false, "Upload build to itch.io.") |
| 245 | + |
| 246 | + flag.Parse() |
| 247 | + |
| 248 | + if *buildMP { |
| 249 | + targetName := runtime.GOOS |
| 250 | + targetArch := runtime.GOARCH |
| 251 | + if *osFlag != "" { |
| 252 | + targetName = *osFlag |
| 253 | + } |
| 254 | + if *archFlag != "" { |
| 255 | + targetArch = *archFlag |
| 256 | + } |
| 257 | + |
| 258 | + build(filepath.Join("..", "bin", targetName+"-0.9-Release-"+targetArch), "release", targetName, targetArch) |
| 259 | + build(filepath.Join("..", "bin", targetName+"-0.9-Demo-"+targetArch), "demo", targetName, targetArch) |
| 260 | + } |
| 261 | + if *compressMP { |
| 262 | + compress() // Compresses all built binary folders in the ./bin folder |
| 263 | + } |
| 264 | + if *itch { |
| 265 | + publishToItch() |
| 266 | + } |
| 267 | + |
| 268 | + if !*buildMP && !*compressMP && !*itch { |
| 269 | + |
| 270 | + fmt.Println( |
| 271 | + "MASTERPLAN BUILD SCRIPT:\n", |
| 272 | + "To use this script, you can use the following arguments:\n", |
| 273 | + "\n", |
| 274 | + "-b to build MasterPlan for the current OS; cross-platform builds aren't fully supported yet. ", |
| 275 | + "\n", |
| 276 | + "Example to build for current OS, current architecture: go run . -b\n", |
| 277 | + "Example to build for AMD64 Windows: go run . -b -os windows -arch amd64 \n", |
| 278 | + "Example to build for ARM64 Mac: go run . -b -os darwin -arch arm64 \n", |
| 279 | + "Example to build for Mac, current arch: go run . -b -os darwin\n", |
| 280 | + "Example to build for Linux: go run . -b -os linux \n", |
| 281 | + "\n", |
| 282 | + "-c to compress the build output, as a .tar.gz file for Linux or Mac, or a .zip file for Windows.\n", |
| 283 | + "\n", |
| 284 | + "-i to publish the bin contents to itch.", |
| 285 | + ) |
| 286 | + |
| 287 | + } |
| 288 | + |
| 289 | +} |
0 commit comments