aboutsummaryrefslogtreecommitdiffstats
path: root/prototypes/connections/package_native.py
blob: 7ee9c4f28319b2d4b08d669fe91a60a60262dc8c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#!/usr/bin/env python3
"""Bundle the selected Linux document app without folding hosts into the core."""
import argparse
import gzip
import hashlib
import io
import json
from pathlib import Path
import platform
import re
import subprocess
import tarfile
import tempfile

HERE = Path(__file__).resolve().parent
NEEDED = re.compile(r'\(NEEDED\).*Shared library: \[([^]]+)\]')
ROOT_NAME = 'pure-document'
PAGED_STORAGE = """# Selected page storage format

This preview uses the PUREP001 16 KiB-page store. It cannot open PUREB001
flat-store objects. Use a new --store directory; this is a research format,
not a migration or a default storage backend. Small objects currently pay a
full-page write cost. See docs/research/033-paged-document-storage.md in the
source repository for measured boundaries and limitations.
"""
HYBRID_STORAGE = """# Selected inline/page storage experiment

Objects up to 16 KiB use the existing PUREB001 file format. Larger objects
use PUREP001 page roots and append-only data generations. The current flat
binary cannot read PUREP001 objects, so use a new --store directory for this
preview. This is a selected research build, not the default backend or a
bidirectional migration path. It requires a Linux host with 16 KiB OS pages.
See docs/research/034-hybrid-document-storage.md in the source repository.
"""

RUN = """# Pure document preview — Linux native build

This archive includes the CLI and editable editor.pure program. HTTP, TLS peer,
and GTK hosts are included only when listed in manifest.json. Shared system libraries are not bundled;
manifest.json records every binary's direct dependencies.

From this directory, try a local edit:

    printf 'hello Pure\\n' > /tmp/pure-insert.txt
    bin/pure-edit --program share/editor.pure --store /tmp/pure-preview-store \\
      edit note 0 0 0 /tmp/pure-insert.txt
    bin/pure-edit --program share/editor.pure --store /tmp/pure-preview-store show note

When selected, the same program and store can be opened with the GTK window:

    bin/pure-edit-window --program share/editor.pure \\
      --store /tmp/pure-preview-store note

When selected, the source-loaded terminal editor uses the same program and
store without GTK. Edit share/editor.pure, then start a new TUI session to
load the changed definition:

    bin/pure-edit-tui --program share/editor.pure \\
      --store /tmp/pure-preview-store note

When selected, the TLS peer needs your own CA and server/client certificates. The server
certificate needs a DNS SAN matching SERVER_DNS; the client certificate needs
a DNS SAN matching CLIENT_DNS. This example binds only to loopback:

    bin/pure-document-peer serve 127.0.0.1 9443 share/editor.pure \\
      /tmp/pure-preview-store CA.pem SERVER.pem SERVER.key CLIENT_DNS
    bin/pure-document-peer call 127.0.0.1 9443 SERVER_DNS share/editor.pure \\
      CA.pem CLIENT.pem CLIENT.key show note

If manifest.json lists pure-document-server instead, run:

    bin/pure-document-server serve 127.0.0.1 9443 share/editor.pure \\
      /tmp/pure-preview-store CA.pem SERVER.pem SERVER.key CLIENT_DNS

That selection omits the native `call` command; connect with an authenticated
external client using the pure-doc/1 binary protocol.

When selected, the HTTP host is a plaintext compatibility adapter; bind it only to loopback.
This preview was checked on Linux AArch64. It is not a static or macOS package.
"""
RUN_FIXED = """# Pure fixed document runner — Linux AArch64

This artifact runs one build-time prepared editor.pure definition. It has no
source parser, compiler, GTK or TLS host. Editing a .pure file does not
change this binary; rebuild it with `make fixed-editor` in the source tree.

    bin/pure-edit-fixed --store STORE edit KEY 0 0 0 INSERT_FILE
    bin/pure-edit-fixed --store STORE show KEY

When manifest.json lists pure-connections-fixed, it serves the same definition
and store through a loopback HTTP compatibility boundary:

    bin/pure-connections-fixed --store STORE --port 8767

When manifest.json lists pure-edit-tui-fixed, it opens a native terminal
editor for the same stored document. Ctrl+S saves, Ctrl+O enters another key,
Ctrl+L browses stored keys (type a prefix to jump), Ctrl+R reloads, and
Ctrl+Q quits:

    bin/pure-edit-tui-fixed --store STORE note

The external storage files use the default PUREB001 format. The ELF binary
needs the host's libc and is not a static or macOS package.
"""


