1 #! /usr/bin/env python3
13 def __init__(self
, strip_path
: str = None):
14 self
.strip_path
= strip_path
16 def can_process(self
, fpath
: str):
19 def dump(self
, fpath
: str):
22 def _preparse_dump(self
, source
: str):
25 if not source
.startswith("MODULE"):
26 logging
.error("file doesn't starst with MODULE")
28 for line
in source
.split("\n"):
29 if line
.startswith("MODULE"):
30 #MODULE <os> <arch> <buildid> <filename>
31 line_split
= line
.split(" ")
32 if len(line_split
) != 5:
33 logging
.error("malformed MODULE entry")
35 _
, _os
, cpu
, buildid
, filename
= line_split
36 if filename
.endswith(".dbg"):
37 filename
= filename
[:-4]
40 meta
["debug_file"] = filename
41 meta
["code_file"] = filename
42 #see CompactIdentifier in symbol_upload.cc
43 meta
["debug_identifier"] = buildid
.replace("-", "")
44 dest
.write("MODULE {} {} {} {}".format(_os
, cpu
, buildid
, filename
))
46 elif line
.startswith("FILE"):
48 _
, line
, *path_split
= line
.split(" ")
49 path
= " ".join(path_split
)
50 path
= os
.path
.normpath(path
)
52 if self
.strip_path
and path
.startswith(self
.strip_path
):
53 path
= os
.path
.relpath(path
, self
.strip_path
)
55 dest
.write("FILE {} {}\n".format(line
, path
))
62 class WindowDumper(Dumper
):
63 def __init__(self
, *args
, **kwargs
):
64 super().__init
__(*args
, **kwargs
)
66 def can_process(self
, fpath
: str):
67 return any(fpath
.endswith(ext
) for ext
in ["dbg", "dll", "exe"])
69 def dump(self
, fpath
: str):
70 proc
= subprocess
.run(
71 ["dump_syms_win", "-r", fpath
],
72 stdout
=subprocess
.PIPE
,
73 stderr
=subprocess
.PIPE
,
75 if proc
.returncode
!= 0:
76 logging
.error("unable to extract symbols from {}".format(fpath
))
77 logging
.error(proc
.stderr
)
79 return self
._preparse
_dump
(proc
.stdout
.decode("utf8"))
81 class MacDumper(Dumper
):
82 def __init__(self
, *args
, **kwargs
):
83 super().__init
__(*args
, **kwargs
)
85 # Helper to check Mach-O header
86 def is_mach_o(self
, fpath
: str):
87 file = open(fpath
, "rb")
91 if b
'\xFE\xED\xFA\xCE' == header
:
94 elif b
'\xCE\xFA\xED\xFE' == header
:
97 elif b
'\xFE\xED\xFA\xCF' == header
:
100 elif b
'\xCF\xFA\xED\xFE' == header
:
104 def can_process(self
, fpath
: str):
105 if fpath
.endswith(".dylib") or os
.access(fpath
, os
.X_OK
):
106 return self
.is_mach_o(fpath
) and not os
.path
.islink(fpath
)
109 def dump(self
, fpath
: str):
110 dsymbundle
= fpath
+ ".dSYM"
111 if os
.path
.exists(dsymbundle
):
112 shutil
.rmtree(dsymbundle
)
114 #generate symbols file
115 proc
= subprocess
.run(
117 stdout
=subprocess
.DEVNULL
,
118 stderr
=subprocess
.PIPE
,
121 if proc
.returncode
!= 0:
122 logging
.error("unable to run dsymutil on {}:".format(fpath
))
123 logging
.error(proc
.stderr
)
125 if not os
.path
.exists(dsymbundle
):
126 logging
.error("No symbols in {}".format(fpath
))
129 proc
= subprocess
.run(
130 ["dump_syms", "-r", "-g", dsymbundle
, fpath
],
131 stdout
=subprocess
.PIPE
,
132 stderr
=subprocess
.PIPE
,
135 # Cleanup dsymbundle file
136 shutil
.rmtree(dsymbundle
)
138 if proc
.returncode
!= 0:
139 logging
.error("unable to extract symbols from {}:".format(fpath
))
140 logging
.error(proc
.stderr
)
143 return self
._preparse
_dump
(proc
.stdout
.decode("utf8"))
147 def store(self
, dump
: typing
.io
.TextIO
, meta
):
150 class HTTPOutputStore(OutputStore
):
151 def __init__(self
, url
: str, version
= None, prod
= None):
156 self
.extra_args
["ver"] = version
158 self
.extra_args
["prod"] = prod
160 def store(self
, dump
: typing
.io
.TextIO
, meta
):
161 post_args
= {**meta
, **self
.extra_args
}
162 r
= requests
.post(self
.url
, post_args
, files
={"symfile": dump
})
164 logging
.error("Unable to perform request, ret {}".format(r
.status_code
))
167 class LocalDirOutputStore(OutputStore
):
168 def __init__(self
, rootdir
: str):
170 self
.rootdir
= rootdir
172 def store(self
, dump
: typing
.io
, meta
):
173 basepath
= os
.path
.join(self
.rootdir
, meta
["debug_file"], meta
["debug_identifier"])
174 if not os
.path
.exists(basepath
):
175 os
.makedirs(basepath
)
176 with
open(os
.path
.join(basepath
, meta
["debug_file"] + ".sym"), "w+") as fd
:
177 shutil
.copyfileobj(dump
, fd
)
179 def process_dir(sourcedir
, dumper
, store
):
180 for root
, dirnames
, filenames
, in os
.walk(sourcedir
):
181 for fname
in filenames
:
182 if not dumper
.can_process(os
.path
.join(root
, fname
)):
184 logging
.info("processing {}".format(fname
))
185 meta
, dump
= dumper
.dump(os
.path
.join(root
, fname
))
186 if meta
is None or dump
is None:
187 logging
.warning("unable to dump {}".format(fname
))
189 store
.store(dump
, meta
)
193 parser
= argparse
.ArgumentParser(description
='extract symbols for breakpad and upload or store them')
194 parser
.add_argument("sourcedir", help="source directory")
195 parser
.add_argument("--upload-url", metavar
="URL", dest
="uploadurl", type=str, help="upload url")
196 parser
.add_argument("--strip-path", metavar
="PATH", dest
="strippath", type=str, help="strip path prefix")
197 parser
.add_argument("-p","--platform",metavar
="OS", dest
="platform",
198 choices
=["mac", "linux", "win"], required
=True, help="symbol platform (mac, linux, win)")
199 parser
.add_argument("--output-dir", metavar
="DIRECTORY", dest
="outdir", type=str, help="output directory")
200 parser
.add_argument("--version", metavar
="VERSION", dest
="version", type=str, help="specify symbol version for uploading")
201 parser
.add_argument("--prod", metavar
="PRODUCT", dest
="prod", type=str, help="specify product name for uploading")
202 parser
.add_argument("--log", metavar
="LOGLEVEL", dest
="log", type=str, help="log level (INFO, WARNING, ERROR)")
203 args
= parser
.parse_args()
206 numeric_level
= getattr(logging
, args
.log
.upper(), None)
207 if not isinstance(numeric_level
, int):
208 raise ValueError("Invalid log level: {}".format(loglevel
))
209 logging
.basicConfig(format
='%(levelname)s: %(message)s', level
=numeric_level
)
212 if args
.platform
== "win":
213 dumper
= WindowDumper(strip_path
=args
.strippath
)
214 elif args
.platform
== "mac":
215 dumper
= MacDumper(strip_path
=args
.strippath
)
217 logging
.error("Dumper {} is not implemented yet".format(args
.platform
))
221 store
=HTTPOutputStore(args
.uploadurl
, version
=args
.version
, prod
=args
.prod
)
223 store
=LocalDirOutputStore(args
.outdir
)
225 logging
.error("You must chose either --output-dir or --upload-url")
228 process_dir(args
.sourcedir
, dumper
, store
)
231 if __name__
== "__main__":
232 assert(sys
.version_info
>= (3,5))