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 ===") }