def dependencies(path):
    header = subprocess.check_output(['readelf', '-h', str(path)], text=True)
    if not re.search(r'Machine:\s+AArch64\s*$', header, re.MULTILINE):
        raise ValueError(f'{path} is not an AArch64 ELF')
    output = subprocess.check_output(['readelf', '-d', str(path)], text=True)
    return sorted(NEEDED.findall(output))


def reviewed_budget(build, name, selected=()):
    command = ['python3', str(HERE / 'check_profile.py'), '--build', str(build),
               '--budget', str(HERE / name)]
    for binary in selected:
        command.extend(['--only', binary])
    result = subprocess.run(command, capture_output=True, text=True)
    if result.returncode:
        raise ValueError(f'{name} failed: {result.stdout or result.stderr}')
    checked = json.loads(result.stdout)
    return {'profile': checked['profile'], 'budget_sha256': checked['budget_sha256'],
            'checked_binaries': sorted(checked['binaries'])}


def reviewed_memory(build, fixed=False):
    result = subprocess.run(['python3', str(HERE / 'check_micro_memory.py'),
                             '--build', str(build), *(['--fixed'] if fixed else [])],
                            capture_output=True, text=True)
    if result.returncode:
        raise ValueError(f'memory-budget.json failed: {result.stdout or result.stderr}')
    checked = json.loads(result.stdout)
    return {'profile': checked['profile'], 'budget_sha256': checked['budget_sha256']}


def reviewed_tui_memory(build, source=False):
    result = subprocess.run(['python3', str(HERE / 'check_tui.py'), '--build', str(build),
                             *(['--source'] if source else [])],
                            capture_output=True, text=True)
    if result.returncode:
        raise ValueError(f'terminal editor check failed: {result.stdout or result.stderr}')
    checked = json.loads(result.stdout)
    return {'profile': checked['profile'],
            'one_mib_peak_rss_limit_kib': checked['one_mib_peak_rss_limit_kib'],
            'binary_sha256': checked['binary_sha256']}


def add_bytes(archive, name, content, mode):
    info = tarfile.TarInfo(ROOT_NAME + '/' + name)
    info.size = len(content)
    info.mode = mode
    info.mtime = 0
    info.uid = info.gid = 0
    info.uname = info.gname = ''
    archive.addfile(info, io.BytesIO(content))


def without_debug(source, compact=False):
    # The release build retains symbols for local debugging; the runnable
    # fixed package needs neither DWARF nor unused symbols. Never mutate the
    # build used by size checks or source/authoring package symbols.
    with tempfile.TemporaryDirectory(prefix='pure-package-strip-') as directory:
        destination = Path(directory) / source.name
        destination.write_bytes(source.read_bytes())
        subprocess.run(['strip', '--strip-unneeded' if compact else '--strip-debug',
                        str(destination)], check=True)
        if dependencies(destination) != dependencies(source):
            raise ValueError(f'{source}: stripping changed direct dependencies')
        return destination.read_bytes()


