Files
gopeg/cmd/build/main.go
T

477 lines
12 KiB
Go
Raw Normal View History

2026-06-07 10:00:34 +03:00
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 ===")
}