Fix #673: Make copied template files writable for immutable distros

On NixOS and other immutable distributions, package files in the store are
read-only. shutil.copytree preserves source permissions, so copied template
files remain read-only. This causes PermissionError when create-theme tries
to write __init__.py into the copied directory.

Added make_tree_writable() that adds user-write permission to all files and
directories after copying. Called automatically by copy_templates().
This commit is contained in:
Sina Atalay
2026-03-20 21:50:28 +03:00
parent e80ecfc559
commit 21e3cd7f33
2 changed files with 66 additions and 1 deletions

View File

@@ -1,5 +1,7 @@
import os
import pathlib
import shutil
import stat
from typing import Literal
@@ -29,3 +31,23 @@ def copy_templates(
copy_templates_to,
ignore=shutil.ignore_patterns("__init__.py", "__pycache__"),
)
make_tree_writable(copy_templates_to)
def make_tree_writable(path: pathlib.Path) -> None:
"""Add user-write permission to all files and directories in a tree.
Why:
On immutable distributions like NixOS, package files are read-only.
shutil.copytree preserves source permissions, so copied files remain
read-only. Users need write access to customize templates.
Args:
path: Root directory to make writable.
"""
for dirpath, _, filenames in os.walk(path):
dp = pathlib.Path(dirpath)
dp.chmod(dp.stat().st_mode | stat.S_IWUSR)
for filename in filenames:
fp = dp / filename
fp.chmod(fp.stat().st_mode | stat.S_IWUSR)

View File

@@ -1,6 +1,10 @@
import os
import pathlib
import stat
import pytest
from rendercv.cli.copy_templates import copy_templates
from rendercv.cli.copy_templates import copy_templates, make_tree_writable
@pytest.mark.parametrize("template_type", ["markdown", "typst"])
@@ -14,3 +18,42 @@ def test_copy_templates(template_type, tmp_path):
assert not (destination / "__pycache__").exists()
# Check that at least some files were copied
assert any(destination.iterdir())
def test_copy_templates_produces_writable_files(tmp_path):
destination = tmp_path / "templates"
copy_templates("typst", destination)
for dirpath, _, filenames in os.walk(destination):
dp = pathlib.Path(dirpath)
assert dp.stat().st_mode & stat.S_IWUSR, f"Directory {dp} is not user-writable"
for filename in filenames:
fp = dp / filename
assert fp.stat().st_mode & stat.S_IWUSR, f"File {fp} is not user-writable"
def test_make_tree_writable(tmp_path):
# Create a tree with read-only files and directories
subdir = tmp_path / "readonly_dir"
subdir.mkdir()
test_file = subdir / "test.txt"
test_file.write_text("content")
nested = subdir / "nested"
nested.mkdir()
nested_file = nested / "nested.txt"
nested_file.write_text("nested content")
# Make everything read-only
nested_file.chmod(stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
nested.chmod(stat.S_IRUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IROTH)
test_file.chmod(stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
subdir.chmod(stat.S_IRUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IROTH)
make_tree_writable(subdir)
# All should be user-writable now
assert subdir.stat().st_mode & stat.S_IWUSR
assert test_file.stat().st_mode & stat.S_IWUSR
assert nested.stat().st_mode & stat.S_IWUSR
assert nested_file.stat().st_mode & stat.S_IWUSR