def verify_storage(binary, mode):
    with tempfile.TemporaryDirectory(prefix='pure-package-store-probe-') as directory:
        root = Path(directory)
        payload = root / 'insert'
        payload.write_bytes(b'X')
        result = subprocess.run([str(binary), '--program', str(HERE / 'examples/editor.pure'),
                                 '--store', str(root / 'store'), 'edit', 'probe', '0', '0', '0',
                                 str(payload)], capture_output=True, timeout=15)
        expected = b'PUREB001' if mode == 'hybrid' else b'PUREP001'
        if result.returncode or (root / 'store/obj-probe').read_bytes()[:8] != expected:
            raise ValueError(f'{binary} is not a working {mode} CLI: {result.stderr[-300:]}')
        if mode == 'hybrid':
            payload.write_bytes(b'X' * (16 * 1024 + 1))
            result = subprocess.run([str(binary), '--program', str(HERE / 'examples/editor.pure'),
                                     '--store', str(root / 'store'), 'edit', 'large', '0', '0', '0',
                                     str(payload)], capture_output=True, timeout=15)
            if result.returncode or (root / 'store/obj-large').read_bytes()[:8] != b'PUREP001':
                raise ValueError(f'{binary} does not select PUREP001 for large objects')


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--build', type=Path, default=HERE / 'build')
    parser.add_argument('--output', type=Path, required=True)
    parser.add_argument('--with-http', action='store_true')
    parser.add_argument('--with-tui', action='store_true')
    peer = parser.add_mutually_exclusive_group()
    peer.add_argument('--with-peer', action='store_true',
                      help='include the combined native TLS client/server')
    peer.add_argument('--with-peer-server', action='store_true',
                      help='include only the authenticated TLS server')
    parser.add_argument('--with-window', action='store_true')
    parser.add_argument('--fixed', action='store_true',
                        help='package only the parser-free build-time prepared editor')
    storage = parser.add_mutually_exclusive_group()
    storage.add_argument('--paged-storage', action='store_true',
                         help='package the historical PUREP001-only storage backend')
    storage.add_argument('--hybrid-storage', action='store_true',
                         help='package the selected PUREB001/PUREP001 storage experiment')
    args = parser.parse_args()
    if platform.system() != 'Linux' or platform.machine() != 'aarch64':
        parser.error('this reviewed package profile is Linux AArch64 only')
    if args.output.exists():
        parser.error('output already exists')
    if args.fixed and (args.with_peer or args.with_peer_server or args.with_window or
                       args.paged_storage or args.hybrid_storage):
        parser.error('--fixed selects only the flat-store CLI and optional HTTP/TUI hosts')
    if args.with_tui and args.paged_storage:
        parser.error('--with-tui supports the default or selected hybrid store')
    if (args.with_peer or args.with_peer_server) and (args.paged_storage or args.hybrid_storage):
        parser.error('the document TLS peer currently uses the default flat store')
    if args.fixed:
        built = subprocess.run(['make', '-C', str(HERE), f'BUILD={args.build.resolve()}',
                                'FIXED_PROGRAM=examples/editor.pure', 'fixed-editor',
                                *(['fixed-server'] if args.with_http else []),
                                *(['fixed-tui'] if args.with_tui else [])],
                               capture_output=True, text=True)
        if built.returncode:
            parser.error(f'could not prepare the fixed editor: {built.stderr[-500:]}')
    elif args.with_tui:
        built = subprocess.run(['make', '-C', str(HERE), f'BUILD={args.build.resolve()}',
                                *(['STORE=store-paged.c'] if args.hybrid_storage else []),
                                'editor', 'source-tui'], capture_output=True, text=True)
        if built.returncode:
            parser.error(f'could not prepare the source editor: {built.stderr[-500:]}')
    if args.with_peer_server:
        built = subprocess.run(['make', '-C', str(HERE), f'BUILD={args.build.resolve()}',
                                'editor', 'document-server'], capture_output=True, text=True)
        if built.returncode:
            parser.error(f'could not prepare the server-only peer: {built.stderr[-500:]}')
    if args.paged_storage or args.hybrid_storage:
        verify_storage(args.build / 'pure-edit', 'hybrid' if args.hybrid_storage else 'paged')
    helper = args.build.resolve() / 'measure-peak-rss'
    built = subprocess.run(['make', '-C', str(HERE), f'BUILD={args.build.resolve()}',
                            str(helper)], capture_output=True, text=True)
    if built.returncode:
        parser.error(f'could not prepare the RSS helper: {built.stderr[-500:]}')
    binaries = ['pure-edit-fixed' if args.fixed else 'pure-edit']
    if args.with_http:
        binaries.append('pure-connections-fixed' if args.fixed else 'pure-connections')
    if args.with_tui:
        binaries.append('pure-edit-tui-fixed' if args.fixed else 'pure-edit-tui')
    if args.with_peer:
        binaries.append('pure-document-peer')
    if args.with_peer_server:
        binaries.append('pure-document-server')
    if args.with_window:
        binaries.append('pure-edit-window')
    entries = {}
    manifest_files = {}
    for binary in binaries:
        source = args.build / binary
        content = without_debug(source, args.fixed)
        if content[:4] != b'\x7fELF':
            parser.error(f'{source} is not an ELF binary')
        name = 'bin/' + binary
        entries[name] = (content, 0o755)
        manifest_files[name] = {'sha256': hashlib.sha256(content).hexdigest(),
                                'bytes': len(content), 'needed': dependencies(source)}
    program = (HERE / 'examples/editor.pure').read_bytes()
    if not args.fixed:
        entries['share/editor.pure'] = (program, 0o644)
        manifest_files['share/editor.pure'] = {'sha256': hashlib.sha256(program).hexdigest(),
                                               'bytes': len(program)}
    entries['RUN.md'] = ((RUN_FIXED if args.fixed else RUN).encode(), 0o644)
    manifest_files['RUN.md'] = {'sha256': hashlib.sha256(entries['RUN.md'][0]).hexdigest(),
                                'bytes': len(entries['RUN.md'][0])}
    if args.paged_storage or args.hybrid_storage:
        entries['STORAGE.md'] = ((HYBRID_STORAGE if args.hybrid_storage else PAGED_STORAGE).encode(), 0o644)
        manifest_files['STORAGE.md'] = {'sha256': hashlib.sha256(entries['STORAGE.md'][0]).hexdigest(),
                                        'bytes': len(entries['STORAGE.md'][0])}
    budgets = [reviewed_budget(args.build,
                               'fixed-budget.json' if args.fixed else
                               'hybrid-budget.json' if args.hybrid_storage else
                               'paged-budget.json' if args.paged_storage else 'profile-budget.json',
                               ['pure-edit-fixed'] +
                               (['pure-connections-fixed'] if args.with_http else []) if args.fixed else
                               ['pure-edit'] + (['pure-connections'] if args.with_http else []))]
    if args.with_peer:
        budgets.append(reviewed_budget(args.build, 'peer-budget.json'))
    if args.with_peer_server:
        budgets.append(reviewed_budget(args.build, 'peer-server-budget.json'))
    if args.with_tui:
        budgets.append(reviewed_budget(args.build,
                                       'tui-budget.json' if args.fixed else
                                       'hybrid-tui-budget.json' if args.hybrid_storage else
                                       'source-tui-budget.json'))
    if args.with_window:
        budgets.append(reviewed_budget(args.build, 'window-budget.json'))
    manifest = {'format': 'pure-document-preview-1', 'platform': 'linux-aarch64',
                'selected_hosts': binaries, 'shared_libraries_bundled': False,
                'reviewed_budgets': budgets, 'reviewed_memory': reviewed_memory(args.build, args.fixed),
                'files': manifest_files}
    if args.fixed:
        manifest['program_mode'] = 'build-time-prepared'
        manifest['source_sha256'] = hashlib.sha256(program).hexdigest()
    if args.with_tui:
        manifest['reviewed_tui_memory'] = reviewed_tui_memory(args.build, not args.fixed)
    if args.with_window:
        manifest['features'] = ['native-document-key-switch']
    if args.paged_storage:
        manifest['storage_profile'] = 'PUREP001-16k-page-research'
    if args.hybrid_storage:
        manifest['storage_profile'] = 'PUREB001+PUREP001-hybrid-16k-research'
    maximum = None
    package_profile = None
    if args.fixed:
        budget_path = HERE / 'fixed-package-budget.json'
        package_budget = json.loads(budget_path.read_text())
        package_profile = 'cli' + ('_http' if args.with_http else '') + ('_tui' if args.with_tui else '')
        maximum = package_budget['max_archive_bytes'][package_profile]
        manifest['reviewed_package_budget'] = {
            'profile': package_budget['profile'], 'selection': package_profile,
            'max_archive_bytes': maximum,
            'budget_sha256': hashlib.sha256(budget_path.read_bytes()).hexdigest()}
    elif (args.with_peer or args.with_peer_server) and not (
            args.with_http or args.with_tui or args.with_window or
            args.paged_storage or args.hybrid_storage):
        budget_path = HERE / 'peer-package-budget.json'
        package_budget = json.loads(budget_path.read_text())
        package_profile = 'server_only' if args.with_peer_server else 'combined'
        maximum = package_budget['max_archive_bytes'][package_profile]
        manifest['reviewed_package_budget'] = {
            'profile': package_budget['profile'], 'selection': package_profile,
            'max_archive_bytes': maximum,
            'budget_sha256': hashlib.sha256(budget_path.read_bytes()).hexdigest()}
    entries['manifest.json'] = ((json.dumps(manifest, indent=2, sort_keys=True) + '\n').encode(), 0o644)
    stream = io.BytesIO()
    with gzip.GzipFile(filename='', mode='wb', fileobj=stream, mtime=0) as compressed:
        with tarfile.open(fileobj=compressed, mode='w') as archive:
            for name in sorted(entries):
                content, mode = entries[name]
                add_bytes(archive, name, content, mode)
    packed = stream.getvalue()
    if maximum is not None and len(packed) > maximum:
        parser.error(f'{package_profile} archive grew to {len(packed)} bytes '
                     f'(reviewed maximum {maximum}); no package was published')
    args.output.parent.mkdir(parents=True, exist_ok=True)
    with args.output.open('xb') as output:
        output.write(packed)
    print(json.dumps({'archive': str(args.output.resolve()),
                      'sha256': hashlib.sha256(packed).hexdigest(),
                      'bytes': len(packed),
                      'selected_hosts': binaries}, sort_keys=True))


if __name__ == '__main__':
    main()