mapuche 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ # This workflow will upload a Python Package using Twine when a release is created
2
+ # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
3
+
4
+ # This workflow uses actions that are not certified by GitHub.
5
+ # They are provided by a third-party and are governed by
6
+ # separate terms of service, privacy policy, and support
7
+ # documentation.
8
+
9
+ name: Upload Python Package
10
+
11
+ on:
12
+ release:
13
+ types: [published]
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ deploy:
20
+
21
+ runs-on: ubuntu-latest
22
+
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+ - name: Set up Python
26
+ uses: actions/setup-python@v3
27
+ with:
28
+ python-version: '3.x'
29
+ - name: Install dependencies
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ pip install build
33
+ - name: Build package
34
+ run: python -m build
35
+ - name: Publish package
36
+ uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
37
+ with:
38
+ user: __token__
39
+ password: ${{ secrets.PYPI_API_TOKEN }}
@@ -0,0 +1,4 @@
1
+ *.egg-info
2
+ *.whl
3
+ __pycache__
4
+ build
mapuche-0.0.1/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2024 Alexey Lapshin and others
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
mapuche-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,53 @@
1
+ Metadata-Version: 2.1
2
+ Name: mapuche
3
+ Version: 0.0.1
4
+ Summary: Linker's map file browser
5
+ Author-email: Alexey Lapshin <alexeyfonlapshin@gmail.com>
6
+ License: MIT
7
+ Keywords: map,elf,linker
8
+ Classifier: Programming Language :: Python :: 3
9
+ Requires-Python: >=3.6
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: textual==0.40.0
13
+
14
+ # mapuche
15
+
16
+ Mapuche is a linker's map file browser
17
+
18
+ If you are reading this, please don't forget to give this project a star on GitHub!
19
+
20
+ > [!CAUTION]
21
+ > Mapuche is in pre-alfa development stage. Crashes or unexpected output may occur! Please create an issue if any.
22
+
23
+
24
+ ## Install
25
+
26
+ ```
27
+ pip install mapuche
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ > [!IMPORTANT]
33
+ > For now mapuche supports only map files of ELFs generated with `-ffunction-sections` and `-fdata-sections` compile options.
34
+
35
+ ```
36
+ mapuche <elf.map> [elf_for_diff.map]
37
+ ```
38
+
39
+ ## Screenshot
40
+
41
+ ![mapuche diff maps](./imgs/mapuche_diff_demo.png)
42
+
43
+ ## TODO list
44
+
45
+ - [ ] implement `--help`/`--version`
46
+ - [ ] regex filters
47
+ - [ ] columns sort
48
+ - [ ] support expand/collapse by space button
49
+ - [ ] move input section name from "name" to separate column
50
+ - [ ] cute alignment for `size`/`diff`/`delta` columns
51
+ - [ ] assembler diff viewer in popup widget
52
+ - [ ] support map files for ELFs without `-ffunction-sections`/`-fdata-sections`
53
+ - [ ] tests
@@ -0,0 +1,40 @@
1
+ # mapuche
2
+
3
+ Mapuche is a linker's map file browser
4
+
5
+ If you are reading this, please don't forget to give this project a star on GitHub!
6
+
7
+ > [!CAUTION]
8
+ > Mapuche is in pre-alfa development stage. Crashes or unexpected output may occur! Please create an issue if any.
9
+
10
+
11
+ ## Install
12
+
13
+ ```
14
+ pip install mapuche
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ > [!IMPORTANT]
20
+ > For now mapuche supports only map files of ELFs generated with `-ffunction-sections` and `-fdata-sections` compile options.
21
+
22
+ ```
23
+ mapuche <elf.map> [elf_for_diff.map]
24
+ ```
25
+
26
+ ## Screenshot
27
+
28
+ ![mapuche diff maps](./imgs/mapuche_diff_demo.png)
29
+
30
+ ## TODO list
31
+
32
+ - [ ] implement `--help`/`--version`
33
+ - [ ] regex filters
34
+ - [ ] columns sort
35
+ - [ ] support expand/collapse by space button
36
+ - [ ] move input section name from "name" to separate column
37
+ - [ ] cute alignment for `size`/`diff`/`delta` columns
38
+ - [ ] assembler diff viewer in popup widget
39
+ - [ ] support map files for ELFs without `-ffunction-sections`/`-fdata-sections`
40
+ - [ ] tests
Binary file
File without changes
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python
2
+
3
+ import sys
4
+
5
+ INFO = "*info*" # TODO remove
6
+
7
+ sections = {}
8
+ sections_diff = {}
9
+
10
+ def pretty_print_dict(d):
11
+ # print pretty json
12
+ import json
13
+ print(json.dumps(d, indent=4))
14
+
15
+ def parse_lib_and_objfile(source):
16
+ if source == None:
17
+ return (None, None)
18
+ objfile = None
19
+
20
+ lib_end = source.rfind('(')
21
+ if lib_end > 0:
22
+ objfile = source[lib_end + 1:-1]
23
+ else:
24
+ lib_end = len(source)
25
+ lib_start = source.rfind('/') + 1
26
+ lib = source[lib_start:lib_end]
27
+ return (lib, objfile)
28
+
29
+ def parse_mapfile_entry(f, line):
30
+ split = line.split()
31
+ name = split[0]
32
+ # remove the first element from split
33
+ split.pop(0)
34
+ # check if one word in line
35
+ if len(split) == 0:
36
+ # read new line and print
37
+ line = f.readline()
38
+ split = line.split()
39
+
40
+ if len(split) == 1:
41
+ return None
42
+
43
+ source = split[2] if len(split) == 3 else None
44
+
45
+ return {'name': name, 'address': split[0], 'size': int(split[1], 16), 'source': source}
46
+
47
+
48
+ def fill_all_fields_with_info(data):
49
+ size = 0
50
+ for k in data['entries']:
51
+ size += fill_all_fields_with_info(data['entries'][k])
52
+
53
+ if data['size'] == None:
54
+ data['size'] = size
55
+ return data['size']
56
+
57
+ def convert_to_textual_rows(json_data, is_diff=False, add_header=True):
58
+ rows = []
59
+ if add_header:
60
+ if is_diff:
61
+ rows += [tuple(['name', 'address', 'size', 'diff', 'delta'])]
62
+ else:
63
+ rows += [tuple(['name', 'address', 'size'])]
64
+ for k, v in json_data['entries'].items():
65
+ if is_diff:
66
+ rows.append(tuple([v['name'], v['address'], v['size'], v['diff'], v['delta']]))
67
+ else:
68
+ rows.append(tuple([v['name'], v['address'], v['size']]))
69
+ if len(v['entries']) == 0:
70
+ continue
71
+ rows += tuple([convert_to_textual_rows(v, is_diff, False)])
72
+ return rows
73
+
74
+
75
+ def add_entry_to_entries(entries, name, entry=None):
76
+ if name in entries:
77
+ return
78
+
79
+ if entry != None:
80
+ entries[name] = entry
81
+ else:
82
+ entries[name] = {'name': name, 'address': None, 'size': None, 'source': None}
83
+ entries[name]['entries'] = {}
84
+
85
+ def _parse_map_file(map_file):
86
+ # open file in argv[0] and read until string "Linker script and memory map"
87
+ # array of sections. Contains section name, address and size
88
+ sections = {}
89
+ parse = False
90
+ root_entry_name = ''
91
+ with open(map_file, 'r') as f:
92
+ while True:
93
+ line = f.readline()
94
+ if not line:
95
+ break
96
+ if not parse:
97
+ if "Linker script and memory map" in line:
98
+ parse = True
99
+ continue
100
+ if "Cross Reference Table" in line:
101
+ break
102
+
103
+ if line.startswith('.'):
104
+ is_root_entry = True
105
+ elif line.startswith(' .'):
106
+ is_root_entry = False
107
+ else:
108
+ continue
109
+
110
+ map_entry = parse_mapfile_entry(f, line)
111
+
112
+ if map_entry is None:
113
+ continue
114
+
115
+ if is_root_entry:
116
+ root_entry_name = map_entry['name']
117
+ add_entry_to_entries(sections, root_entry_name, map_entry)
118
+ else:
119
+ # append last sections element with entry
120
+ lib, obj_file = parse_lib_and_objfile(map_entry['source'])
121
+ add_entry_to_entries(sections[root_entry_name]['entries'], lib)
122
+ add_entry_to_entries(sections[root_entry_name]['entries'][lib]['entries'], obj_file)
123
+ add_entry_to_entries(sections[root_entry_name]['entries'][lib]['entries'][obj_file]['entries'], map_entry['name'], map_entry)
124
+ sections = {'name': 'total', 'size': 0, 'entries': sections}
125
+ fill_all_fields_with_info(sections)
126
+ return sections
127
+
128
+ def parse_map_file(map_file):
129
+ p = _parse_map_file(map_file)
130
+ return convert_to_textual_rows(p, False)
131
+
132
+ def get_all_entries_keys(d1, d2):
133
+ result = []
134
+ if not d1 is None:
135
+ result += d1['entries'].keys()
136
+ if not d2 is None:
137
+ result += d2['entries'].keys()
138
+ return list(dict.fromkeys(result))
139
+
140
+ def _collect_diff(diff_entries, leaf, m1, m2):
141
+ has_diff = False
142
+
143
+ diff_entries[leaf] = {'name': leaf, 'address': None, 'size': 0, 'diff': 0, 'delta': 0, 'source': None, 'entries': {}}
144
+
145
+ for k in get_all_entries_keys(m1, m2):
146
+ if m2 == None or not k in m2['entries']:
147
+ has_diff |= _collect_diff(diff_entries[leaf]['entries'], k, m1['entries'][k], None)
148
+ continue
149
+
150
+ if m1 == None or not k in m1['entries']:
151
+ has_diff |= _collect_diff(diff_entries[leaf]['entries'], k, None, m2['entries'][k])
152
+ continue
153
+
154
+ has_diff |= _collect_diff(diff_entries[leaf]['entries'], k, m1['entries'][k], m2['entries'][k])
155
+
156
+
157
+ s1 = 0 if m1 == None else m1['size']
158
+ s2 = 0 if m2 == None else m2['size']
159
+ has_diff |= s1 != s2
160
+
161
+ if not has_diff:
162
+ del diff_entries[leaf]
163
+ return has_diff
164
+
165
+ reference = m1
166
+ if m1 is None:
167
+ reference = m2
168
+
169
+ diff_entries[leaf]['name'] = reference['name']
170
+ diff_entries[leaf]['size'] = s1
171
+ diff_entries[leaf]['address'] = reference['address'] if 'address' in reference else None
172
+ diff_entries[leaf]['source'] = reference['source'] if 'source' in reference else None
173
+ diff_entries[leaf]['diff'] = s1 - s2
174
+ diff_entries[leaf]['delta'] = 100 if s2 == 0 else round((s1 - s2 ) / s2 * 100, 1)
175
+
176
+ return has_diff
177
+
178
+
179
+ def generate_diff_table(map_1, map_2):
180
+ m1 = _parse_map_file(map_1)
181
+ m2 = _parse_map_file(map_2)
182
+
183
+ _collect_diff(sections_diff, 'total', m1, m2)
184
+
185
+ return convert_to_textual_rows(sections_diff['total'], True)
186
+
187
+
188
+ if __name__ == "__main__":
189
+ if len(sys.argv) == 2:
190
+ rows_file = parse_map_file(sys.argv[1])
191
+ print(rows_file)
192
+ elif len(sys.argv) == 3:
193
+ rows_diff = generate_diff_table(sys.argv[1], sys.argv[2])
194
+ else:
195
+ print("Wrong number of arguments. Please provide one or two files.")
196
+ exit()
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env python3
2
+
3
+ from itertools import cycle
4
+ from os import sys
5
+ from textual.app import App, ComposeResult
6
+ from .tree_table import DataTable
7
+ from .map_parse import parse_map_file, generate_diff_table
8
+
9
+ class TableApp(App):
10
+ def compose(self) -> ComposeResult:
11
+ yield DataTable()
12
+
13
+ def on_mount(self) -> None:
14
+ table = self.query_one(DataTable)
15
+ table.cursor_type = next(cycle(["row"]))
16
+ table.zebra_stripes = True
17
+ if len(sys.argv) == 2:
18
+ rows = parse_map_file(sys.argv[1])
19
+ elif len(sys.argv) == 3:
20
+ rows = generate_diff_table(sys.argv[1], sys.argv[2])
21
+ table.add_columns(*rows[0])
22
+ table.add_rows(rows[1:])
23
+
24
+ def key_c(self):
25
+ table = self.query_one(DataTable)
26
+ table.cursor_type = next(cursors)
27
+
28
+
29
+ def main():
30
+ if len(sys.argv) != 2 and len(sys.argv) != 3:
31
+ print("Wrong parameters count!")
32
+ app = TableApp()
33
+ app.run()
34
+
35
+ if __name__ == "__main__":
36
+ main()