Important changes to repositories hosted on mbed.com
Mbed hosted mercurial repositories are deprecated and are due to be permanently deleted in July 2026.
To keep a copy of this software download the repository Zip archive or clone locally using Mercurial.
It is also possible to export all your personal repositories from the account settings page.
Fork of mbed-sdk-tools by
export/uvision/__init__.py@32:8ea194f6145b, 2017-01-04 (annotated)
- Committer:
- The Other Jimmy
- Date:
- Wed Jan 04 11:58:24 2017 -0600
- Revision:
- 32:8ea194f6145b
Update tools to follow mbed-os tools release 5.3.1
Who changed what in which revision?
User | Revision | Line number | New contents of line |
---|---|---|---|
The Other Jimmy |
32:8ea194f6145b | 1 | import os |
The Other Jimmy |
32:8ea194f6145b | 2 | from os.path import sep, normpath, join, exists |
The Other Jimmy |
32:8ea194f6145b | 3 | import ntpath |
The Other Jimmy |
32:8ea194f6145b | 4 | import copy |
The Other Jimmy |
32:8ea194f6145b | 5 | from collections import namedtuple |
The Other Jimmy |
32:8ea194f6145b | 6 | import shutil |
The Other Jimmy |
32:8ea194f6145b | 7 | from subprocess import Popen, PIPE |
The Other Jimmy |
32:8ea194f6145b | 8 | import re |
The Other Jimmy |
32:8ea194f6145b | 9 | |
The Other Jimmy |
32:8ea194f6145b | 10 | from tools.arm_pack_manager import Cache |
The Other Jimmy |
32:8ea194f6145b | 11 | from tools.targets import TARGET_MAP |
The Other Jimmy |
32:8ea194f6145b | 12 | from tools.export.exporters import Exporter |
The Other Jimmy |
32:8ea194f6145b | 13 | from tools.export.cmsis import DeviceCMSIS |
The Other Jimmy |
32:8ea194f6145b | 14 | |
The Other Jimmy |
32:8ea194f6145b | 15 | cache_d = False |
The Other Jimmy |
32:8ea194f6145b | 16 | |
The Other Jimmy |
32:8ea194f6145b | 17 | |
The Other Jimmy |
32:8ea194f6145b | 18 | class DeviceUvision(DeviceCMSIS): |
The Other Jimmy |
32:8ea194f6145b | 19 | """Uvision Device class, inherits CMSIS Device class |
The Other Jimmy |
32:8ea194f6145b | 20 | |
The Other Jimmy |
32:8ea194f6145b | 21 | Encapsulates information necessary for uvision project targets""" |
The Other Jimmy |
32:8ea194f6145b | 22 | def __init__(self, target): |
The Other Jimmy |
32:8ea194f6145b | 23 | DeviceCMSIS.__init__(self, target) |
The Other Jimmy |
32:8ea194f6145b | 24 | dev_format = "$$Device:{0}${1}" |
The Other Jimmy |
32:8ea194f6145b | 25 | self.svd = '' |
The Other Jimmy |
32:8ea194f6145b | 26 | if self.debug_svd: |
The Other Jimmy |
32:8ea194f6145b | 27 | self.svd = dev_format.format(self.dname, self.debug_svd) |
The Other Jimmy |
32:8ea194f6145b | 28 | self.reg_file = dev_format.format(self.dname, self.compile_header) |
The Other Jimmy |
32:8ea194f6145b | 29 | self.debug_interface = self.uv_debug() |
The Other Jimmy |
32:8ea194f6145b | 30 | self.flash_dll = self.generate_flash_dll() |
The Other Jimmy |
32:8ea194f6145b | 31 | |
The Other Jimmy |
32:8ea194f6145b | 32 | def uv_debug(self): |
The Other Jimmy |
32:8ea194f6145b | 33 | """Return a namedtuple of information about uvision debug settings""" |
The Other Jimmy |
32:8ea194f6145b | 34 | UVDebug = namedtuple('UVDebug',['bin_loc','core_flag', 'key']) |
The Other Jimmy |
32:8ea194f6145b | 35 | |
The Other Jimmy |
32:8ea194f6145b | 36 | # CortexMXn => pCMX |
The Other Jimmy |
32:8ea194f6145b | 37 | cpu = self.core.replace("Cortex-", "C") |
The Other Jimmy |
32:8ea194f6145b | 38 | cpu = cpu.replace("+", "") |
The Other Jimmy |
32:8ea194f6145b | 39 | cpu = cpu.replace("F", "") |
The Other Jimmy |
32:8ea194f6145b | 40 | cpu_flag = "p"+cpu |
The Other Jimmy |
32:8ea194f6145b | 41 | |
The Other Jimmy |
32:8ea194f6145b | 42 | # Locations found in Keil_v5/TOOLS.INI |
The Other Jimmy |
32:8ea194f6145b | 43 | debuggers = {"st-link": ('STLink\\ST-LINKIII-KEIL_SWO.dll', 'ST-LINKIII-KEIL_SWO'), |
The Other Jimmy |
32:8ea194f6145b | 44 | "j-link":('Segger\\JL2CM3.dll', 'JL2CM3'), |
The Other Jimmy |
32:8ea194f6145b | 45 | "cmsis-dap":('BIN\\CMSIS_AGDI.dll', 'CMSIS_AGDI'), |
The Other Jimmy |
32:8ea194f6145b | 46 | "nulink":('NULink\\Nu_Link.dll','Nu_Link')} |
The Other Jimmy |
32:8ea194f6145b | 47 | res = debuggers[self.debug.lower()] |
The Other Jimmy |
32:8ea194f6145b | 48 | binary = res[0] |
The Other Jimmy |
32:8ea194f6145b | 49 | key = res[1] |
The Other Jimmy |
32:8ea194f6145b | 50 | |
The Other Jimmy |
32:8ea194f6145b | 51 | return UVDebug(binary, cpu_flag, key) |
The Other Jimmy |
32:8ea194f6145b | 52 | |
The Other Jimmy |
32:8ea194f6145b | 53 | def generate_flash_dll(self): |
The Other Jimmy |
32:8ea194f6145b | 54 | '''Flash DLL string from uvision |
The Other Jimmy |
32:8ea194f6145b | 55 | S = SW/JTAG Clock ID |
The Other Jimmy |
32:8ea194f6145b | 56 | C = CPU index in JTAG chain |
The Other Jimmy |
32:8ea194f6145b | 57 | P = Access Port |
The Other Jimmy |
32:8ea194f6145b | 58 | For the Options for Target -> Debug tab -> settings -> "Flash" tab in the dialog: |
The Other Jimmy |
32:8ea194f6145b | 59 | FD = RAM Start for Flash Functions |
The Other Jimmy |
32:8ea194f6145b | 60 | FC = RAM Size for Flash Functions |
The Other Jimmy |
32:8ea194f6145b | 61 | FN = Number of Flash types |
The Other Jimmy |
32:8ea194f6145b | 62 | FF = Flash File Name (without an extension) |
The Other Jimmy |
32:8ea194f6145b | 63 | FS = Start Address of the Flash Device |
The Other Jimmy |
32:8ea194f6145b | 64 | FL = Size of the Flash Device |
The Other Jimmy |
32:8ea194f6145b | 65 | FP = Full path to the Device algorithm (RTE) |
The Other Jimmy |
32:8ea194f6145b | 66 | |
The Other Jimmy |
32:8ea194f6145b | 67 | Necessary to flash some targets. Info gathered from algorithms field of pdsc file. |
The Other Jimmy |
32:8ea194f6145b | 68 | ''' |
The Other Jimmy |
32:8ea194f6145b | 69 | fl_count = 0 |
The Other Jimmy |
32:8ea194f6145b | 70 | def get_mem_no_x(mem_str): |
The Other Jimmy |
32:8ea194f6145b | 71 | mem_reg = "\dx(\w+)" |
The Other Jimmy |
32:8ea194f6145b | 72 | m = re.search(mem_reg, mem_str) |
The Other Jimmy |
32:8ea194f6145b | 73 | return m.group(1) if m else None |
The Other Jimmy |
32:8ea194f6145b | 74 | |
The Other Jimmy |
32:8ea194f6145b | 75 | RAMS = [(get_mem_no_x(info["start"]), get_mem_no_x(info["size"])) |
The Other Jimmy |
32:8ea194f6145b | 76 | for mem, info in self.target_info["memory"].items() if "RAM" in mem] |
The Other Jimmy |
32:8ea194f6145b | 77 | format_str = "UL2CM3(-S0 -C0 -P0 -FD{ramstart}"+" -FC{ramsize} "+"-FN{num_algos} {extra_flags})" |
The Other Jimmy |
32:8ea194f6145b | 78 | ramstart = '' |
The Other Jimmy |
32:8ea194f6145b | 79 | #Default according to Keil developer |
The Other Jimmy |
32:8ea194f6145b | 80 | ramsize = '1000' |
The Other Jimmy |
32:8ea194f6145b | 81 | if len(RAMS)>=1: |
The Other Jimmy |
32:8ea194f6145b | 82 | ramstart = RAMS[0][0] |
The Other Jimmy |
32:8ea194f6145b | 83 | extra_flags = [] |
The Other Jimmy |
32:8ea194f6145b | 84 | for name, info in self.target_info["algorithm"].items(): |
The Other Jimmy |
32:8ea194f6145b | 85 | if not name or not info: |
The Other Jimmy |
32:8ea194f6145b | 86 | continue |
The Other Jimmy |
32:8ea194f6145b | 87 | if int(info["default"])==0: |
The Other Jimmy |
32:8ea194f6145b | 88 | continue |
The Other Jimmy |
32:8ea194f6145b | 89 | name_reg = "\w*/([\w_]+)\.flm" |
The Other Jimmy |
32:8ea194f6145b | 90 | m = re.search(name_reg, name.lower()) |
The Other Jimmy |
32:8ea194f6145b | 91 | fl_name = m.group(1) if m else None |
The Other Jimmy |
32:8ea194f6145b | 92 | name_flag = "-FF" + str(fl_count) + fl_name |
The Other Jimmy |
32:8ea194f6145b | 93 | |
The Other Jimmy |
32:8ea194f6145b | 94 | start, size = get_mem_no_x(info["start"]), get_mem_no_x(info["size"]) |
The Other Jimmy |
32:8ea194f6145b | 95 | rom_start_flag = "-FS"+str(fl_count)+str(start) |
The Other Jimmy |
32:8ea194f6145b | 96 | rom_size_flag = "-FL" + str(fl_count) + str(size) |
The Other Jimmy |
32:8ea194f6145b | 97 | |
The Other Jimmy |
32:8ea194f6145b | 98 | if info["ramstart"] is not None and info["ramsize"] is not None: |
The Other Jimmy |
32:8ea194f6145b | 99 | ramstart = get_mem_no_x(info["ramstart"]) |
The Other Jimmy |
32:8ea194f6145b | 100 | ramsize = get_mem_no_x(info["ramsize"]) |
The Other Jimmy |
32:8ea194f6145b | 101 | |
The Other Jimmy |
32:8ea194f6145b | 102 | path_flag = "-FP" + str(fl_count) + "($$Device:"+self.dname+"$"+name+")" |
The Other Jimmy |
32:8ea194f6145b | 103 | |
The Other Jimmy |
32:8ea194f6145b | 104 | extra_flags.extend([name_flag, rom_start_flag, rom_size_flag, path_flag]) |
The Other Jimmy |
32:8ea194f6145b | 105 | fl_count += 1 |
The Other Jimmy |
32:8ea194f6145b | 106 | |
The Other Jimmy |
32:8ea194f6145b | 107 | extra = " ".join(extra_flags) |
The Other Jimmy |
32:8ea194f6145b | 108 | return format_str.format(ramstart=ramstart, |
The Other Jimmy |
32:8ea194f6145b | 109 | ramsize=ramsize, |
The Other Jimmy |
32:8ea194f6145b | 110 | extra_flags=extra, num_algos=fl_count) |
The Other Jimmy |
32:8ea194f6145b | 111 | |
The Other Jimmy |
32:8ea194f6145b | 112 | |
The Other Jimmy |
32:8ea194f6145b | 113 | class Uvision(Exporter): |
The Other Jimmy |
32:8ea194f6145b | 114 | """Keil Uvision class |
The Other Jimmy |
32:8ea194f6145b | 115 | |
The Other Jimmy |
32:8ea194f6145b | 116 | This class encapsulates information to be contained in a Uvision |
The Other Jimmy |
32:8ea194f6145b | 117 | project file (.uvprojx). |
The Other Jimmy |
32:8ea194f6145b | 118 | The needed information can be viewed in uvision.tmpl |
The Other Jimmy |
32:8ea194f6145b | 119 | """ |
The Other Jimmy |
32:8ea194f6145b | 120 | NAME = 'uvision5' |
The Other Jimmy |
32:8ea194f6145b | 121 | TOOLCHAIN = 'ARM' |
The Other Jimmy |
32:8ea194f6145b | 122 | TARGETS = [] |
The Other Jimmy |
32:8ea194f6145b | 123 | for target, obj in TARGET_MAP.iteritems(): |
The Other Jimmy |
32:8ea194f6145b | 124 | if not ("ARM" in obj.supported_toolchains and hasattr(obj, "device_name")): |
The Other Jimmy |
32:8ea194f6145b | 125 | continue |
The Other Jimmy |
32:8ea194f6145b | 126 | if not DeviceCMSIS.check_supported(target): |
The Other Jimmy |
32:8ea194f6145b | 127 | continue |
The Other Jimmy |
32:8ea194f6145b | 128 | TARGETS.append(target) |
The Other Jimmy |
32:8ea194f6145b | 129 | #File associations within .uvprojx file |
The Other Jimmy |
32:8ea194f6145b | 130 | file_types = {'.cpp': 8, '.c': 1, '.s': 2, |
The Other Jimmy |
32:8ea194f6145b | 131 | '.obj': 3, '.o': 3, '.lib': 4, |
The Other Jimmy |
32:8ea194f6145b | 132 | '.ar': 4, '.h': 5, '.hpp': 5, '.sct': 4} |
The Other Jimmy |
32:8ea194f6145b | 133 | |
The Other Jimmy |
32:8ea194f6145b | 134 | def uv_files(self, files): |
The Other Jimmy |
32:8ea194f6145b | 135 | """An generator containing Uvision specific information about project files |
The Other Jimmy |
32:8ea194f6145b | 136 | Positional Arguments: |
The Other Jimmy |
32:8ea194f6145b | 137 | files - the location of source files |
The Other Jimmy |
32:8ea194f6145b | 138 | |
The Other Jimmy |
32:8ea194f6145b | 139 | .uvprojx XML for project file: |
The Other Jimmy |
32:8ea194f6145b | 140 | <File> |
The Other Jimmy |
32:8ea194f6145b | 141 | <FileType>{{file.type}}</FileType> |
The Other Jimmy |
32:8ea194f6145b | 142 | <FileName>{{file.name}}</FileName> |
The Other Jimmy |
32:8ea194f6145b | 143 | <FilePath>{{file.loc}}</FilePath> |
The Other Jimmy |
32:8ea194f6145b | 144 | </File> |
The Other Jimmy |
32:8ea194f6145b | 145 | """ |
The Other Jimmy |
32:8ea194f6145b | 146 | for loc in files: |
The Other Jimmy |
32:8ea194f6145b | 147 | #Encapsulates the information necessary for template entry above |
The Other Jimmy |
32:8ea194f6145b | 148 | UVFile = namedtuple('UVFile', ['type','loc','name']) |
The Other Jimmy |
32:8ea194f6145b | 149 | _, ext = os.path.splitext(loc) |
The Other Jimmy |
32:8ea194f6145b | 150 | if ext.lower() in self.file_types: |
The Other Jimmy |
32:8ea194f6145b | 151 | type = self.file_types[ext.lower()] |
The Other Jimmy |
32:8ea194f6145b | 152 | name = ntpath.basename(normpath(loc)) |
The Other Jimmy |
32:8ea194f6145b | 153 | yield UVFile(type, loc, name) |
The Other Jimmy |
32:8ea194f6145b | 154 | |
The Other Jimmy |
32:8ea194f6145b | 155 | def format_flags(self): |
The Other Jimmy |
32:8ea194f6145b | 156 | """Format toolchain flags for Uvision""" |
The Other Jimmy |
32:8ea194f6145b | 157 | flags = copy.deepcopy(self.flags) |
The Other Jimmy |
32:8ea194f6145b | 158 | asm_flag_string = '--cpreproc --cpreproc_opts=-D__ASSERT_MSG,' + \ |
The Other Jimmy |
32:8ea194f6145b | 159 | ",".join(flags['asm_flags']) |
The Other Jimmy |
32:8ea194f6145b | 160 | # asm flags only, common are not valid within uvision project, |
The Other Jimmy |
32:8ea194f6145b | 161 | # they are armcc specific |
The Other Jimmy |
32:8ea194f6145b | 162 | flags['asm_flags'] = asm_flag_string |
The Other Jimmy |
32:8ea194f6145b | 163 | # cxx flags included, as uvision have them all in one tab |
The Other Jimmy |
32:8ea194f6145b | 164 | flags['c_flags'] = list(set(['-D__ASSERT_MSG'] |
The Other Jimmy |
32:8ea194f6145b | 165 | + flags['common_flags'] |
The Other Jimmy |
32:8ea194f6145b | 166 | + flags['c_flags'] |
The Other Jimmy |
32:8ea194f6145b | 167 | + flags['cxx_flags'])) |
The Other Jimmy |
32:8ea194f6145b | 168 | # not compatible with c99 flag set in the template |
The Other Jimmy |
32:8ea194f6145b | 169 | try: flags['c_flags'].remove("--c99") |
The Other Jimmy |
32:8ea194f6145b | 170 | except ValueError: pass |
The Other Jimmy |
32:8ea194f6145b | 171 | # cpp is not required as it's implicit for cpp files |
The Other Jimmy |
32:8ea194f6145b | 172 | try: flags['c_flags'].remove("--cpp") |
The Other Jimmy |
32:8ea194f6145b | 173 | except ValueError: pass |
The Other Jimmy |
32:8ea194f6145b | 174 | # we want no-vla for only cxx, but it's also applied for C in IDE, |
The Other Jimmy |
32:8ea194f6145b | 175 | # thus we remove it |
The Other Jimmy |
32:8ea194f6145b | 176 | try: flags['c_flags'].remove("--no_vla") |
The Other Jimmy |
32:8ea194f6145b | 177 | except ValueError: pass |
The Other Jimmy |
32:8ea194f6145b | 178 | flags['c_flags'] =" ".join(flags['c_flags']) |
The Other Jimmy |
32:8ea194f6145b | 179 | return flags |
The Other Jimmy |
32:8ea194f6145b | 180 | |
The Other Jimmy |
32:8ea194f6145b | 181 | def format_src(self, srcs): |
The Other Jimmy |
32:8ea194f6145b | 182 | """Make sources into the named tuple for use in the template""" |
The Other Jimmy |
32:8ea194f6145b | 183 | grouped = self.group_project_files(srcs) |
The Other Jimmy |
32:8ea194f6145b | 184 | for group, files in grouped.items(): |
The Other Jimmy |
32:8ea194f6145b | 185 | grouped[group] = self.uv_files(files) |
The Other Jimmy |
32:8ea194f6145b | 186 | return grouped |
The Other Jimmy |
32:8ea194f6145b | 187 | |
The Other Jimmy |
32:8ea194f6145b | 188 | def generate(self): |
The Other Jimmy |
32:8ea194f6145b | 189 | """Generate the .uvproj file""" |
The Other Jimmy |
32:8ea194f6145b | 190 | cache = Cache(True, False) |
The Other Jimmy |
32:8ea194f6145b | 191 | if cache_d: |
The Other Jimmy |
32:8ea194f6145b | 192 | cache.cache_descriptors() |
The Other Jimmy |
32:8ea194f6145b | 193 | |
The Other Jimmy |
32:8ea194f6145b | 194 | srcs = self.resources.headers + self.resources.s_sources + \ |
The Other Jimmy |
32:8ea194f6145b | 195 | self.resources.c_sources + self.resources.cpp_sources + \ |
The Other Jimmy |
32:8ea194f6145b | 196 | self.resources.objects + self.resources.libraries |
The Other Jimmy |
32:8ea194f6145b | 197 | ctx = { |
The Other Jimmy |
32:8ea194f6145b | 198 | 'name': self.project_name, |
The Other Jimmy |
32:8ea194f6145b | 199 | # project_files => dict of generators - file group to generator of |
The Other Jimmy |
32:8ea194f6145b | 200 | # UVFile tuples defined above |
The Other Jimmy |
32:8ea194f6145b | 201 | 'project_files': self.format_src(srcs), |
The Other Jimmy |
32:8ea194f6145b | 202 | 'linker_script':self.resources.linker_script, |
The Other Jimmy |
32:8ea194f6145b | 203 | 'include_paths': '; '.join(self.resources.inc_dirs).encode('utf-8'), |
The Other Jimmy |
32:8ea194f6145b | 204 | 'device': DeviceUvision(self.target), |
The Other Jimmy |
32:8ea194f6145b | 205 | } |
The Other Jimmy |
32:8ea194f6145b | 206 | # Turn on FPU optimizations if the core has an FPU |
The Other Jimmy |
32:8ea194f6145b | 207 | ctx['fpu_setting'] = 1 if 'f' not in ctx['device'].core.lower() \ |
The Other Jimmy |
32:8ea194f6145b | 208 | or 'd' in ctx['device'].core.lower() else 2 |
The Other Jimmy |
32:8ea194f6145b | 209 | ctx.update(self.format_flags()) |
The Other Jimmy |
32:8ea194f6145b | 210 | self.gen_file('uvision/uvision.tmpl', ctx, self.project_name+".uvprojx") |
The Other Jimmy |
32:8ea194f6145b | 211 | self.gen_file('uvision/uvision_debug.tmpl', ctx, self.project_name + ".uvoptx") |
The Other Jimmy |
32:8ea194f6145b | 212 | |
The Other Jimmy |
32:8ea194f6145b | 213 | @staticmethod |
The Other Jimmy |
32:8ea194f6145b | 214 | def build(project_name, log_name='build_log.txt', cleanup=True): |
The Other Jimmy |
32:8ea194f6145b | 215 | """ Build Uvision project """ |
The Other Jimmy |
32:8ea194f6145b | 216 | # > UV4 -r -j0 -o [log_name] [project_name].uvprojx |
The Other Jimmy |
32:8ea194f6145b | 217 | proj_file = project_name + ".uvprojx" |
The Other Jimmy |
32:8ea194f6145b | 218 | cmd = ['UV4', '-r', '-j0', '-o', log_name, proj_file] |
The Other Jimmy |
32:8ea194f6145b | 219 | |
The Other Jimmy |
32:8ea194f6145b | 220 | # Build the project |
The Other Jimmy |
32:8ea194f6145b | 221 | p = Popen(cmd, stdout=PIPE, stderr=PIPE) |
The Other Jimmy |
32:8ea194f6145b | 222 | out, err = p.communicate() |
The Other Jimmy |
32:8ea194f6145b | 223 | ret_code = p.returncode |
The Other Jimmy |
32:8ea194f6145b | 224 | |
The Other Jimmy |
32:8ea194f6145b | 225 | # Print the log file to stdout |
The Other Jimmy |
32:8ea194f6145b | 226 | with open(log_name, 'r') as f: |
The Other Jimmy |
32:8ea194f6145b | 227 | print f.read() |
The Other Jimmy |
32:8ea194f6145b | 228 | |
The Other Jimmy |
32:8ea194f6145b | 229 | # Cleanup the exported and built files |
The Other Jimmy |
32:8ea194f6145b | 230 | if cleanup: |
The Other Jimmy |
32:8ea194f6145b | 231 | os.remove(log_name) |
The Other Jimmy |
32:8ea194f6145b | 232 | os.remove(project_name+".uvprojx") |
The Other Jimmy |
32:8ea194f6145b | 233 | os.remove(project_name+".uvoptx") |
The Other Jimmy |
32:8ea194f6145b | 234 | # legacy .build directory cleaned if exists |
The Other Jimmy |
32:8ea194f6145b | 235 | if exists('.build'): |
The Other Jimmy |
32:8ea194f6145b | 236 | shutil.rmtree('.build') |
The Other Jimmy |
32:8ea194f6145b | 237 | if exists('BUILD'): |
The Other Jimmy |
32:8ea194f6145b | 238 | shutil.rmtree('BUILD') |
The Other Jimmy |
32:8ea194f6145b | 239 | |
The Other Jimmy |
32:8ea194f6145b | 240 | # Returns 0 upon success, 1 upon a warning, and neither upon an error |
The Other Jimmy |
32:8ea194f6145b | 241 | if ret_code != 0 and ret_code != 1: |
The Other Jimmy |
32:8ea194f6145b | 242 | # Seems like something went wrong. |
The Other Jimmy |
32:8ea194f6145b | 243 | return -1 |
The Other Jimmy |
32:8ea194f6145b | 244 | else: |
The Other Jimmy |
32:8ea194f6145b | 245 | return 0 |