diff --git a/.gitignore b/.gitignore index 5b90e79..26e4cce 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ go.work.sum # env file .env +.build/ diff --git a/cmd/build/main.go b/cmd/build/main.go new file mode 100644 index 0000000..4f7f034 --- /dev/null +++ b/cmd/build/main.go @@ -0,0 +1,476 @@ +package main + +import ( + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" +) + +var ( + projectDir = func() string { + // go run cmd/build/main.go — __file__ is in cmd/build/, project root is ../../ + exe, _ := os.Getwd() + return exe + }() + buildDir = filepath.Join(projectDir, ".build") + vendorDir = filepath.Join(projectDir, "vendor") + gav1dDir = filepath.Join(buildDir, "dav1d") + ffmpegDir = filepath.Join(buildDir, "ffmpeg") +) + +func init() { + github := os.Getenv("GITHUB_WORKSPACE") + if github != "" { + projectDir = github + buildDir = filepath.Join(projectDir, ".build") + vendorDir = filepath.Join(projectDir, "vendor") + gav1dDir = filepath.Join(buildDir, "dav1d") + ffmpegDir = filepath.Join(buildDir, "ffmpeg") + } +} + +type buildTarget struct { + name string + goos string + goarch string + dav1dCrossFile string + ffmpegExtraArgs []string +} + +func availableTargets() []buildTarget { + switch runtime.GOOS { + case "linux": + return []buildTarget{ + {name: "linux_amd64", goos: "linux", goarch: "amd64"}, + { + name: "linux_arm64", + goos: "linux", + goarch: "arm64", + dav1dCrossFile: "cross_linux_arm64.ini", + ffmpegExtraArgs: []string{ + "--enable-cross-compile", + "--arch=aarch64", + "--target-os=linux", + "--cross-prefix=aarch64-linux-gnu-", + }, + }, + { + name: "windows_amd64", + goos: "windows", + goarch: "amd64", + dav1dCrossFile: "cross_windows_amd64.ini", + ffmpegExtraArgs: []string{ + "--enable-cross-compile", + "--arch=x86_64", + "--target-os=mingw32", + "--cross-prefix=x86_64-w64-mingw32-", + }, + }, + } + case "darwin": + return []buildTarget{ + {name: "darwin_arm64", goos: "darwin", goarch: "arm64"}, + { + name: "darwin_amd64", + goos: "darwin", + goarch: "amd64", + dav1dCrossFile: "cross_darwin_amd64.ini", + ffmpegExtraArgs: []string{ + "--enable-cross-compile", + "--arch=x86_64", + "--target-os=darwin", + }, + }, + } + case "windows": + return []buildTarget{ + {name: "windows_amd64", goos: "windows", goarch: "amd64"}, + } + } + return nil +} + +func isNative(t buildTarget) bool { + return t.goos == runtime.GOOS && t.goarch == runtime.GOARCH +} + +func checkTool(tool string) bool { + _, err := exec.LookPath(tool) + return err == nil +} + +func checkBuildDeps(target buildTarget) []string { + var missing []string + cc := "gcc" + if runtime.GOOS == "darwin" { + cc = "clang" + } + tools := []string{cc, "meson", "ninja", "pkg-config"} + if isNative(target) { + tools = append(tools, "nasm") + } + for _, t := range tools { + if !checkTool(t) { + missing = append(missing, t) + } + } + if !isNative(target) { + switch target.name { + case "linux_arm64": + if !checkTool("aarch64-linux-gnu-gcc") { + missing = append(missing, "aarch64-linux-gnu-gcc") + } + case "windows_amd64": + if !checkTool("x86_64-w64-mingw32-gcc") { + missing = append(missing, "x86_64-w64-mingw32-gcc") + } + } + } + return missing +} + +func run(dir, name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = os.Environ() + fmt.Printf("\n>>> %s %v (in %s)\n", name, args, dir) + return cmd.Run() +} + +func writeFile(path, content string) error { + if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + return os.WriteFile(path, []byte(content), 0644) +} + +func copyFile(src, dst string) error { + s, err := os.Open(src) + if err != nil { + return fmt.Errorf("open %s: %w", src, err) + } + defer s.Close() + if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil { + return err + } + d, err := os.Create(dst) + if err != nil { + return fmt.Errorf("create %s: %w", dst, err) + } + defer d.Close() + if _, err := io.Copy(d, s); err != nil { + return fmt.Errorf("copy %s -> %s: %w", src, dst, err) + } + fmt.Printf(" %s -> %s\n", filepath.Base(src), dst) + return nil +} + +func artifactExists(target buildTarget) bool { + dest := filepath.Join(vendorDir, target.name) + for _, lib := range []string{"libavcodec.a", "libavformat.a", "libavutil.a", "libswresample.a", "libdav1d.a"} { + if _, err := os.Stat(filepath.Join(dest, lib)); err != nil { + return false + } + } + return true +} + +func cloneIfMissing(dir, url string) error { + if _, err := os.Stat(dir); os.IsNotExist(err) { + fmt.Printf("cloning %s into %s...\n", url, dir) + if err := os.MkdirAll(filepath.Dir(dir), 0755); err != nil { + return err + } + return run(filepath.Dir(dir), "git", "clone", "--depth", "1", url, dir) + } + fmt.Printf("already exists: %s\n", dir) + return nil +} + +func buildDav1d(target buildTarget) error { + name := target.name + fmt.Printf("\n=== dav1d %s ===\n", name) + + targetBuildDir := filepath.Join(gav1dDir, "build_"+name) + installDir := filepath.Join(buildDir, "dav1d_install_"+name) + _ = os.RemoveAll(targetBuildDir) + + args := []string{ + "setup", targetBuildDir, + "--default-library=static", + "--buildtype=release", + "-Denable_tools=false", + "-Denable_tests=false", + "--prefix=" + installDir, + } + if isNative(target) { + args = append(args, "-Denable_asm=true") + } else { + args = append(args, "-Denable_asm=false") + } + if target.dav1dCrossFile != "" { + args = append(args, "--cross-file="+filepath.Join(buildDir, target.dav1dCrossFile)) + } + + if err := run(gav1dDir, "meson", args...); err != nil { + return fmt.Errorf("meson setup dav1d %s: %w", name, err) + } + if err := run(gav1dDir, "ninja", "-C", targetBuildDir); err != nil { + return fmt.Errorf("ninja dav1d %s: %w", name, err) + } + if err := run(gav1dDir, "ninja", "-C", targetBuildDir, "install"); err != nil { + return fmt.Errorf("ninja install dav1d %s: %w", name, err) + } + return copyFile( + filepath.Join(installDir, "lib", "libdav1d.a"), + filepath.Join(vendorDir, name, "libdav1d.a"), + ) +} + +func buildFFmpeg(target buildTarget) error { + name := target.name + fmt.Printf("\n=== ffmpeg %s ===\n", name) + + installDir := filepath.Join(buildDir, "ffmpeg_install_"+name) + dav1dInstall := filepath.Join(buildDir, "dav1d_install_"+name) + if err := os.MkdirAll(installDir, 0755); err != nil { + return err + } + + run(ffmpegDir, "make", "distclean") + + extraCFlags := "-I" + filepath.Join(dav1dInstall, "include") + extraLDFlags := "-L" + filepath.Join(dav1dInstall, "lib") + if target.goos == "darwin" && target.goarch == "amd64" { + extraCFlags += " -arch x86_64" + extraLDFlags += " -arch x86_64" + } + + args := []string{ + "--prefix=" + installDir, + "--disable-everything", + "--disable-programs", + "--disable-doc", + "--disable-network", + "--disable-avdevice", + "--disable-avfilter", + "--disable-swscale", + "--disable-vaapi", + "--disable-vdpau", + "--disable-bzlib", + "--disable-xlib", + "--disable-libdrm", + "--disable-hwaccels", + "--enable-libdav1d", + "--enable-avcodec", + "--enable-avformat", + "--enable-avutil", + "--enable-swresample", + "--enable-static", + "--disable-shared", + "--enable-pic", + "--enable-decoder=h264,hevc,libdav1d,vp9,vp8,mpeg4,mp3,aac,opus,vorbis,flac,pcm_s16le,pcm_s16be,pcm_f32le", + "--enable-demuxer=mov,mp4,matroska,webm,avi,flv,ogg,mp3,aac,flac,ivf,h264,hevc,mpegts", + "--enable-parser=h264,hevc,av1,vp9,vp8,mpeg4video,mp3,aac,opus,vorbis,flac", + "--enable-protocol=file,pipe", + "--enable-bsf=h264_mp4toannexb,hevc_mp4toannexb,extract_extradata", + "--pkg-config=pkg-config", + "--pkg-config-flags=--static", + "--extra-cflags=" + extraCFlags, + "--extra-ldflags=" + extraLDFlags, + } + args = append(args, target.ffmpegExtraArgs...) + + pkgPath := filepath.Join(dav1dInstall, "lib", "pkgconfig") + filtered := []string{} + for _, e := range os.Environ() { + if !strings.HasPrefix(e, "PKG_CONFIG_PATH=") && + !strings.HasPrefix(e, "PKG_CONFIG_LIBDIR=") { + filtered = append(filtered, e) + } + } + + cmd := exec.Command(filepath.Join(ffmpegDir, "configure"), args...) + cmd.Dir = ffmpegDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = append(filtered, + "PKG_CONFIG_PATH="+pkgPath, + "PKG_CONFIG_LIBDIR="+pkgPath, + ) + fmt.Printf("\n>>> ./configure ... (in %s)\n", ffmpegDir) + if err := cmd.Run(); err != nil { + return fmt.Errorf("configure ffmpeg %s: %w", name, err) + } + + nproc := fmt.Sprintf("%d", runtime.NumCPU()) + if err := run(ffmpegDir, "make", "-j"+nproc); err != nil { + return fmt.Errorf("make ffmpeg %s: %w", name, err) + } + if err := run(ffmpegDir, "make", "install"); err != nil { + return fmt.Errorf("make install ffmpeg %s: %w", name, err) + } + + dest := filepath.Join(vendorDir, name) + for _, lib := range []string{"libavcodec.a", "libavformat.a", "libavutil.a", "libswresample.a"} { + if err := copyFile(filepath.Join(installDir, "lib", lib), filepath.Join(dest, lib)); err != nil { + return err + } + } + return nil +} + +func firstBuiltTarget() string { + for _, t := range availableTargets() { + if artifactExists(t) { + return t.name + } + } + return availableTargets()[0].name +} + +func copyHeaders() error { + fmt.Println("\n=== copying headers ===") + first := firstBuiltTarget() + + src := filepath.Join(buildDir, "ffmpeg_install_"+first, "include") + dst := filepath.Join(vendorDir, "include") + filepath.Walk(src, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + rel, _ := filepath.Rel(src, path) + return copyFile(path, filepath.Join(dst, rel)) + }) + + srcDav1d := filepath.Join(buildDir, "dav1d_install_"+first, "include", "dav1d") + dstDav1d := filepath.Join(vendorDir, "include", "dav1d") + filepath.Walk(srcDav1d, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + rel, _ := filepath.Rel(srcDav1d, path) + return copyFile(path, filepath.Join(dstDav1d, rel)) + }) + return nil +} + +func writeCrossFiles() error { + if runtime.GOOS == "darwin" { + pkgConfigPath, _ := exec.LookPath("pkg-config") + wrapperPath := filepath.Join(buildDir, "clang_x86_64.sh") + if err := writeFile(wrapperPath, "#!/bin/sh\nexec clang -arch x86_64 \"$@\"\n"); err != nil { + return err + } + if err := os.Chmod(wrapperPath, 0755); err != nil { + return err + } + if err := writeFile( + filepath.Join(buildDir, "cross_darwin_amd64.ini"), + fmt.Sprintf(`[binaries] +c = '%s' +ar = 'ar' +strip = 'strip' +pkg-config = '%s' + +[built-in options] +c_args = ['-arch', 'x86_64'] +c_link_args = ['-arch', 'x86_64'] + +[host_machine] +system = 'darwin' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' +`, wrapperPath, pkgConfigPath)); err != nil { + return err + } + } + + if err := writeFile(filepath.Join(buildDir, "cross_linux_arm64.ini"), `[binaries] +c = 'aarch64-linux-gnu-gcc' +ar = 'aarch64-linux-gnu-ar' +strip = 'aarch64-linux-gnu-strip' +pkgconfig = 'pkg-config' + +[host_machine] +system = 'linux' +cpu_family = 'aarch64' +cpu = 'aarch64' +endian = 'little' +`); err != nil { + return err + } + + return writeFile(filepath.Join(buildDir, "cross_windows_amd64.ini"), `[binaries] +c = 'x86_64-w64-mingw32-gcc' +ar = 'x86_64-w64-mingw32-ar' +strip = 'x86_64-w64-mingw32-strip' +pkgconfig = 'pkg-config' + +[host_machine] +system = 'windows' +cpu_family = 'x86_64' +cpu = 'x86_64' +endian = 'little' +`) +} + +func main() { + if err := os.MkdirAll(buildDir, 0755); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + if err := cloneIfMissing(ffmpegDir, "https://m8sh.su/x/ffmpeg"); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if err := cloneIfMissing(gav1dDir, "https://m8sh.su/x/dav1d"); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + if err := writeCrossFiles(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + var targetsToBuild []buildTarget + for _, t := range availableTargets() { + missing := checkBuildDeps(t) + if len(missing) > 0 { + fmt.Printf("SKIP %s (missing deps: %v)\n", t.name, missing) + continue + } + targetsToBuild = append(targetsToBuild, t) + } + + for _, t := range targetsToBuild { + if artifactExists(t) { + fmt.Printf("\nSKIP %s (artifacts already exist)\n", t.name) + continue + } + if err := buildDav1d(t); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + if err := buildFFmpeg(t); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + } + + if err := copyHeaders(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + + fmt.Println("\n=== all done ===") +}