Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,21 @@ jobs:
name: Build for remarkable
runs-on: ubuntu-latest
steps:
- name: Free up disk space
shell: bash
run: |
sudo rm -vrf \
/usr/lib/jvm \
/usr/share/dotnet \
/usr/share/swift \
/usr/local/.ghcup \
/usr/local/julia* \
/usr/local/lib/android \
/usr/local/share/chromium \
/opt/microsoft /opt/google \
/opt/az \
/usr/local/share/powershell \
| wc -l
- name: Checkout the codexctl repository
uses: actions/checkout@v4
- name: Nuitka ccache
Expand Down Expand Up @@ -210,4 +225,4 @@ jobs:
tag: ${{ env.TAG }}
commit: ${{ github.sha }}
generateReleaseNotes: true
makeLatest: true
makeLatest: true
39 changes: 37 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ FW_DATA := wVbHkgKisg-
IMG_SHA := fc7d145e18f14a1a3f435f2fd5ca5924fe8dfe59bf45605dc540deed59551ae4
LS_DATA := ". .. lost+found bin boot dev etc home lib media mnt postinst proc run sbin sys tmp uboot-postinst uboot-version usr var"
CAT_DATA := 20221026104022
FW_VERSION_SWU := 3.20.0.92
FW_FILE_SWU := remarkable-production-memfault-image-$(FW_VERSION_SWU)-rm2-public
IMG_SHA_SWU := 7de74325d82d249ccd644e6a6be2ada954a225cfe434d3bf16c4fa6e1c145eb9
LS_DATA_SWU := ". .. lost+found bin boot dev etc home lib media mnt postinst postinst-waveform proc run sbin srv sys tmp uboot-version usr var"
CAT_DATA_SWU := 20250613122401
SHELL := /bin/bash

ifeq ($(OS),Windows_NT)
Expand Down Expand Up @@ -49,7 +54,13 @@ $(VENV_BIN_ACTIVATE): requirements.remote.txt requirements.txt
. $(VENV_BIN_ACTIVATE); \
python -m codexctl download --hardware rm2 --out .venv ${FW_VERSION}

test: $(VENV_BIN_ACTIVATE) .venv/${FW_VERSION}_reMarkable2-${FW_DATA}.signed
.venv/$(FW_FILE_SWU): $(VENV_BIN_ACTIVATE) $(OBJ)
@echo "[info] Downloading remarkable .swu update file"
@set -e; \
. $(VENV_BIN_ACTIVATE); \
python -m codexctl download --hardware rm2 --out .venv $(FW_VERSION_SWU)

test: $(VENV_BIN_ACTIVATE) .venv/${FW_VERSION}_reMarkable2-${FW_DATA}.signed .venv/$(FW_FILE_SWU)
@echo "[info] Running test"
@set -e; \
. $(VENV_BIN_ACTIVATE); \
Expand All @@ -74,9 +85,21 @@ test: $(VENV_BIN_ACTIVATE) .venv/${FW_VERSION}_reMarkable2-${FW_DATA}.signed
if ! diff --color <(python -m codexctl cat ".venv/${FW_VERSION}_reMarkable2-${FW_DATA}.signed" /etc/version | tr -d "\n\r") <(echo -n ${CAT_DATA}) | cat -te; then \
echo "codexctl cat failed test"; \
exit 1; \
fi; \
echo "[info] Running .swu tests"; \
python -m codexctl extract --out ".venv/$(FW_FILE_SWU).img" ".venv/$(FW_FILE_SWU)"; \
echo "$(IMG_SHA_SWU) .venv/$(FW_FILE_SWU).img" | $(SHA256SUM) -c; \
rm -f ".venv/$(FW_FILE_SWU).img"; \
if ! diff --color <(python -m codexctl ls ".venv/$(FW_FILE_SWU)" / | tr -d "\n\r") <(echo -n $(LS_DATA_SWU)) | cat -te; then \
echo "codexctl ls .swu failed test"; \
exit 1; \
fi; \
if ! diff --color <(python -m codexctl cat ".venv/$(FW_FILE_SWU)" /etc/version | tr -d "\n\r") <(echo -n $(CAT_DATA_SWU)) | cat -te; then \
echo "codexctl cat .swu failed test"; \
exit 1; \
fi

