diff --git a/.github/ISSUE_TEMPLATE/01_bug.yml b/.github/ISSUE_TEMPLATE/01_bug.yml
index ab40754fc9..92061f6418 100644
--- a/.github/ISSUE_TEMPLATE/01_bug.yml
+++ b/.github/ISSUE_TEMPLATE/01_bug.yml
@@ -41,8 +41,8 @@ body:
attributes:
value: >
**Note**: Assuming you have network connectivity,
- you can easily post the installation log using the following command:
- `curl -F'file=@/var/log/archinstall/install.log' https://0x0.st`
+ you can easily upload the installation log and get a shareable URL by running:
+ `archinstall share-log`
- type: textarea
id: freeform
diff --git a/README.md b/README.md
index 95d1570252..4e6847f406 100644
--- a/README.md
+++ b/README.md
@@ -101,9 +101,9 @@ If you come across any issues, kindly submit your issue here on GitHub or post y
When submitting an issue, please:
* Provide the stacktrace of the output if applicable
* Attach the `/var/log/archinstall/install.log` to the issue ticket. This helps us help you!
- * To extract the log from the ISO image, one way is to use
+ * To upload the log from the ISO image and get a shareable URL, run
```shell
- curl -F'file=@/var/log/archinstall/install.log' https://0x0.st
+ archinstall share-log
```
diff --git a/archinstall/lib/args.py b/archinstall/lib/args.py
index 8d78f7e6d8..d8aaaa83fe 100644
--- a/archinstall/lib/args.py
+++ b/archinstall/lib/args.py
@@ -431,7 +431,6 @@ def _define_arguments(self) -> ArgumentParser:
default=False,
help='Enabled verbose options',
)
-
return parser
def _parse_args(self) -> Arguments:
diff --git a/archinstall/lib/output.py b/archinstall/lib/output.py
index bc4bb11399..3a972dfb08 100644
--- a/archinstall/lib/output.py
+++ b/archinstall/lib/output.py
@@ -1,6 +1,8 @@
import logging
import os
import sys
+import urllib.error
+import urllib.request
from collections.abc import Callable
from dataclasses import asdict, is_dataclass
from datetime import UTC, datetime
@@ -333,3 +335,51 @@ def log(
if level != logging.DEBUG:
print(text)
+
+
+def share_install_log(
+ paste_url: str = 'https://paste.rs',
+ max_size: int = 10 * 1024 * 1024,
+ confirm: Callable[[str], bool] = lambda _: True,
+) -> int:
+ log_path = logger.path
+
+ if not log_path.exists():
+ info(f'Log file not found: {log_path}')
+ return 1
+
+ size = log_path.stat().st_size
+ if size == 0:
+ info(f'Log file is empty: {log_path}')
+ return 1
+
+ if size > max_size:
+ info(f'Log file exceeds {max_size} bytes, uploading last {max_size} bytes')
+ content = log_path.read_bytes()[-max_size:]
+ else:
+ content = log_path.read_bytes()
+
+ header = f'About to upload {log_path} ({len(content)} bytes) to {paste_url}\n\n'
+ header += 'The log may contain hostname, mirror URLs, package list and partition layout.\n'
+ header += 'The uploaded paste is public.\n\n'
+ header += 'Continue?'
+
+ if not confirm(header):
+ info('Cancelled.')
+ return 1
+
+ try:
+ req = urllib.request.Request(paste_url, data=content)
+ with urllib.request.urlopen(req) as response:
+ url = response.read().decode().strip()
+ except urllib.error.URLError as e:
+ info(f'Upload failed: {e}')
+ return 1
+
+ if not url.startswith('http'):
+ info(f'Unexpected response from {paste_url}: {url[:200]!r}')
+ return 1
+
+ # raw print so the URL is pipe-friendly (no ANSI colors, no log prefix)
+ print(url)
+ return 0
diff --git a/archinstall/main.py b/archinstall/main.py
index 505652a4f7..9b91681b00 100644
--- a/archinstall/main.py
+++ b/archinstall/main.py
@@ -11,14 +11,16 @@
from archinstall.lib.args import ArchConfigHandler
from archinstall.lib.disk.utils import disk_layouts
from archinstall.lib.hardware import SysInfo
+from archinstall.lib.menu.helpers import Confirmation
from archinstall.lib.network.wifi_handler import WifiHandler
from archinstall.lib.networking import ping
-from archinstall.lib.output import debug, error, info, warn
+from archinstall.lib.output import debug, error, info, share_install_log, warn
from archinstall.lib.packages.util import check_version_upgrade
from archinstall.lib.pacman.pacman import Pacman
from archinstall.lib.translationhandler import tr, translation_handler
from archinstall.lib.utils.util import running_from_iso
from archinstall.tui.components import tui
+from archinstall.tui.menu_item import MenuItemGroup
def _log_sys_info() -> None:
@@ -73,12 +75,28 @@ def _list_scripts() -> str:
return '\n'.join(lines)
+def _tui_confirm(header: str) -> bool:
+ async def _ask() -> bool:
+ result = await Confirmation(
+ group=MenuItemGroup.yes_no(),
+ header=header,
+ allow_skip=False,
+ preset=False,
+ ).show()
+ return result.get_value()
+
+ return tui.run(_ask)
+
+
def run() -> int:
"""
This can either be run as the compiled and installed application: python setup.py install
OR straight as a module: python -m archinstall
In any case we will be attempting to load the provided script to be run from the scripts/ folder
"""
+ if 'share-log' in sys.argv:
+ return share_install_log(confirm=_tui_confirm)
+
arch_config_handler = ArchConfigHandler()
if '--help' in sys.argv or '-h' in sys.argv:
@@ -141,8 +159,8 @@ def _error_message(exc: Exception) -> None:
Archinstall experienced the above error. If you think this is a bug, please report it to
https://github.com/archlinux/archinstall and include the log file "/var/log/archinstall/install.log".
- Hint: To extract the log from a live ISO
- curl -F 'file=@/var/log/archinstall/install.log' https://0x0.st
+ Hint: To upload the log and get a shareable URL, run
+ archinstall share-log
"""
)
warn(text)
diff --git a/docs/help/report_bug.rst b/docs/help/report_bug.rst
index bacaeb4cc2..b11027ab3e 100644
--- a/docs/help/report_bug.rst
+++ b/docs/help/report_bug.rst
@@ -15,7 +15,7 @@ When submitting a help ticket, please include the :code:`/var/log/archinstall/in
It can be found both on the live ISO but also in the installed filesystem if the base packages were strapped in.
.. tip::
- | An easy way to submit logs is ``curl -F 'file=@/var/log/archinstall/install.log' https://0x0.st``.
+ | An easy way to submit logs is ``archinstall share-log``, which uploads ``install.log`` to paste.rs and prints a shareable URL.
| Use caution when submitting other log files, but ``archinstall`` pledges to keep ``install.log`` safe for posting publicly!
There are additional log files under ``/var/log/archinstall/`` that can be useful:
diff --git a/tests/test_share_log.py b/tests/test_share_log.py
new file mode 100644
index 0000000000..c1c3ca84d7
--- /dev/null
+++ b/tests/test_share_log.py
@@ -0,0 +1,94 @@
+# pylint: disable=redefined-outer-name
+import urllib.error
+from io import BytesIO
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+from archinstall.lib.output import share_install_log
+
+
+@pytest.fixture()
+def log_file(tmp_path: Path) -> Path:
+ log_dir = tmp_path / 'archinstall'
+ log_dir.mkdir()
+ return log_dir / 'install.log'
+
+
+def _fake_logger(log_file: Path) -> MagicMock:
+ mock = MagicMock()
+ mock.path = log_file
+ return mock
+
+
+def test_file_not_found(tmp_path: Path) -> None:
+ missing = tmp_path / 'no-such' / 'install.log'
+ with patch('archinstall.lib.output.logger', _fake_logger(missing)):
+ assert share_install_log() == 1
+
+
+def test_empty_file(log_file: Path) -> None:
+ log_file.write_bytes(b'')
+ with patch('archinstall.lib.output.logger', _fake_logger(log_file)):
+ assert share_install_log() == 1
+
+
+def test_user_cancels(log_file: Path) -> None:
+ log_file.write_text('some log content')
+ with patch('archinstall.lib.output.logger', _fake_logger(log_file)):
+ assert share_install_log(confirm=lambda _: False) == 1
+
+
+def test_successful_upload(log_file: Path) -> None:
+ log_file.write_text('some log content')
+ fake_response = BytesIO(b'https://paste.rs/abc.def')
+
+ with (
+ patch('archinstall.lib.output.logger', _fake_logger(log_file)),
+ patch('urllib.request.urlopen', return_value=fake_response) as mock_open,
+ ):
+ result = share_install_log()
+
+ assert result == 0
+ req = mock_open.call_args[0][0]
+ assert req.data == b'some log content'
+
+
+def test_truncation(log_file: Path) -> None:
+ max_size = 100
+ content = b'A' * 50 + b'B' * 80
+ log_file.write_bytes(content)
+ fake_response = BytesIO(b'https://paste.rs/abc.def')
+
+ with (
+ patch('archinstall.lib.output.logger', _fake_logger(log_file)),
+ patch('urllib.request.urlopen', return_value=fake_response) as mock_open,
+ ):
+ result = share_install_log(max_size=max_size)
+
+ assert result == 0
+ req = mock_open.call_args[0][0]
+ assert len(req.data) == max_size
+ assert req.data == content[-max_size:]
+
+
+def test_network_error(log_file: Path) -> None:
+ log_file.write_text('some log content')
+
+ with (
+ patch('archinstall.lib.output.logger', _fake_logger(log_file)),
+ patch('urllib.request.urlopen', side_effect=urllib.error.URLError('no network')),
+ ):
+ assert share_install_log() == 1
+
+
+def test_unexpected_response(log_file: Path) -> None:
+ log_file.write_text('some log content')
+ fake_response = BytesIO(b'ERROR: something went wrong')
+
+ with (
+ patch('archinstall.lib.output.logger', _fake_logger(log_file)),
+ patch('urllib.request.urlopen', return_value=fake_response),
+ ):
+ assert share_install_log() == 1