diff --git a/README.md b/README.md index 875e06dc..e3d60ab6 100644 --- a/README.md +++ b/README.md @@ -41,18 +41,12 @@ There are other options,see [doc/INSTALLATION.md](./doc/INSTALLATION.md). There is no specific limitation on OS and Architecture. **All OS and Architectures** are supported by `xgo` as long as they are supported by `go`. - -OS: -- MacOS -- Linux -- Windows (+WSL) -- ... - -Architecture: -- x86 -- x86_64(amd64) -- arm64 -- ... +| | x86_64 | ARM64 | Any Other Arch... | +|---------|-----------|-----------|-----------| +| Linux | Y | Y | Y| +| Windows | Y | Y | Y| +| macOS | Y | Y | Y| +| Any Other OS... | Y | Y | Y| # Quick Start Let's write a unit test with `xgo`: diff --git a/README_zh_cn.md b/README_zh_cn.md index bc816ad6..e71d05f4 100644 --- a/README_zh_cn.md +++ b/README_zh_cn.md @@ -9,7 +9,7 @@ 允许对`go`的函数进行拦截, 并提供Mock和Trace等工具帮助开发者编写测试和快速调试。 -`xgo`作为一个预处理器工作在`go run`,`go build`,和`go test`之上(查看[blog](https://blog.xhd2015.xyz/posts/xgo-monkey-patching-in-go-using-toolexec/))。 +`xgo`作为一个预处理器工作在`go run`,`go build`,和`go test`之上(查看[blog](https://blog.xhd2015.xyz/zh/posts/xgo-monkey-patching-in-go-using-toolexec/))。 `xgo`对源代码和IR(中间码)进行预处理之后, 再调用`go`进行后续的编译工作。通过这种方式, `xgo`实现了一些在`go`中缺乏的能力。 @@ -39,17 +39,12 @@ xgo version 对OS和Arch没有限制, `xgo`支持所有`go`支持的OS和Arch。 -OS: -- macOS -- Linux -- Windows (+WSL) -- ... - -Arch: -- x86 -- x86_64(amd64) -- arm64 -- ... +| | x86_64 | ARM64 | 任何其他架构... | +|---------|-----------|-----------|-----------| +| Linux | Y | Y | Y| +| Windows | Y | Y | Y| +| macOS | Y | Y | Y| +| 任何其他OS... | Y | Y | Y| # 快速开始 我们基于`xgo`编写一个单元测试: diff --git a/cmd/xgo/main.go b/cmd/xgo/main.go index 00acfc5e..bd19300d 100644 --- a/cmd/xgo/main.go +++ b/cmd/xgo/main.go @@ -241,6 +241,7 @@ func handleBuild(cmd string, args []string) error { } buildCacheDir := filepath.Join(instrumentDir, "build-cache"+buildCacheSuffix) revisionFile := filepath.Join(instrumentDir, "xgo-revision.txt") + fullSyncRecord := filepath.Join(instrumentDir, "full-sync-record.txt") var realXgoSrc string if isDevelopment { @@ -294,7 +295,7 @@ func handleBuild(cmd string, args []string) error { } if isDevelopment || resetInstrument || revisionChanged { logDebug("sync goroot %s -> %s", goroot, instrumentGoroot) - err = syncGoroot(goroot, instrumentGoroot, revisionChanged) + err = syncGoroot(goroot, instrumentGoroot, fullSyncRecord) if err != nil { return err } diff --git a/cmd/xgo/patch.go b/cmd/xgo/patch.go index 2786ffac..d029d451 100644 --- a/cmd/xgo/patch.go +++ b/cmd/xgo/patch.go @@ -9,12 +9,34 @@ import ( "os/exec" "path/filepath" "strings" + "time" "github.com/xhd2015/xgo/support/filecopy" "github.com/xhd2015/xgo/support/goinfo" "github.com/xhd2015/xgo/support/osinfo" ) +// the _FilePath declared at toplevel +// serves as an item list where +// the runtime and compiler maybe +// affected. +// NOTE: do not remove files, always add files, +// these old files may exists in older version +// so can be cleared by newer xgo +type _FilePath []string + +func (c _FilePath) Join(s ...string) string { + return filepath.Join(filepath.Join(s...), filepath.Join(c...)) +} + +var affectedFiles []_FilePath + +func init() { + affectedFiles = append(affectedFiles, compilerFiles...) + affectedFiles = append(affectedFiles, runtimeFiles...) + affectedFiles = append(affectedFiles, reflectFiles...) +} + // assume go 1.20 // the patch should be idempotent // the origGoroot is used to generate runtime defs, see https://github.com/xhd2015/xgo/issues/4#issuecomment-2017880791 @@ -44,14 +66,6 @@ func patchRuntimeAndCompiler(origGoroot string, goroot string, xgoSrc string, go return nil } -func getInternalPatch(goroot string, subDirs ...string) string { - dir := filepath.Join(goroot, "src", "cmd", "compile", "internal", "xgo_rewrite_internal", "patch") - if len(subDirs) > 0 { - dir = filepath.Join(dir, filepath.Join(subDirs...)) - } - return dir -} - func replaceBuildIgnore(content []byte) ([]byte, error) { const buildIgnore = "//go:build ignore" @@ -101,50 +115,104 @@ func readOrEmpty(file string) (string, error) { } // NOTE: flagA never cause goroot to reset -func syncGoroot(goroot string, dstDir string, forceCopy bool) error { +func syncGoroot(goroot string, instrumentGoroot string, fullSyncRecordFile string) error { // check if src goroot has src/runtime srcRuntimeDir := filepath.Join(goroot, "src", "runtime") err := assertDir(srcRuntimeDir) if err != nil { return err } - if !forceCopy { - srcGoBin := filepath.Join(goroot, "bin", "go") - dstGoBin := filepath.Join(dstDir, "bin", "go") + var goBinaryChanged bool = true + srcGoBin := filepath.Join(goroot, "bin", "go") + dstGoBin := filepath.Join(instrumentGoroot, "bin", "go") + + srcFile, err := os.Stat(srcGoBin) + if err != nil { + return nil + } + if srcFile.IsDir() { + return fmt.Errorf("bad goroot: %s", goroot) + } + + dstFile, statErr := os.Stat(dstGoBin) + if statErr != nil { + if !os.IsNotExist(statErr) { + return statErr + } + } - srcFile, err := os.Stat(srcGoBin) + if dstFile != nil && !dstFile.IsDir() && dstFile.Size() == srcFile.Size() { + goBinaryChanged = false + } + + var doPartialCopy bool + if !goBinaryChanged && statNoErr(fullSyncRecordFile) { + // full sync record does not yet exist + doPartialCopy = true + } + if doPartialCopy { + // do partial copy + err := partialCopy(goroot, instrumentGoroot) if err != nil { - return nil + return err } - if srcFile.IsDir() { - return fmt.Errorf("bad goroot: %s", goroot) + } else { + rmErr := os.Remove(fullSyncRecordFile) + if rmErr != nil { + if !errors.Is(rmErr, os.ErrNotExist) { + return rmErr + } } - dstFile, statErr := os.Stat(dstGoBin) - if statErr != nil { - if !os.IsNotExist(statErr) { - return statErr - } + // need copy, delete target dst dir first + // TODO: use git worktree add if .git exists + err = filecopy.NewOptions(). + Concurrent(10). + CopyReplaceDir(goroot, instrumentGoroot) + if err != nil { + return err } - if dstFile != nil && !dstFile.IsDir() && dstFile.Size() == srcFile.Size() { - // already copied - return nil + // record this full sync + copyTime := time.Now().Format("2006-01-02T15:04:05Z07:00") + err = os.WriteFile(fullSyncRecordFile, []byte(copyTime), 0755) + if err != nil { + return err } } + // change binary executable + return nil +} - // need copy, delete target dst dir first - // TODO: use git worktree add if .git exists - err = filecopy.NewOptions(). - Concurrent(10). - CopyReplaceDir(goroot, dstDir) +func partialCopy(goroot string, instrumentGoroot string) error { + err := os.RemoveAll(xgoRewriteInternal.Join(instrumentGoroot)) if err != nil { return err } - // change binary executable + for _, affectedFile := range affectedFiles { + srcFile := affectedFile.Join(goroot) + dstFile := affectedFile.Join(instrumentGoroot) + + err := filecopy.CopyFileAll(srcFile, dstFile) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return err + } + // delete dstFile + err := os.RemoveAll(dstFile) + if err != nil { + return err + } + continue + } + } return nil } +func statNoErr(f string) bool { + _, err := os.Stat(f) + return err == nil +} func buildInstrumentTool(goroot string, xgoSrc string, compilerBin string, compilerBuildIDFile string, execToolBin string, xgoBin string, debugPkg string, logCompile bool, noSetup bool, debugWithDlv bool) (compilerChanged bool, toolExecFlag string, err error) { var execToolCmd []string if !noSetup { diff --git a/cmd/xgo/patch_compiler.go b/cmd/xgo/patch_compiler.go index 415f8c8c..a56fe912 100644 --- a/cmd/xgo/patch_compiler.go +++ b/cmd/xgo/patch_compiler.go @@ -17,6 +17,30 @@ import ( "github.com/xhd2015/xgo/support/osinfo" ) +var xgoRewriteInternal = _FilePath{"src", "cmd", "compile", "internal", "xgo_rewrite_internal"} +var xgoRewriteInternalPatch = append(xgoRewriteInternal, "patch") + +var xgoNodes = _FilePath{"src", "cmd", "compile", "internal", "syntax", "xgo_nodes.go"} +var gcMain = _FilePath{"src", "cmd", "compile", "internal", "gc", "main.go"} +var noderFile = _FilePath{"src", "cmd", "compile", "internal", "noder", "noder.go"} +var noderFile16 = _FilePath{"src", "cmd", "compile", "internal", "gc", "noder.go"} +var irgenFile = _FilePath{"src", "cmd", "compile", "internal", "noder", "irgen.go"} + +var compilerRuntimeDefFile = _FilePath{"src", "cmd", "compile", "internal", "typecheck", "_builtin", "runtime.go"} +var compilerRuntimeDefFile18 = _FilePath{"src", "cmd", "compile", "internal", "typecheck", "builtin", "runtime.go"} +var compilerRuntimeDefFile16 = _FilePath{"src", "cmd", "compile", "internal", "gc", "builtin", "runtime.go"} + +var compilerFiles = []_FilePath{ + xgoNodes, + gcMain, + noderFile, + noderFile16, + irgenFile, + compilerRuntimeDefFile, + compilerRuntimeDefFile18, + compilerRuntimeDefFile16, +} + func patchCompiler(origGoroot string, goroot string, goVersion *goinfo.GoVersion, xgoSrc string, forceReset bool, syncWithLink bool) error { // copy compiler internal dependencies err := importCompileInternalPatch(goroot, xgoSrc, forceReset, syncWithLink) @@ -50,6 +74,37 @@ func patchCompiler(origGoroot string, goroot string, goVersion *goinfo.GoVersion return nil } +func patchCompilerInternal(goroot string, goVersion *goinfo.GoVersion) error { + // src/cmd/compile/internal/noder/noder.go + err := patchCompilerNoder(goroot, goVersion) + if err != nil { + return fmt.Errorf("patching noder: %w", err) + } + if goVersion.Major == 1 && (goVersion.Minor == 18 || goVersion.Minor == 19) { + err := poatchIRGenericGen(goroot, goVersion) + if err != nil { + return fmt.Errorf("patching generic trap: %w", err) + } + } + err = patchSynatxNode(goroot, goVersion) + if err != nil { + return fmt.Errorf("patching syntax node:%w", err) + } + err = patchGcMain(goroot, goVersion) + if err != nil { + return fmt.Errorf("patching gc main:%w", err) + } + return nil +} + +func getInternalPatch(goroot string, subDirs ...string) string { + dir := filepath.Join(goroot, filepath.Join(xgoRewriteInternalPatch...)) + if len(subDirs) > 0 { + dir = filepath.Join(dir, filepath.Join(subDirs...)) + } + return dir +} + func patchSynatxNode(goroot string, goVersion *goinfo.GoVersion) error { if goVersion.Major > 1 || goVersion.Minor >= 22 { return nil @@ -67,12 +122,12 @@ func patchSynatxNode(goroot string, goVersion *goinfo.GoVersion) error { if len(fragments) == 0 { return nil } - file := filepath.Join(goroot, "src", "cmd", "compile", "internal", "syntax", "xgo_nodes.go") + file := filepath.Join(goroot, filepath.Join(xgoNodes...)) return os.WriteFile(file, []byte("package syntax\n"+strings.Join(fragments, "\n")), 0755) } func patchGcMain(goroot string, goVersion *goinfo.GoVersion) error { - file := filepath.Join(goroot, "src", "cmd", "compile", "internal", "gc", "main.go") + file := filepath.Join(goroot, filepath.Join(gcMain...)) go116AndUnder := goVersion.Major == 1 && goVersion.Minor <= 16 go117 := goVersion.Major == 1 && goVersion.Minor == 17 go118 := goVersion.Major == 1 && goVersion.Minor == 18 @@ -198,12 +253,12 @@ func patchGcMain(goroot string, goVersion *goinfo.GoVersion) error { } func patchCompilerNoder(goroot string, goVersion *goinfo.GoVersion) error { - files := []string{"src", "cmd", "compile", "internal", "noder", "noder.go"} + files := []string(noderFile) var noderFiles string if goVersion.Major == 1 { minor := goVersion.Minor if minor == 16 { - files = []string{"src", "cmd", "compile", "internal", "gc", "noder.go"} + files = []string(noderFile16) noderFiles = patch.NoderFiles_1_17 } else if minor == 17 { noderFiles = patch.NoderFiles_1_17 @@ -254,7 +309,7 @@ func patchCompilerNoder(goroot string, goVersion *goinfo.GoVersion) error { } func poatchIRGenericGen(goroot string, goVersion *goinfo.GoVersion) error { - file := filepath.Join(goroot, "src", "cmd", "compile", "internal", "noder", "irgen.go") + file := irgenFile.Join(goroot) return editFile(file, func(content string) (string, error) { imports := []string{ `xgo_patch "cmd/compile/internal/xgo_rewrite_internal/patch"`, @@ -387,15 +442,14 @@ func patchRuntimeDef(origGoroot string, goroot string, goVersion *goinfo.GoVersi return nil } - func prepareRuntimeDefs(goRoot string, goVersion *goinfo.GoVersion) error { - runtimeDefFiles := []string{"src", "cmd", "compile", "internal", "typecheck", "_builtin", "runtime.go"} + runtimeDefFiles := []string(compilerRuntimeDefFile) if goVersion.Major == 1 && goVersion.Minor <= 19 { if goVersion.Minor > 16 { // in go1.19 and below, builtin has no _ prefix - runtimeDefFiles = []string{"src", "cmd", "compile", "internal", "typecheck", "builtin", "runtime.go"} + runtimeDefFiles = []string(compilerRuntimeDefFile18) } else { - runtimeDefFiles = []string{"src", "cmd", "compile", "internal", "gc", "builtin", "runtime.go"} + runtimeDefFiles = []string(compilerRuntimeDefFile16) } } runtimeDefFile := filepath.Join(runtimeDefFiles...) @@ -411,26 +465,3 @@ func prepareRuntimeDefs(goRoot string, goVersion *goinfo.GoVersion) error { return content, nil }) } - -func patchCompilerInternal(goroot string, goVersion *goinfo.GoVersion) error { - // src/cmd/compile/internal/noder/noder.go - err := patchCompilerNoder(goroot, goVersion) - if err != nil { - return fmt.Errorf("patching noder: %w", err) - } - if goVersion.Major == 1 && (goVersion.Minor == 18 || goVersion.Minor == 19) { - err := poatchIRGenericGen(goroot, goVersion) - if err != nil { - return fmt.Errorf("patching generic trap: %w", err) - } - } - err = patchSynatxNode(goroot, goVersion) - if err != nil { - return fmt.Errorf("patching syntax node:%w", err) - } - err = patchGcMain(goroot, goVersion) - if err != nil { - return fmt.Errorf("patching gc main:%w", err) - } - return nil -} diff --git a/cmd/xgo/patch_reflect.go b/cmd/xgo/patch_reflect.go index 1455d3a3..d1eb890c 100644 --- a/cmd/xgo/patch_reflect.go +++ b/cmd/xgo/patch_reflect.go @@ -16,8 +16,18 @@ import ( "github.com/xhd2015/xgo/support/transform" ) +var xgoReflectFile = _FilePath{"src", "reflect", "xgo_reflect.go"} +var reflectValueFile = _FilePath{"src", "reflect", "value.go"} +var reflectTypeFile = _FilePath{"src", "reflect", "type.go"} + +var reflectFiles = []_FilePath{ + xgoReflectFile, + reflectValueFile, + reflectTypeFile, +} + func addReflectFunctions(goroot string, goVersion *goinfo.GoVersion, xgoSrc string) error { - dstFile := filepath.Join(goroot, "src", "reflect", "xgo_reflect.go") + dstFile := filepath.Join(goroot, filepath.Join(xgoReflectFile...)) content, err := readXgoSrc(xgoSrc, []string{"trap_runtime", "xgo_reflect.go"}) if err != nil { return err @@ -28,11 +38,11 @@ func addReflectFunctions(goroot string, goVersion *goinfo.GoVersion, xgoSrc stri return fmt.Errorf("file %s: %w", filepath.Base(dstFile), err) } - valCode, err := transformReflectValue(filepath.Join(goroot, "src", "reflect", "value.go")) + valCode, err := transformReflectValue(filepath.Join(goroot, filepath.Join(reflectValueFile...))) if err != nil { return fmt.Errorf("transforming reflect/value.go: %w", err) } - typeCode, err := transformReflectType(filepath.Join(goroot, "src", "reflect", "type.go")) + typeCode, err := transformReflectType(filepath.Join(goroot, filepath.Join(reflectTypeFile...))) if err != nil { return fmt.Errorf("transforming reflect/type.go: %w", err) } diff --git a/cmd/xgo/patch_runtime.go b/cmd/xgo/patch_runtime.go index 05f56bb1..d95b7ff9 100644 --- a/cmd/xgo/patch_runtime.go +++ b/cmd/xgo/patch_runtime.go @@ -12,6 +12,22 @@ import ( "github.com/xhd2015/xgo/support/goinfo" ) +var xgoAutoGenRegisterFuncHelper = _FilePath{"src", "runtime", "__xgo_autogen_register_func_helper.go"} +var xgoTrap = _FilePath{"src", "runtime", "xgo_trap.go"} +var runtimeProc = _FilePath{"src", "runtime", "proc.go"} +var testingFile = _FilePath{"src", "testing", "testing.go"} +var runtimeTime _FilePath = _FilePath{"src", "runtime", "time.go"} +var timeSleep _FilePath = _FilePath{"src", "time", "sleep.go"} + +var runtimeFiles = []_FilePath{ + xgoAutoGenRegisterFuncHelper, + xgoTrap, + runtimeProc, + testingFile, + runtimeTime, + timeSleep, +} + func patchRuntimeAndTesting(goroot string) error { err := patchRuntimeProc(goroot) if err != nil { @@ -38,7 +54,7 @@ func addRuntimeFunctions(goroot string, goVersion *goinfo.GoVersion, xgoSrc stri // add debug file // rational: when debugging, dlv will jump to __xgo_autogen_register_func_helper.go // previously this file does not exist, making the debugging blind - runtimeAutoGenFile := filepath.Join(goroot, "src", "runtime", "__xgo_autogen_register_func_helper.go") + runtimeAutoGenFile := filepath.Join(goroot, filepath.Join(xgoAutoGenRegisterFuncHelper...)) srcAutoGen := getInternalPatch(goroot, "syntax", "helper_code.go") err = filecopy.CopyFile(srcAutoGen, runtimeAutoGenFile) if err != nil { @@ -46,7 +62,7 @@ func addRuntimeFunctions(goroot string, goVersion *goinfo.GoVersion, xgoSrc stri } } - dstFile := filepath.Join(goroot, "src", "runtime", "xgo_trap.go") + dstFile := filepath.Join(goroot, filepath.Join(xgoTrap...)) content, err := readXgoSrc(xgoSrc, []string{"trap_runtime", "xgo_trap.go"}) if err != nil { return false, err @@ -85,7 +101,7 @@ func addRuntimeFunctions(goroot string, goVersion *goinfo.GoVersion, xgoSrc stri } func patchRuntimeProc(goroot string) error { - procFile := filepath.Join(goroot, "src", "runtime", "proc.go") + procFile := filepath.Join(goroot, filepath.Join(runtimeProc...)) anchors := []string{ "func main() {", "doInit(", "runtime_inittask", ")", // first doInit for runtime @@ -120,7 +136,7 @@ func patchRuntimeProc(goroot string) error { } func patchRuntimeTesting(goroot string) error { - testingFile := filepath.Join(goroot, "src", "testing", "testing.go") + testingFile := filepath.Join(goroot, filepath.Join(testingFile...)) return editFile(testingFile, func(content string) (string, error) { // func tRunner(t *T, fn func(t *T)) { anchor := []string{"func tRunner(t *T", "{", "\n"} @@ -140,8 +156,8 @@ func patchRuntimeTesting(goroot string) error { // only required if need to mock time.Sleep func patchRuntimeTime(goroot string) error { - runtimeTimeFile := filepath.Join(goroot, "src", "runtime", "time.go") - timeSleepFile := filepath.Join(goroot, "src", "time", "sleep.go") + runtimeTimeFile := filepath.Join(goroot, filepath.Join(runtimeTime...)) + timeSleepFile := filepath.Join(goroot, filepath.Join(timeSleep...)) err := editFile(runtimeTimeFile, func(content string) (string, error) { content = replaceContentAfter(content, diff --git a/cmd/xgo/version.go b/cmd/xgo/version.go index 90cd392d..cb56d9fb 100644 --- a/cmd/xgo/version.go +++ b/cmd/xgo/version.go @@ -3,8 +3,8 @@ package main import "fmt" const VERSION = "1.0.18" -const REVISION = "f0cc411faaf6a9b12a4140f32e02b5f3c461a91d+1" -const NUMBER = 161 +const REVISION = "03d82b3e31832e5947c5d3a7ef8752f4f39db28c+1" +const NUMBER = 162 func getRevision() string { revSuffix := "" diff --git a/runtime/core/version.go b/runtime/core/version.go index f2712ad9..cf856700 100644 --- a/runtime/core/version.go +++ b/runtime/core/version.go @@ -7,8 +7,8 @@ import ( ) const VERSION = "1.0.18" -const REVISION = "f0cc411faaf6a9b12a4140f32e02b5f3c461a91d+1" -const NUMBER = 161 +const REVISION = "03d82b3e31832e5947c5d3a7ef8752f4f39db28c+1" +const NUMBER = 162 // these fields will be filled by compiler const XGO_VERSION = ""