Golang项目构建时如何收集各包源码目录中的信息

Golang项目构建时如何收集各包源码目录中的信息

目的

我正在尝试修改 Go 编译器(版本 1.17.7),以收集当前项目导入的各个包源代码目录中的信息,并将这些信息汇总到模块的根目录(即主包目录)下。

我的做法

我的做法是添加一个 -s 标志,允许 Go 编译器(gc)在每个包的源代码目录中输出信息。我已经成功实现了这一点。

然后我尝试收集这些信息。在 runBuild 函数(它是 gc 分发编译任务的入口点)中,我尝试在 b.Do(ctx, a) 第448行b.Do(ctx, a) 第459行 之后添加以下代码,以将每个包源代码目录中输出的信息收集到主包的目录中。我认为所有导入的包在 b.Do(ctx, a) 执行完毕后都会被编译。但收集操作失败了。

		b.Do(ctx, a)
		var main_path string
		for _, p := range pkgs {
			if p.Name == "main" {
				main_path = p.Dir  // 获取主包的路径
			}
		}
        // 收集每个 pkg.Dir 下的信息,并将文件从 pkg.Dir 移动到 main_path
		collect(pkgs, main_path)

发生了什么?

以下面的项目为例

pkg_export # 主包
|---main.go
|---mypkg # mypkg 包---被 main.go 导入
      |---utils.go

在我的实验中,使用命令 go build -a -gcflags=all=-s .,每个包都能正确地在各自的源代码目录中输出信息,但 collect 函数无法将每个包源代码目录中的信息收集到 main_path。在上面的示例项目中,情况看起来像这样:

pkg_export # 主包
|---main.go
|---info1.txt
|---mypkg # mypkg 包---被 main.go 导入
      |---utils.go
      |---info2.txt

在调用 collect(pkgs, main_path) 函数后,mypkg 目录下的 info2.txt 并没有合并到 pkg_export 目录下的 info1.txt 中。

期望的结果

在调用 collect(pkgs, main_path) 函数后,mypkg 目录下的 info2.txt 应该被合并到 pkg_export 目录下的 info1.txt 中。

我的问题

我的问题是:

  1. 我的代码插入的位置正确吗?我的目的是在模块的每个依赖项被编译且信息生成时进行收集。所有依赖项的编译是否在 b.Do(ctx, a) 第 547 行b.Do(ctx, a) 第 536 行 之后完成?
  2. 我使用的 load.Package 结构体的字段 p.Dir 是否正确?
  3. 编译过程是并行的吗?如果是,这种并行性是否导致了信息合并失败?

示例源代码

一个示例项目位于 Lslightly/pkg_export (github.com)

重新构建 gc 的方法是,在 Go 源代码的 src 目录下使用 ./clean.bash 然后使用 ./make.bash,新的 Go 编译器将会生成。

我非常感谢任何帮助和想法。谢谢!


更多关于Golang项目构建时如何收集各包源码目录中的信息的实战教程也可以访问 https://www.itying.com/category-94-b0.html

5 回复

嗨,@Lslightly,请问您修改编译器来实现这个功能,而不是使用像 golang.org/x/tools/go/packages 这样的包,是有什么特殊原因吗?

更多关于Golang项目构建时如何收集各包源码目录中的信息的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


分析每个源代码文件的内容以提取所需信息。您想要收集的具体信息将取决于您项目的要求。一些常见的例子包括函数或方法名称、类定义、变量声明、导入语句和注释。

我不确定 golang.org/x/tools/go/packages 能否获取到准确的导入关系图,所以我没有尝试它。此外,在 golang.org/x/tools/go/packagesPackage 结构体中,没有与 std 相关的字段,而我也希望过滤掉标准库包。

无论如何,感谢你的建议。下次当我需要导入信息时,我会尝试这个包,因为它比较轻量。

我弄明白了为什么我没有将 mypkg 目录下的 info2.txt 合并到 pkg_export 目录下的 info1.txt 中。原因在于,当在 pkg_export 目录下使用 go build -a -gcflags=all=-s . 进行编译时,以下代码中的 pkgs 切片只包含一个包(即 main 包)。

		b.Do(ctx, a)
		var main_path string
		for _, p := range pkgs {
			if p.Name == "main" {
				main_path = p.Dir  // 获取主包的路径
			}
		}
        // 收集每个 pkg.Dir 下的信息,并将文件从 pkg.Dir 移动到
		collect(pkgs, main_path)

b.Do(ctx, a) 中的 Action a 是一个由 main 包生成的 AutoAction 所产生的 LinkAction。Action a 包含一个 CompileAction,而该 CompileAction 会将其所有依赖的导入包的编译动作追加为其依赖项。相关代码位于 go/action.go#L459,如下所示。

for _, p1 := range p.Internal.Imports {
	a.Deps = append(a.Deps, b.CompileAction(depMode, depMode, p1))
}

因此,我需要做的是使用 p.Internal.Imports 来收集导入的包,使用 pkg.Dir 获取源代码目录,最后将 pkg.Dir 下的信息合并到 main_path,就像下面的代码一样。