test-executable: .venv/${FW_VERSION}_reMarkable2-${FW_DATA}.signed
test-executable: .venv/${FW_VERSION}_reMarkable2-${FW_DATA}.signed .venv/$(FW_FILE_SWU)
@set -e; \
. $(VENV_BIN_ACTIVATE); \
dist/${CODEXCTL_BIN} extract --out ".venv/${FW_VERSION}_reMarkable2-${FW_DATA}.img" ".venv/${FW_VERSION}_reMarkable2-${FW_DATA}.signed"; \
Expand All @@ -90,6 +113,18 @@ test-executable: .venv/${FW_VERSION}_reMarkable2-${FW_DATA}.signed
if ! diff --color <(dist/${CODEXCTL_BIN} cat ".venv/${FW_VERSION}_reMarkable2-${FW_DATA}.signed" /etc/version | tr -d "\n\r") <(echo -n ${CAT_DATA}) | cat -te; then \
echo "codexctl cat failed test"; \
exit 1; \
fi; \
echo "[info] Running .swu tests"; \
dist/${CODEXCTL_BIN} extract --out ".venv/$(FW_FILE_SWU).img" ".venv/$(FW_FILE_SWU)"; \
echo "$(IMG_SHA_SWU) .venv/$(FW_FILE_SWU).img" | $(SHA256SUM) -c; \
rm -f ".venv/$(FW_FILE_SWU).img"; \
if ! diff --color <(dist/${CODEXCTL_BIN} ls ".venv/$(FW_FILE_SWU)" / | tr -d "\n\r") <(echo -n $(LS_DATA_SWU)) | cat -te; then \
echo "codexctl ls .swu failed test"; \
exit 1; \
fi; \
if ! diff --color <(dist/${CODEXCTL_BIN} cat ".venv/$(FW_FILE_SWU)" /etc/version | tr -d "\n\r") <(echo -n $(CAT_DATA_SWU)) | cat -te; then \
echo "codexctl cat .swu failed test"; \
exit 1; \
fi

