Skip to content

Commit 10d14b5

Browse files
authored
Merge pull request #232 from bugsnag/integration/linux
v3.4.0 Release
2 parents fa461f7 + 42a280b commit 10d14b5

File tree

9 files changed

+219
-109
lines changed

9 files changed

+219
-109
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [3.4.0] - 2025-09-12
4+
5+
### Added
6+
7+
- Add support for uploading Linux C/C++ symbol files [#232](https://github.com/bugsnag/bugsnag-cli/pull/232)
8+
39
## [3.3.2] - 2025-09-01
410

511
### Fixed

install.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ display_help() {
9191
EOS
9292
}
9393

94-
VERSION="3.3.2"
94+
VERSION="3.4.0"
9595

9696
while [[ "$#" -gt 0 ]]; do
9797
case "$1" in

js/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@bugsnag/cli",
3-
"version": "3.3.2",
3+
"version": "3.4.0",
44
"description": "BugSnag CLI",
55
"main": "dist/bugsnag-cli-wrapper.js",
66
"types": "dist/bugsnag-cli-wrapper.d.ts",

main.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"github.com/bugsnag/bugsnag-cli/pkg/upload"
1212
)
1313

14-
var package_version = "3.3.2"
14+
var package_version = "3.4.0"
1515

1616
func main() {
1717
commands := options.CLI{}
@@ -181,6 +181,17 @@ func main() {
181181
logger.Fatal(err.Error())
182182
}
183183

184+
case "upload linux", "upload linux <path>":
185+
if commands.ApiKey == "" {
186+
logger.Fatal("missing api key, please specify using `--api-key`")
187+
}
188+
189+
err := upload.ProcessLinux(commands, logger)
190+
191+
if err != nil {
192+
logger.Fatal(err.Error())
193+
}
194+
184195
case "create-build", "create-build <path>":
185196
// Create Build Info
186197
CreateBuildOptions, err := build.GatherBuildInfo(commands)

pkg/options/linux.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package options
2+
3+
import "github.com/bugsnag/bugsnag-cli/pkg/utils"
4+
5+
type LinuxOptions struct {
6+
Path utils.Paths `arg:"" name:"path" help:"The path to the directory or file to upload" type:"path" default:"."`
7+
ApplicationId string `help:"A unique application ID, usually the package name, of the application"`
8+
ProjectRoot string `help:"The path to strip from the beginning of source file names referenced in stacktraces on the BugSnag dashboard" type:"path" default:"."`
9+
VersionName string `help:"The version of the application"`
10+
Overwrite bool `help:"Whether to ignore and overwrite existing uploads with same identifier, rather than failing if a matching file exists"`
11+
}

pkg/options/options.go

Lines changed: 26 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -164,35 +164,37 @@ type Breakpad struct {
164164
Overwrite bool `help:"Whether to ignore and overwrite existing uploads with same identifier, rather than failing if a matching file exists"`
165165
}
166166

167+
type Upload struct {
168+
// shared options
169+
Retries int `help:"The number of retry attempts before failing an upload request" default:"0"`
170+
Timeout int `help:"The number of seconds to wait before failing an upload request" default:"300"`
171+
UploadAPIRootUrl string `help:"The upload server hostname, optionally containing port number"`
172+
173+
// required options
174+
All DiscoverAndUploadAny `cmd:"" help:"Upload any symbol/mapping files"`
175+
AndroidAab AndroidAabMapping `cmd:"" help:"Process and upload application bundle files for Android"`
176+
AndroidNdk AndroidNdkMapping `cmd:"" help:"Process and upload NDK symbol files for Android"`
177+
AndroidProguard AndroidProguardMapping `cmd:"" help:"Process and upload Proguard/R8 mapping files for Android"`
178+
DartSymbol DartSymbol `cmd:"" help:"Process and upload symbol files for Flutter" name:"dart"`
179+
XcodeBuild XcodeBuild `cmd:"" help:"Upload dSYMs for iOS from a build"`
180+
Dsym Dsym `cmd:"" help:"(deprecated) Upload dSYMs for iOS"`
181+
XcodeArchive XcodeArchive `cmd:"" help:"Upload dSYMs for iOS from a Xcode archive"`
182+
Js Js `cmd:"" help:"Upload source maps for JavaScript"`
183+
ReactNative ReactNative `cmd:"" help:"Upload source maps for React Native"`
184+
ReactNativeAndroid ReactNativeAndroid `cmd:"" help:"Upload source maps for React Native Android"`
185+
ReactNativeIos ReactNativeIos `cmd:"" help:"Upload source maps for React Native iOS"`
186+
UnityAndroid UnityAndroid `cmd:"" help:"Upload Android mappings and NDK symbol files from Unity projects"`
187+
UnityIos UnityIos `cmd:"" help:"Upload iOS mappings and dSYMs from Unity projects"`
188+
Breakpad Breakpad `cmd:"" help:"Upload breakpad .sym files"`
189+
Linux LinuxOptions `cmd:"" help:"Upload symbol/mapping files"`
190+
}
191+
167192
// Unique CLI options
168193
type CLI struct {
169194
Globals
170-
171195
CreateAndroidBuildId CreateAndroidBuildId `cmd:"" help:"Generate a reproducible Build ID from .dex files"`
172196
CreateBuild CreateBuild `cmd:"" help:"Provide extra information whenever you build, release, or deploy your application"`
173-
Upload struct {
174-
// shared options
175-
Retries int `help:"The number of retry attempts before failing an upload request" default:"0"`
176-
Timeout int `help:"The number of seconds to wait before failing an upload request" default:"300"`
177-
UploadAPIRootUrl string `help:"The upload server hostname, optionally containing port number"`
178-
179-
// required options
180-
All DiscoverAndUploadAny `cmd:"" help:"Upload any symbol/mapping files"`
181-
AndroidAab AndroidAabMapping `cmd:"" help:"Process and upload application bundle files for Android"`
182-
AndroidNdk AndroidNdkMapping `cmd:"" help:"Process and upload NDK symbol files for Android"`
183-
AndroidProguard AndroidProguardMapping `cmd:"" help:"Process and upload Proguard/R8 mapping files for Android"`
184-
DartSymbol DartSymbol `cmd:"" help:"Process and upload symbol files for Flutter" name:"dart"`
185-
XcodeBuild XcodeBuild `cmd:"" help:"Upload dSYMs for iOS from a build"`
186-
Dsym Dsym `cmd:"" help:"(deprecated) Upload dSYMs for iOS"`
187-
XcodeArchive XcodeArchive `cmd:"" help:"Upload dSYMs for iOS from a Xcode archive"`
188-
Js Js `cmd:"" help:"Upload source maps for JavaScript"`
189-
ReactNative ReactNative `cmd:"" help:"Upload source maps for React Native"`
190-
ReactNativeAndroid ReactNativeAndroid `cmd:"" help:"Upload source maps for React Native Android"`
191-
ReactNativeIos ReactNativeIos `cmd:"" help:"Upload source maps for React Native iOS"`
192-
UnityAndroid UnityAndroid `cmd:"" help:"Upload Android mappings and NDK symbol files from Unity projects"`
193-
UnityIos UnityIos `cmd:"" help:"Upload iOS mappings and dSYMs from Unity projects"`
194-
Breakpad Breakpad `cmd:"" help:"Upload breakpad .sym files"`
195-
} `cmd:"" help:"Upload symbol/mapping files"`
197+
Upload Upload `cmd:"" help:"Upload symbol/mapping files"`
196198
}
197199

198200
type CreateAndroidBuildId struct {

pkg/upload/linux.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package upload
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
"strings"
7+
8+
"github.com/bugsnag/bugsnag-cli/pkg/log"
9+
"github.com/bugsnag/bugsnag-cli/pkg/options"
10+
"github.com/bugsnag/bugsnag-cli/pkg/server"
11+
"github.com/bugsnag/bugsnag-cli/pkg/utils"
12+
)
13+
14+
// uploadSymbolFile uploads a single Linux symbol file to the Bugsnag symbol server.
15+
//
16+
// Parameters:
17+
// - symbolFile: The path to the symbol file to upload.
18+
// - linuxOpts: Linux-specific upload options including appId, versionName, etc.
19+
// - opts: Global CLI options including an API key and overwrite behavior.
20+
// - logger: Logger for structured output.
21+
//
22+
// Returns:
23+
// - error: non-nil if the upload fails due to request or file issues.
24+
func uploadSymbolFile(symbolFile string, linuxOpts options.LinuxOptions, opts options.CLI, logger log.Logger) error {
25+
uploadOpts := map[string]string{}
26+
27+
if linuxOpts.ApplicationId != "" {
28+
uploadOpts["appId"] = linuxOpts.ApplicationId
29+
}
30+
if linuxOpts.VersionName != "" {
31+
uploadOpts["versionName"] = linuxOpts.VersionName
32+
}
33+
if linuxOpts.ProjectRoot != "" {
34+
uploadOpts["projectRoot"] = linuxOpts.ProjectRoot
35+
}
36+
if base := filepath.Base(symbolFile); base != "" {
37+
uploadOpts["sharedObjectName"] = base
38+
}
39+
if linuxOpts.Overwrite {
40+
uploadOpts["overwrite"] = "true"
41+
}
42+
43+
fileField := map[string]server.FileField{
44+
"soFile": server.LocalFile(symbolFile),
45+
}
46+
47+
if err := server.ProcessFileRequest(
48+
opts.ApiKey,
49+
"/linux",
50+
uploadOpts,
51+
fileField,
52+
filepath.Base(symbolFile),
53+
opts,
54+
logger,
55+
); err != nil {
56+
return fmt.Errorf("uploading Linux symbol file %q: %w", symbolFile, err)
57+
}
58+
return nil
59+
}
60+
61+
// ProcessLinux locates, validates, and uploads Linux symbol files.
62+
//
63+
// Parameters:
64+
// - opts: Global CLI options including upload configuration and API key.
65+
// - logger: Logger for structured logging and debug output.
66+
//
67+
// Behavior:
68+
// - Scans provided paths for build folders or symbol files.
69+
// - Reads metadata (appId, versionName) if provided.
70+
// - Uploads all recognized symbol files to the Bugsnag /linux endpoint.
71+
//
72+
// Returns:
73+
// - error: non-nil if scanning, build ID resolution, or upload fails.
74+
func ProcessLinux(opts options.CLI, logger log.Logger) error {
75+
linuxOpts := opts.Upload.Linux
76+
77+
var fileList []string
78+
var soFileList []string
79+
var err error
80+
81+
for _, path := range linuxOpts.Path {
82+
// Build a list of potential symbol files
83+
if utils.IsDir(path) {
84+
logger.Info(fmt.Sprintf("Scanning path: %s", path))
85+
fileList, err = utils.BuildFileList([]string{path})
86+
if err != nil {
87+
return fmt.Errorf("building file list from %q: %w", path, err)
88+
}
89+
logger.Debug(fmt.Sprintf("Found %d files in directory %s", len(fileList), path))
90+
} else {
91+
fileList = append(fileList, path)
92+
}
93+
94+
// Filter for valid ELF symbol files
95+
for _, file := range fileList {
96+
// Check for .so, .so.debug, and .debug files
97+
if strings.HasSuffix(file, ".so") || strings.HasSuffix(file, ".so.debug") || strings.HasSuffix(file, ".debug") {
98+
ok, err := utils.IsSymbolFile(file)
99+
if err != nil {
100+
return err
101+
}
102+
if ok {
103+
soFileList = append(soFileList, file)
104+
logger.Debug(fmt.Sprintf("Found symbol file: %s", file))
105+
} else {
106+
logger.Debug(fmt.Sprintf("%s is not a valid symbol file.", file))
107+
}
108+
} else {
109+
// Skip files not matching the common suffixes
110+
logger.Debug(fmt.Sprintf("Skipping non-symbol file: %s", file))
111+
}
112+
}
113+
114+
if linuxOpts.ProjectRoot != "" {
115+
logger.Debug(fmt.Sprintf("Using project root: %s", linuxOpts.ProjectRoot))
116+
}
117+
118+
if len(soFileList) == 0 {
119+
logger.Warn("No symbol files found to upload")
120+
return nil
121+
}
122+
123+
for _, file := range soFileList {
124+
if err := uploadSymbolFile(file, linuxOpts, opts, logger); err != nil {
125+
return err
126+
}
127+
}
128+
}
129+
return nil
130+
}

pkg/utils/files.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package utils
22

33
import (
4+
"debug/elf"
5+
"errors"
46
"fmt"
57
"os"
68
"os/exec"
@@ -211,3 +213,27 @@ func LocationOf(something string) string {
211213
location, _ := cmd.Output()
212214
return strings.TrimSpace(string(location))
213215
}
216+
217+
// IsSymbolFile determines whether the given file path points to a native library
218+
// or debug symbol file.
219+
//
220+
// Parameters:
221+
// - path: The file system path to check.
222+
//
223+
// Returns:
224+
// - bool: true if the file is recognized as a symbol/debug file, false otherwise.
225+
// - error: non-nil only if the ELF file cannot be read due to an I/O error.
226+
// If the file is not an ELF, it returns (false, nil).
227+
func IsSymbolFile(path string) (bool, error) {
228+
f, err := elf.Open(path)
229+
if err != nil {
230+
// Not a valid ELF file: treat as a non-symbol file, not as an error
231+
if errors.Is(err, elf.ErrNoSymbols) || strings.Contains(err.Error(), "bad magic number") {
232+
return false, nil
233+
}
234+
return false, fmt.Errorf("failed to open ELF file %q: %w", path, err)
235+
}
236+
defer f.Close()
237+
238+
return true, nil
239+
}

0 commit comments

Comments
 (0)