func collectImportPackages(a *Action, pkg_set map[*load.Package]bool) {
	for _, dep := range a.Deps {
		if pkg_set[dep.Package] || dep.Package.Standard {
			continue
		} else {
			pkg_set[dep.Package] = true
		}
		fmt.Println(dep.Package.Name)
		collectImportPackages(dep, pkg_set)
	}
}

所有非标准库的导入包都将被收集为 key_set 映射的键。

从你的描述来看,主要问题在于编译的并行性和文件系统操作的时机。以下是针对你问题的具体分析:

1. 代码插入位置问题

你的插入位置不正确。b.Do(ctx, a) 是异步执行的,它会启动编译任务但不会等待所有任务完成。你需要等待所有编译任务完成后再进行收集操作。

正确的做法是在 runBuild 函数的末尾,所有编译任务完成后进行收集。查看源码,你应该在 build 函数返回前处理:

// 在 runBuild 函数末尾,build 函数调用后添加
func runBuild(ctx context.Context, cmd *base.Command, args []string) {
    // ... 原有代码 ...
    
    // 等待所有编译任务完成
    b.Wait()
    
    // 现在所有包都已编译完成,可以安全收集信息
    var mainPath string
    for _, p := range pkgs {
        if p.Name == "main" {
            mainPath = p.Dir
            break
        }
    }
    
    if mainPath != "" {
        collect(pkgs, mainPath)
    }
}

2. p.Dir 字段的正确性

p.Dir 字段是正确的,它表示包的源代码目录。但需要注意,对于标准库包和第三方依赖,它们的 Dir 路径可能不在当前项目目录下。

3. 编译并行性问题

是的,Go 编译过程是高度并行的。这是导致你收集失败的主要原因。多个包同时编译,你的收集操作可能在部分包还未完成编译时就执行了。

解决方案示例

以下是修改后的 collect 函数实现,需要考虑并发安全和文件锁定:

func collect(pkgs []*load.Package, mainPath string) error {
    // 创建主信息文件
    mainInfoFile := filepath.Join(mainPath, "compilation_info.txt")
    mainFile, err := os.OpenFile(mainInfoFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return err
    }
    defer mainFile.Close()
    
    // 使用互斥锁确保线程安全
    var mu sync.Mutex
    
    // 为每个包启动goroutine收集信息
    var wg sync.WaitGroup
    errors := make(chan error, len(pkgs))
    
    for _, pkg := range pkgs {
        wg.Add(1)
        go func(p *load.Package) {
            defer wg.Done()
            
            // 检查信息文件是否存在
            infoFile := filepath.Join(p.Dir, "info.txt")
            if _, err := os.Stat(infoFile); os.IsNotExist(err) {
                return // 该包没有生成信息文件
            }
            
            // 读取包的信息文件
            content, err := os.ReadFile(infoFile)
            if err != nil {
                errors <- fmt.Errorf("读取 %s 失败: %v", infoFile, err)
                return
            }
            
            // 写入主信息文件(需要加锁)
            mu.Lock()
            _, err = mainFile.WriteString(fmt.Sprintf("=== Package: %s ===\n", p.ImportPath))
            if err == nil {
                _, err = mainFile.Write(content)
                if err == nil {
                    _, err = mainFile.WriteString("\n\n")
                }
            }
            mu.Unlock()
            
            if err != nil {
                errors <- fmt.Errorf("写入主文件失败: %v", err)
            }
        }(pkg)
    }
    
    wg.Wait()
    close(errors)
    
    // 检查是否有错误
    for err := range errors {
        if err != nil {
            return err
        }
    }
    
    return nil
}

在编译器中的正确插入点

src/cmd/go/internal/work/build.go 中,找到 runBuild 函数,在函数返回前添加:

// 在 runBuild 函数中,build 调用之后
func runBuild(ctx context.Context, cmd *base.Command, args []string) {
    // ... 原有代码 ...
    
    // 执行构建
    b := work.NewBuilder(work.BuilderModeTool)
    defer func() {
        if err := b.Close(); err != nil && cfg.BuildN {
            base.Error(err)
        }
    }()
    
    // 原有的构建逻辑...
    
    // 等待所有任务完成
    b.Wait()
    
    // 收集编译信息
    if err := collectCompilationInfo(pkgs); err != nil {
        base.Fatalf("收集编译信息失败: %v", err)
    }
}

func collectCompilationInfo(pkgs []*load.Package) error {
    // 找到主包
    var mainPkg *load.Package
    for _, p := range pkgs {
        if p.Name == "main" && !strings.Contains(p.ImportPath, "/internal/") {
            mainPkg = p
            break
        }
    }
    
    if mainPkg == nil {
        return nil // 没有主包,不收集
    }
    
    return collect(pkgs, mainPkg.Dir)
}

关键修改点总结

  1. 等待机制:在 b.Do() 后添加 b.Wait() 确保所有编译任务完成
  2. 并发安全:使用 sync.Mutex 保护文件写入操作
  3. 错误处理:正确处理文件不存在的情况
  4. 时机正确:在所有编译任务完成后进行文件操作

这些修改能确保在所有包编译完成且信息文件生成后,安全地将信息收集到主包目录中。

回到顶部