clean:
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ A utility program that helps to manage the remarkable device version utilizing [

If your reMarkable device is at or above 3.11.2.5 and you want to downgrade to a version below 3.11.2.5, codexctl cannot do this currently. Please refer to https://github.com/Jayy001/codexctl/issues/95#issuecomment-2305529048 for manual instructions.

## Paper Pro bootloader updates

When downgrading a Paper Pro device across the 3.20/3.22 firmware boundary, codexctl automatically handles bootloader updates. It will download the current version's firmware if needed and extract the necessary bootloader files (`update-bootloader.sh` and `imx-boot`) to ensure a safe downgrade.

## Installation

You can find pre-compiled binaries on the [releases](https://github.com/Jayy001/codexctl/releases/) page. This includes a build for the reMarkable itself, as well as well as builds for linux, macOS, and Windows. Alternatively, you can install directly from pypi with `pip install codexctl`. Codexctl currently only has support for a **command line interfaces** but a graphical interface is soon to come.
Expand Down Expand Up @@ -53,6 +57,11 @@ codexctl install latest
codexctl download 3.15.4.2 --hardware rmpp -o out
codexctl install ./out/remarkable-ct-prototype-image-3.15.4.2-ferrari-public.swu
```
- Downloading rmppm version 3.23.0.64 to a folder named `out` and then installing it
```
codexctl download 3.23.0.64 --hardware rmppm -o out
codexctl install ./out/remarkable-production-image-3.23.0.64-chiappa-public.swu
```
- Backing up all documents to the cwd
```
codexctl backup
Expand All @@ -65,7 +74,7 @@ codexctl backup --incremental
```
codexctl backup -l root -r FM --no-recursion --no-overwrite
```
- Getting the version of the device and then switching to previous version (restore only for rm1/rm2)
- Getting the version of the device and then switching to previous version
```
codexctl status
codexctl restore
Expand All @@ -75,3 +84,7 @@ codexctl restore
codexctl download 3.8.0.1944 --hardware rm2
codexctl cat 3.8.0.1944_reMarkable2-7eGpAv7sYB.signed /etc/version
```
- Extract the filesystem image of an upgrade file as a file named `extracted`
```
codexctl extract remarkable-production-image-3.22.0.64-ferrari-public.swu -o extracted
```
167 changes: 143 additions & 24 deletions codexctl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,17 @@ def call_func(self, function: str, args: dict) -> None:
remarkable_pp_versions = "\n".join(
self.updater.remarkablepp_versions.keys()
)
remarkable_ppm_versions = "\n".join(
self.updater.remarkableppm_versions.keys()
)
remarkable_2_versions = "\n".join(self.updater.remarkable2_versions.keys())
remarkable_1_versions = "\n".join(self.updater.remarkable1_versions.keys())

version_blocks = []
if remarkable_version is None or remarkable_version == HardwareType.RMPP:
version_blocks.append(f"ReMarkable Paper Pro:\n{remarkable_pp_versions}")
if remarkable_version is None or remarkable_version == HardwareType.RMPPM:
version_blocks.append(f"ReMarkable Paper Pro Move:\n{remarkable_ppm_versions}")
if remarkable_version is None or remarkable_version == HardwareType.RM2:
version_blocks.append(f"ReMarkable 2:\n{remarkable_2_versions}")
if remarkable_version is None or remarkable_version == HardwareType.RM1:
Expand All @@ -95,24 +100,35 @@ def call_func(self, function: str, args: dict) -> None:

### Mounting functionalities
elif function in ("extract", "mount"):
try:
from .analysis import get_update_image
except ImportError:
raise ImportError(
"remarkable_update_image is required for analysis. Please install it!"
)

if function == "extract":
if not args["out"]:
args["out"] = os.getcwd() + "/extracted"

logger.debug(f"Extracting {args['file']} to {args['out']}")
image, volume = get_update_image(args["file"])

try:
from remarkable_update_image import UpdateImage
except ImportError:
raise ImportError(
"remarkable_update_image is required for extraction. Please install it!"
) from None

image = UpdateImage(args["file"])
image.seek(0)

with open(args["out"], "wb") as f:
f.write(image.read())

logger.info(f"Extracted image to {args['out']}")
else:
try:
from .analysis import get_update_image
from remarkable_update_fuse import UpdateFS
except ImportError:
raise ImportError(
"remarkable_update_fuse is required for mounting. Please install it!"
)

if args["out"] is None:
args["out"] = "/opt/remarkable/"

Expand All @@ -122,8 +138,6 @@ def call_func(self, function: str, args: dict) -> None:
if not os.path.exists(args["filesystem"]):
raise SystemExit("Firmware file does not exist!")

from remarkable_update_fuse import UpdateFS

server = UpdateFS()
server.parse(
args=[args["filesystem"], args["out"]], values=server, errex=1
Expand Down Expand Up @@ -193,6 +207,13 @@ def call_func(self, function: str, args: dict) -> None:
)
remote = True

try:
from remarkable_update_image import UpdateImage
except ImportError:
raise ImportError(
"remarkable_update_image is required for install. Please install it!"
) from None

from .device import DeviceManager
from .server import get_available_version

Expand All @@ -209,17 +230,15 @@ def call_func(self, function: str, args: dict) -> None:
version = self.updater.get_toltec_version(remarkable.hardware)

if function == "status":
beta, prev, current, version_id = remarkable.get_device_status()
beta, prev, current, version_id, backup = remarkable.get_device_status()
print(
f"\nCurrent version: {current}\nOld update engine: {prev}\nBeta active: {beta}\nVersion id: {version_id}"
f"\nCurrent version: {current}\nBackup version: {backup}\nOld update engine: {prev}\nBeta active: {beta}\nVersion id: {version_id}"
)

elif function == "restore":
if remarkable.hardware == HardwareType.RMPP:
raise SystemError("Restore not available for rmpro.")
remarkable.restore_previous_version()
print(
f"Device restored to previous version [{remarkable.get_device_status()[1]}]"
f"Device restored to previous version [{remarkable.get_device_status()[4]}]"
)
remarkable.reboot_device()
print("Device rebooted")
Expand All @@ -236,18 +255,56 @@ def call_func(self, function: str, args: dict) -> None:
def version_lookup(version: str | None) -> re.Match[str] | None:
return re.search(r"\b\d+\.\d+\.\d+\.\d+\b", cast(str, version))

version_number = version_lookup(version)
version_number = None
swu_hardware = None

if update_file:
try:
from remarkable_update_image import UpdateImage
from remarkable_update_image.cpio import UpdateImage as CPIOUpdateImage

image = UpdateImage(update_file)
if isinstance(image, CPIOUpdateImage):
if image.version is None:
raise SystemError(f"Could not determine version from SWU file: {update_file}")

version_number = image.version
hw_map = {
"reMarkable1": HardwareType.RM1,
"reMarkable2": HardwareType.RM2,
"ferrari": HardwareType.RMPP,
"chiappa": HardwareType.RMPPM,
}
if image.hardware_type not in hw_map:
raise SystemError(f"Unsupported hardware type in SWU file: {update_file}")

swu_hardware = hw_map[image.hardware_type]
logger.info(f"Extracted from SWU: version={version_number}, hardware={swu_hardware.name}")

if swu_hardware != remarkable.hardware:
raise SystemError(
f"Hardware mismatch!\n"
f"SWU file is for: {swu_hardware.name}\n"
f"Connected device is: {remarkable.hardware.name}\n"
f"Cannot install firmware for different hardware."
)
except ValueError as e:
logger.warning(f"Could not extract metadata from update file: {e}")

if not version_number:
version_number = version_lookup(
input(
"Failed to get the version number from the filename, please enter it: "
version_match = version_lookup(version)
if not version_match:
version_match = version_lookup(
input(
"Failed to get the version number from the filename, please enter it: "
)
)
)
if not version_number:
raise SystemError("Invalid version!")
if not version_match:
raise SystemError("Invalid version!")

version_number = version_number.group()
version_number = version_match.group()
else:
version_number = str(version_number)

update_file_requires_new_engine = UpdateManager.uses_new_update_engine(
version_number
Expand Down Expand Up @@ -278,6 +335,68 @@ def version_lookup(version: str | None) -> re.Match[str] | None:

#############################################################

bootloader_files_for_install = None

if (device_version_uses_new_engine and
remarkable.hardware == HardwareType.RMPP):

current_version = remarkable.get_device_status()[2]

if UpdateManager.is_bootloader_boundary_downgrade(current_version, version_number):
print("\n" + "="*60)
print("WARNING: Bootloader Update Required")
print("="*60)
print(f"Current version: {current_version}")
print(f"Target version: {version_number}")
print()
print("Downgrading from 3.22+ to <3.22 requires updating the")
print("bootloader on both partitions. This process will:")
print(" 1. Download the current version's bootloader files")
print(" 2. Download the target OS version")
print(" 3. Install the target OS version")
print(" 4. Update bootloader on both partitions")
print(" 5. Reboot")
print()

response = input("Do you want to continue? (y/N): ")
if response.lower() != 'y':
raise SystemExit("Installation cancelled by user")

expected_swu_name = f"remarkable-production-memfault-image-{current_version}-{remarkable.hardware.new_download_hw}-public"
expected_swu_path = f"./{expected_swu_name}"

if os.path.isfile(expected_swu_path):
print(f"\nUsing existing {expected_swu_name} for bootloader extraction...")
current_swu_path = expected_swu_path
else:
print("\nDownloading current version's SWU for bootloader extraction...")
current_swu_path = self.updater.download_version(
remarkable.hardware,
current_version,
"./"
)

if not current_swu_path:
raise SystemError(
f"Failed to download current version {current_version} for bootloader extraction. "
f"This is required for safe downgrade across bootloader boundary."
)

print("Extracting bootloader files...")
from remarkable_update_image import UpdateImage
swu_image = UpdateImage(current_swu_path)
bootloader_files_for_install = {
'update-bootloader.sh': swu_image.archive[b'update-bootloader.sh'].read(),
'imx-boot': swu_image.archive[b'imx-boot'].read(),
}

if not all(bootloader_files_for_install.values()):
raise SystemError("Failed to extract bootloader files from current version")

print(f"✓ Extracted update-bootloader.sh ({len(bootloader_files_for_install['update-bootloader.sh'])} bytes)")
print(f"✓ Extracted imx-boot ({len(bootloader_files_for_install['imx-boot'])} bytes)")
print()

if not update_file_requires_new_engine:
if update_file: # Check if file exists
if os.path.dirname(
Expand Down Expand Up @@ -318,7 +437,7 @@ def version_lookup(version: str | None) -> re.Match[str] | None:
)

if device_version_uses_new_engine:
remarkable.install_sw_update(update_file)
remarkable.install_sw_update(update_file, bootloader_files=bootloader_files_for_install)
else:
remarkable.install_ohma_update(update_file)

Expand Down
3 changes: 2 additions & 1 deletion codexctl/analysis.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import ext4
import warnings
import warnings
import errno

from remarkable_update_image import UpdateImage
from remarkable_update_image import UpdateImageSignatureException
from .device import HardwareType


def get_update_image(file: str):
Expand Down
Loading
Loading