Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tools] Add chmod in tools.files #17800

Open
wants to merge 11 commits into
base: develop2
Choose a base branch
from
2 changes: 1 addition & 1 deletion conan/tools/files/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from conan.tools.files.files import load, save, mkdir, rmdir, rm, ftp_download, download, get, \
rename, chdir, unzip, replace_in_file, collect_libs, check_md5, check_sha1, check_sha256, \
move_folder_contents
move_folder_contents, chmod

from conan.tools.files.patches import patch, apply_conandata_patches, export_conandata_patches
from conan.tools.files.packager import AutoPackager
Expand Down
43 changes: 43 additions & 0 deletions conan/tools/files/files.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import gzip
import os
import stat
import platform
import shutil
import subprocess
Expand Down Expand Up @@ -263,6 +264,48 @@ def chdir(conanfile, newdir):
os.chdir(old_path)


def chmod(conanfile, path:str, read:bool=None, write:bool=None, execute:bool=None, recursive:bool=False):
"""
Change the permissions of a file or directory. The same as the Unix command chmod, but simplified.
On Windows is limited to changing write permission only.

:param conanfile: The current recipe object. Always use ``self``.
:param path: Path to the file or directory to change the permissions.
:param read: If ``True``, the file or directory will have read permissions for owner user.
:param write: If ``True``, the file or directory will have write permissions for owner user.
:param execute: If ``True``, the file or directory will have execute permissions for owner user.
:param recursive: If ``True``, the permissions will be applied
"""
if read is None and write is None and execute is None:
raise ConanException("Could not change permission: At least one of the permissions should be set.")

if not os.path.exists(path):
raise ConanException(f"Could not change permission: Path \"{path}\" does not exist.")

def _change_permission(it_path:str):
mode = os.stat(it_path).st_mode
permissions = [
(read, stat.S_IRUSR),
(write, stat.S_IWUSR),
(execute, stat.S_IXUSR)
]
for enabled, mask in permissions:
if enabled is None:
continue
elif enabled:
mode |= mask
else:
mode &= ~mask
os.chmod(it_path, mode)

if recursive:
for root, _, files in os.walk(path):
for file in files:
_change_permission(os.path.join(root, file))
else:
_change_permission(path)


def unzip(conanfile, filename, destination=".", keep_permissions=False, pattern=None,
strip_root=False, extract_filter=None):
"""
Expand Down
110 changes: 110 additions & 0 deletions test/unittests/tools/files/test_chmod.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import os
import platform
import stat
import pytest

from conan.errors import ConanException
from conan.tools.files import chmod
from conan.test.utils.mocks import ConanFileMock
from conan.test.utils.tools import temp_folder, save_files


@pytest.mark.skipif(platform.system() == "Windows", reason="validate full permissions only in Unix")
@pytest.mark.parametrize("read,write,execute,expected", [
(True, True, True, 0o700),
(False, True, False, 0o200),
(False, False, True, 0o100),
(True, False, True, 0o500),
(True, True, False, 0o600),
(True, False, False, 0o400),
(False, False, False, 0o000),])
def test_chmod_single_file(read, write, execute, expected):
"""
The chmod should be able to change the permissions of a single file.
"""
tmp = temp_folder()
save_files(tmp, {"file.txt": "foobar"})
file_path = os.path.join(tmp, "file.txt")
os.chmod(file_path, 0o000)
conanfile = ConanFileMock()
chmod(conanfile, file_path, read=read, write=write, execute=execute, recursive=False)
file_mode = os.stat(file_path).st_mode
assert stat.S_IMODE(file_mode) == expected


@pytest.mark.skipif(platform.system() == "Windows", reason="validate full permissions only in Unix")
@pytest.mark.parametrize("read,write,execute,expected", [
(True, True, True, 0o700),
(False, True, False, 0o200),
(False, False, True, 0o100),
(True, False, True, 0o500),
(True, True, False, 0o600),
(True, False, False, 0o400),
(False, False, False, 0o000),])
def test_chmod_recursive(read, write, execute, expected):
"""
The chmod should be able to change the permissions of all files in a folder when recursive is set to True.
"""
tmp = temp_folder()
files = {"foobar/qux/file.txt": "foobar",
"foobar/file.txt": "qux",
"foobar/foo/file.txt": "foobar"}
save_files(tmp, files)
folder_path = os.path.join(tmp, "foobar")
for file in files.keys():
file_path = os.path.join(tmp, file)
os.chmod(file_path, 0o000)
conanfile = ConanFileMock()
chmod(conanfile, folder_path, read=read, write=write, execute=execute, recursive=True)
for file in files.keys():
file_mode = os.stat(os.path.join(tmp, file)).st_mode
assert stat.S_IMODE(file_mode) == expected


@pytest.mark.skipif(platform.system() == "Windows", reason="Validate default permissions only in Unix")
def test_chmod_default_values():
"""
When not passing a permission parameter, chmod should not change the specific permission.
"""
tmp = temp_folder()
save_files(tmp, {"file.txt": "foobar"})
file_path = os.path.join(tmp, "file.txt")
os.chmod(file_path, 0o111)
conanfile = ConanFileMock()
chmod(conanfile, file_path, read=True)
file_mode = os.stat(file_path).st_mode
assert stat.S_IMODE(file_mode) == 0o511


def test_missing_permission_arguments():
"""
The chmod should raise an exception if no new permission is provided.
"""
conanfile = ConanFileMock()
with pytest.raises(ConanException) as error:
chmod(conanfile, "invalid_path")
assert 'Could not change permission: At least one of the permissions should be set.' in str(error.value)


def test_invalid_path():
"""
The chmod should raise an exception if the path does not exist.
"""
conanfile = ConanFileMock()
with pytest.raises(ConanException) as error:
chmod(conanfile, "invalid_path", read=True, write=True, execute=True, recursive=False)
assert 'Could not change permission: Path "invalid_path" does not exist.' in str(error.value)


@pytest.mark.skipif(platform.system() != "Windows", reason="Validate read-only permissions only in Windows")
def test_chmod_windows():
"""
The chmod should be able to change read-only state in Windows.
"""
tmp = temp_folder()
save_files(tmp, {"file.txt": "foobar"})
file_path = os.path.join(tmp, "file.txt")
os.chmod(file_path, 0o000)
conanfile = ConanFileMock()
chmod(conanfile, file_path, read=True, write=True, execute=True, recursive=False)
assert os.access(file_path, os.W_OK)
Loading