Patrick Williams | b48b7b4 | 2016-08-17 15:04:38 -0500 | [diff] [blame] | 1 | # Inherit this class when you want to allow Mozilla Socorro to link Breakpad's |
| 2 | # stack trace information to the correct source code revision. |
| 3 | # This class creates a new version of the symbol file (.sym) created by |
| 4 | # Breakpad. The absolute file paths in the symbol file will be replaced by VCS, |
| 5 | # branch, file and revision of the source file. That information facilitates the |
| 6 | # lookup of a particular source code line in the stack trace. |
| 7 | # |
| 8 | # Use example: |
| 9 | # |
| 10 | # BREAKPAD_BIN = "YourBinary" |
| 11 | # inherit socorro-syms |
| 12 | # |
| 13 | |
| 14 | # We depend on Breakpad creating the original symbol file. |
| 15 | inherit breakpad |
| 16 | |
| 17 | PACKAGE_PREPROCESS_FUNCS += "symbol_file_preprocess" |
| 18 | PACKAGES =+ "${PN}-socorro-syms" |
Patrick Williams | 213cb26 | 2021-08-07 19:21:33 -0500 | [diff] [blame] | 19 | FILES:${PN}-socorro-syms = "/usr/share/socorro-syms" |
Patrick Williams | b48b7b4 | 2016-08-17 15:04:38 -0500 | [diff] [blame] | 20 | |
| 21 | |
| 22 | python symbol_file_preprocess() { |
| 23 | |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 24 | package_dir = d.getVar("PKGD") |
| 25 | breakpad_bin = d.getVar("BREAKPAD_BIN") |
Patrick Williams | b48b7b4 | 2016-08-17 15:04:38 -0500 | [diff] [blame] | 26 | if not breakpad_bin: |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 27 | package_name = d.getVar("PN") |
Patrick Williams | b48b7b4 | 2016-08-17 15:04:38 -0500 | [diff] [blame] | 28 | bb.error("Package %s depends on Breakpad via socorro-syms. See " |
| 29 | "breakpad.bbclass for instructions on setting up the Breakpad " |
| 30 | "configuration." % package_name) |
| 31 | raise ValueError("BREAKPAD_BIN not defined in %s." % package_name) |
| 32 | |
| 33 | sym_file_name = breakpad_bin + ".sym" |
| 34 | |
| 35 | breakpad_syms_dir = os.path.join( |
| 36 | package_dir, "usr", "share", "breakpad-syms") |
| 37 | socorro_syms_dir = os.path.join( |
| 38 | package_dir, "usr", "share", "socorro-syms") |
| 39 | if not os.path.exists(socorro_syms_dir): |
| 40 | os.makedirs(socorro_syms_dir) |
| 41 | |
| 42 | breakpad_sym_file_path = os.path.join(breakpad_syms_dir, sym_file_name) |
| 43 | socorro_sym_file_path = os.path.join(socorro_syms_dir, sym_file_name) |
| 44 | |
| 45 | create_socorro_sym_file(d, breakpad_sym_file_path, socorro_sym_file_path) |
| 46 | |
| 47 | arrange_socorro_sym_file(socorro_sym_file_path, socorro_syms_dir) |
| 48 | |
| 49 | return |
| 50 | } |
| 51 | |
| 52 | |
| 53 | def run_command(command, directory): |
| 54 | |
| 55 | (output, error) = bb.process.run(command, cwd=directory) |
| 56 | if error: |
| 57 | raise bb.process.ExecutionError(command, error) |
| 58 | |
| 59 | return output.rstrip() |
| 60 | |
| 61 | |
| 62 | def create_socorro_sym_file(d, breakpad_sym_file_path, socorro_sym_file_path): |
| 63 | |
| 64 | # In the symbol file, all source files are referenced like the following. |
| 65 | # FILE 123 /path/to/some/File.cpp |
| 66 | # Go through all references and replace the file paths with repository |
| 67 | # paths. |
| 68 | with open(breakpad_sym_file_path, 'r') as breakpad_sym_file, \ |
| 69 | open(socorro_sym_file_path, 'w') as socorro_sym_file: |
| 70 | |
| 71 | for line in breakpad_sym_file: |
| 72 | if line.startswith("FILE "): |
| 73 | socorro_sym_file.write(socorro_file_reference(d, line)) |
| 74 | else: |
| 75 | socorro_sym_file.write(line) |
| 76 | |
| 77 | return |
| 78 | |
| 79 | |
| 80 | def socorro_file_reference(d, line): |
| 81 | |
| 82 | # The 3rd position is the file path. See example above. |
| 83 | source_file_path = line.split()[2] |
| 84 | source_file_repo_path = repository_path( |
| 85 | d, os.path.normpath(source_file_path)) |
| 86 | |
| 87 | # If the file could be found in any repository then replace it with the |
| 88 | # repository's path. |
| 89 | if source_file_repo_path: |
| 90 | return line.replace(source_file_path, source_file_repo_path) |
| 91 | |
| 92 | return line |
| 93 | |
| 94 | |
| 95 | def repository_path(d, source_file_path): |
| 96 | |
| 97 | if not os.path.isfile(source_file_path): |
| 98 | return None |
| 99 | |
| 100 | # Check which VCS is used and use that to extract repository information. |
| 101 | (output, error) = bb.process.run("git status", |
| 102 | cwd=os.path.dirname(source_file_path)) |
| 103 | if not error: |
| 104 | # Make sure the git repository we just found wasn't the yocto repository |
| 105 | # itself, i.e. the root of the repository we're looking for must be a |
| 106 | # child of the build directory TOPDIR. |
| 107 | git_root_dir = run_command( |
| 108 | "git rev-parse --show-toplevel", os.path.dirname(source_file_path)) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 109 | if not git_root_dir.startswith(d.getVar("TOPDIR")): |
Patrick Williams | b48b7b4 | 2016-08-17 15:04:38 -0500 | [diff] [blame] | 110 | return None |
| 111 | |
| 112 | return git_repository_path(source_file_path) |
| 113 | |
| 114 | # Here we can add support for other VCSs like hg, svn, cvs, etc. |
| 115 | |
| 116 | # The source file isn't under any VCS so we leave it be. |
| 117 | return None |
| 118 | |
| 119 | |
| 120 | def is_local_url(url): |
| 121 | |
| 122 | return \ |
| 123 | url.startswith("file:") or url.startswith("/") or url.startswith("./") |
| 124 | |
| 125 | |
| 126 | def git_repository_path(source_file_path): |
| 127 | |
| 128 | import re |
| 129 | |
| 130 | # We need to extract the following. |
| 131 | # (1): VCS URL, (2): branch, (3): repo root directory name, (4): repo file, |
| 132 | # (5): revision. |
| 133 | |
| 134 | source_file_dir = os.path.dirname(source_file_path) |
| 135 | |
| 136 | # (1) Get the VCS URL and extract the server part, i.e. change the URL from |
| 137 | # gitolite@git.someserver.com:SomeRepo.git to just git.someserver.com. |
| 138 | source_long_url = run_command( |
| 139 | "git config --get remote.origin.url", source_file_dir) |
| 140 | |
| 141 | # The URL could be a local download directory. If so, get the URL again |
| 142 | # using the local directory's config file. |
| 143 | if is_local_url(source_long_url): |
| 144 | git_config_file = os.path.join(source_long_url, "config") |
| 145 | source_long_url = run_command( |
| 146 | "git config --file %s --get remote.origin.url" % git_config_file, |
| 147 | source_file_dir) |
| 148 | |
| 149 | # If also the download directory redirects to a local git directory, |
| 150 | # then we're probably using source code from a local debug branch which |
| 151 | # won't be accessible by Socorro. |
| 152 | if is_local_url(source_long_url): |
| 153 | return None |
| 154 | |
| 155 | # The URL can have several formats. A full list can be found using |
| 156 | # git help clone. Extract the server part with a regex. |
| 157 | url_match = re.search(".*(://|@)([^:/]*).*", source_long_url) |
| 158 | source_server = url_match.group(2) |
| 159 | |
| 160 | # (2) Get the branch for this file. |
| 161 | source_branch_list = run_command("git show-branch --list", source_file_dir) |
| 162 | source_branch_match = re.search(".*?\[(.*?)\].*", source_branch_list) |
| 163 | source_branch = source_branch_match.group(1) |
| 164 | |
| 165 | # (3) Since the repo root directory name can be changed without affecting |
| 166 | # git, we need to extract the name from something more reliable. |
| 167 | # The git URL has a repo name that we could use. We just need to strip off |
| 168 | # everything around it - from gitolite@git.someserver.com:SomeRepo.git/ to |
| 169 | # SomeRepo. |
| 170 | source_repo_dir = re.sub("/$", "", source_long_url) |
| 171 | source_repo_dir = re.sub("\.git$", "", source_repo_dir) |
| 172 | source_repo_dir = re.sub(".*[:/]", "", source_repo_dir) |
| 173 | |
| 174 | # (4) We know the file but want to remove all of the build system dependent |
| 175 | # path up to and including the repository's root directory, e.g. remove |
| 176 | # /home/someuser/dev/repo/projectx/ |
| 177 | source_toplevel = run_command( |
| 178 | "git rev-parse --show-toplevel", source_file_dir) |
| 179 | source_toplevel = source_toplevel + os.path.sep |
| 180 | source_file = source_file_path.replace(source_toplevel, "") |
| 181 | |
| 182 | # (5) Get the source revision this file is part of. |
| 183 | source_revision = run_command("git rev-parse HEAD", source_file_dir) |
| 184 | |
| 185 | # Assemble the repository path according to the Socorro format. |
| 186 | socorro_reference = "git:%s/%s:%s/%s:%s" % \ |
| 187 | (source_server, source_branch, |
| 188 | source_repo_dir, source_file, |
| 189 | source_revision) |
| 190 | |
| 191 | return socorro_reference |
| 192 | |
| 193 | |
| 194 | def arrange_socorro_sym_file(socorro_sym_file_path, socorro_syms_dir): |
| 195 | |
| 196 | import re |
| 197 | |
| 198 | # Breakpad's minidump_stackwalk needs a certain directory structure in order |
| 199 | # to find correct symbols when extracting a stack trace out of a minidump. |
| 200 | # The directory structure must look like the following. |
| 201 | # YourBinary/<hash>/YourBinary.sym |
| 202 | # YourLibrary.so/<hash>/YourLibrary.so.sym |
| 203 | # To be able to create such structure we need to extract the hash value that |
| 204 | # is found in each symbol file. The header of the symbol file looks |
| 205 | # something like this: |
| 206 | # MODULE Linux x86 A079E473106CE51C74C1C25AF536CCD30 YourBinary |
| 207 | # See |
| 208 | # http://code.google.com/p/google-breakpad/wiki/LinuxStarterGuide |
| 209 | |
| 210 | # Create the directory with the same name as the binary. |
| 211 | binary_dir = re.sub("\.sym$", "", socorro_sym_file_path) |
| 212 | if not os.path.exists(binary_dir): |
| 213 | os.makedirs(binary_dir) |
| 214 | |
| 215 | # Get the hash from the header of the symbol file. |
| 216 | with open(socorro_sym_file_path, 'r') as socorro_sym_file: |
| 217 | |
| 218 | # The hash is the 4th argument of the first line. |
| 219 | sym_file_hash = socorro_sym_file.readline().split()[3] |
| 220 | |
| 221 | # Create the hash directory. |
| 222 | hash_dir = os.path.join(binary_dir, sym_file_hash) |
| 223 | if not os.path.exists(hash_dir): |
| 224 | os.makedirs(hash_dir) |
| 225 | |
| 226 | # Move the symbol file to the hash directory. |
| 227 | sym_file_name = os.path.basename(socorro_sym_file_path) |
| 228 | os.rename(socorro_sym_file_path, os.path.join(hash_dir, sym_file_name)) |
| 229 | |
| 230 | return |
| 231 | |