Skip to content

Commit fbb90c4

Browse files
author
MacroFake
committed
Merge bitcoin/bitcoin#25015: test: Use permissions from git in lint-files.py
908fb7e test: Use permissions from git in `lint-files.py` (laanwj) 48d2e80 test: Don't use shell=True in `lint-files.py` (laanwj) Pull request description: Improvements to the `lint-files.py` script: - Avoid use of `shell=True`. - Check the permissions in git's metadata instead of in the filesystem. This stops the umask or filesystem from interfering. It's also more efficient as it only needs a single call to `git ls-files`. (what triggered this change was `File "..." contains a shebang line, but has the file permission 775 instead of the expected executable permission 755.` errors running the script locally). ACKs for top commit: vincenzopalazzo: re-tACK bitcoin/bitcoin@908fb7e Tree-SHA512: 2eaf868c55a9c3508b12658a5b3ac429963fd0551e645332d0ac54be56fefccee95115e4667386df24b46b545593cb0d0bf8c6cecab73f9cb19d37ddf704c614
2 parents 3368f84 + 908fb7e commit fbb90c4

File tree

1 file changed

+45
-32
lines changed

1 file changed

+45
-32
lines changed

test/lint/lint-files.py

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,36 @@
1111
import re
1212
import sys
1313
from subprocess import check_output
14-
from typing import Optional, NoReturn
14+
from typing import Dict, Optional, NoReturn
1515

1616
CMD_TOP_LEVEL = ["git", "rev-parse", "--show-toplevel"]
17-
CMD_ALL_FILES = "git ls-files -z --full-name"
18-
CMD_SOURCE_FILES = 'git ls-files -z --full-name -- "*.[cC][pP][pP]" "*.[hH]" "*.[pP][yY]" "*.[sS][hH]"'
19-
CMD_SHEBANG_FILES = "git grep --full-name --line-number -I '^#!'"
17+
CMD_ALL_FILES = ["git", "ls-files", "-z", "--full-name", "--stage"]
18+
CMD_SHEBANG_FILES = ["git", "grep", "--full-name", "--line-number", "-I", "^#!"]
19+
20+
ALL_SOURCE_FILENAMES_REGEXP = r"^.*\.(cpp|h|py|sh)$"
2021
ALLOWED_FILENAME_REGEXP = "^[a-zA-Z0-9/_.@][a-zA-Z0-9/_.@-]*$"
2122
ALLOWED_SOURCE_FILENAME_REGEXP = "^[a-z0-9_./-]+$"
2223
ALLOWED_SOURCE_FILENAME_EXCEPTION_REGEXP = (
2324
"^src/(secp256k1/|minisketch/|univalue/|test/fuzz/FuzzedDataProvider.h)"
2425
)
25-
ALLOWED_PERMISSION_NON_EXECUTABLES = 644
26-
ALLOWED_PERMISSION_EXECUTABLES = 755
26+
ALLOWED_PERMISSION_NON_EXECUTABLES = 0o644
27+
ALLOWED_PERMISSION_EXECUTABLES = 0o755
2728
ALLOWED_EXECUTABLE_SHEBANG = {
2829
"py": [b"#!/usr/bin/env python3"],
2930
"sh": [b"#!/usr/bin/env bash", b"#!/bin/sh"],
3031
}
3132

3233

3334
class FileMeta(object):
34-
def __init__(self, file_path: str):
35-
self.file_path = file_path
35+
def __init__(self, file_spec: str):
36+
'''Parse a `git ls files --stage` output line.'''
37+
# 100755 5a150d5f8031fcd75e80a4dd9843afa33655f579 0 ci/test/00_setup_env.sh
38+
meta, self.file_path = file_spec.split('\t', 2)
39+
meta = meta.split()
40+
# The octal file permission of the file. Internally, git only
41+
# keeps an 'executable' bit, so this will always be 0o644 or 0o755.
42+
self.permissions = int(meta[0], 8) & 0o7777
43+
# We don't currently care about the other fields
3644

3745
@property
3846
def extension(self) -> Optional[str]:
@@ -60,20 +68,24 @@ def full_extension(self) -> Optional[str]:
6068
except IndexError:
6169
return None
6270

63-
@property
64-
def permissions(self) -> int:
65-
"""
66-
Returns the octal file permission of the file
67-
"""
68-
return int(oct(os.stat(self.file_path).st_mode)[-3:])
6971

72+
def get_git_file_metadata() -> Dict[str, FileMeta]:
73+
'''
74+
Return a dictionary mapping the name of all files in the repository to git tree metadata.
75+
'''
76+
files_raw = check_output(CMD_ALL_FILES).decode("utf8").rstrip("\0").split("\0")
77+
files = {}
78+
for file_spec in files_raw:
79+
meta = FileMeta(file_spec)
80+
files[meta.file_path] = meta
81+
return files
7082

71-
def check_all_filenames() -> int:
83+
def check_all_filenames(files) -> int:
7284
"""
7385
Checks every file in the repository against an allowed regexp to make sure only lowercase or uppercase
7486
alphanumerics (a-zA-Z0-9), underscores (_), hyphens (-), at (@) and dots (.) are used in repository filenames.
7587
"""
76-
filenames = check_output(CMD_ALL_FILES, shell=True).decode("utf8").rstrip("\0").split("\0")
88+
filenames = files.keys()
7789
filename_regex = re.compile(ALLOWED_FILENAME_REGEXP)
7890
failed_tests = 0
7991
for filename in filenames:
@@ -85,14 +97,14 @@ def check_all_filenames() -> int:
8597
return failed_tests
8698

8799

88-
def check_source_filenames() -> int:
100+
def check_source_filenames(files) -> int:
89101
"""
90102
Checks only source files (*.cpp, *.h, *.py, *.sh) against a stricter allowed regexp to make sure only lowercase
91103
alphanumerics (a-z0-9), underscores (_), hyphens (-) and dots (.) are used in source code filenames.
92104
93105
Additionally there is an exception regexp for directories or files which are excepted from matching this regexp.
94106
"""
95-
filenames = check_output(CMD_SOURCE_FILES, shell=True).decode("utf8").rstrip("\0").split("\0")
107+
filenames = [filename for filename in files.keys() if re.match(ALL_SOURCE_FILENAMES_REGEXP, filename, re.IGNORECASE)]
96108
filename_regex = re.compile(ALLOWED_SOURCE_FILENAME_REGEXP)
97109
filename_exception_regex = re.compile(ALLOWED_SOURCE_FILENAME_EXCEPTION_REGEXP)
98110
failed_tests = 0
@@ -105,24 +117,22 @@ def check_source_filenames() -> int:
105117
return failed_tests
106118

107119

108-
def check_all_file_permissions() -> int:
120+
def check_all_file_permissions(files) -> int:
109121
"""
110122
Checks all files in the repository match an allowed executable or non-executable file permission octal.
111123
112124
Additionally checks that for executable files, the file contains a shebang line
113125
"""
114-
filenames = check_output(CMD_ALL_FILES, shell=True).decode("utf8").rstrip("\0").split("\0")
115126
failed_tests = 0
116-
for filename in filenames:
117-
file_meta = FileMeta(filename)
127+
for filename, file_meta in files.items():
118128
if file_meta.permissions == ALLOWED_PERMISSION_EXECUTABLES:
119129
with open(filename, "rb") as f:
120130
shebang = f.readline().rstrip(b"\n")
121131

122132
# For any file with executable permissions the first line must contain a shebang
123133
if not shebang.startswith(b"#!"):
124134
print(
125-
f"""File "{filename}" has permission {ALLOWED_PERMISSION_EXECUTABLES} (executable) and is thus expected to contain a shebang '#!'. Add shebang or do "chmod {ALLOWED_PERMISSION_NON_EXECUTABLES} {filename}" to make it non-executable."""
135+
f"""File "{filename}" has permission {ALLOWED_PERMISSION_EXECUTABLES:03o} (executable) and is thus expected to contain a shebang '#!'. Add shebang or do "chmod {ALLOWED_PERMISSION_NON_EXECUTABLES:03o} {filename}" to make it non-executable."""
126136
)
127137
failed_tests += 1
128138

@@ -145,26 +155,26 @@ def check_all_file_permissions() -> int:
145155
continue
146156
else:
147157
print(
148-
f"""File "{filename}" has unexpected permission {file_meta.permissions}. Do "chmod {ALLOWED_PERMISSION_NON_EXECUTABLES} {filename}" (if non-executable) or "chmod {ALLOWED_PERMISSION_EXECUTABLES} {filename}" (if executable)."""
158+
f"""File "{filename}" has unexpected permission {file_meta.permissions:03o}. Do "chmod {ALLOWED_PERMISSION_NON_EXECUTABLES:03o} {filename}" (if non-executable) or "chmod {ALLOWED_PERMISSION_EXECUTABLES:03o} {filename}" (if executable)."""
149159
)
150160
failed_tests += 1
151161

152162
return failed_tests
153163

154164

155-
def check_shebang_file_permissions() -> int:
165+
def check_shebang_file_permissions(files_meta) -> int:
156166
"""
157167
Checks every file that contains a shebang line to ensure it has an executable permission
158168
"""
159-
filenames = check_output(CMD_SHEBANG_FILES, shell=True).decode("utf8").strip().split("\n")
169+
filenames = check_output(CMD_SHEBANG_FILES).decode("utf8").strip().split("\n")
160170

161171
# The git grep command we use returns files which contain a shebang on any line within the file
162172
# so we need to filter the list to only files with the shebang on the first line
163173
filenames = [filename.split(":1:")[0] for filename in filenames if ":1:" in filename]
164174

165175
failed_tests = 0
166176
for filename in filenames:
167-
file_meta = FileMeta(filename)
177+
file_meta = files_meta[filename]
168178
if file_meta.permissions != ALLOWED_PERMISSION_EXECUTABLES:
169179
# These file types are typically expected to be sourced and not executed directly
170180
if file_meta.full_extension in ["bash", "init", "openrc", "sh.in"]:
@@ -178,7 +188,7 @@ def check_shebang_file_permissions() -> int:
178188
continue
179189

180190
print(
181-
f"""File "{filename}" contains a shebang line, but has the file permission {file_meta.permissions} instead of the expected executable permission {ALLOWED_PERMISSION_EXECUTABLES}. Do "chmod {ALLOWED_PERMISSION_EXECUTABLES} {filename}" (or remove the shebang line)."""
191+
f"""File "{filename}" contains a shebang line, but has the file permission {file_meta.permissions:03o} instead of the expected executable permission {ALLOWED_PERMISSION_EXECUTABLES:03o}. Do "chmod {ALLOWED_PERMISSION_EXECUTABLES:03o} {filename}" (or remove the shebang line)."""
182192
)
183193
failed_tests += 1
184194
return failed_tests
@@ -187,11 +197,14 @@ def check_shebang_file_permissions() -> int:
187197
def main() -> NoReturn:
188198
root_dir = check_output(CMD_TOP_LEVEL).decode("utf8").strip()
189199
os.chdir(root_dir)
200+
201+
files = get_git_file_metadata()
202+
190203
failed_tests = 0
191-
failed_tests += check_all_filenames()
192-
failed_tests += check_source_filenames()
193-
failed_tests += check_all_file_permissions()
194-
failed_tests += check_shebang_file_permissions()
204+
failed_tests += check_all_filenames(files)
205+
failed_tests += check_source_filenames(files)
206+
failed_tests += check_all_file_permissions(files)
207+
failed_tests += check_shebang_file_permissions(files)
195208

196209
if failed_tests:
197210
print(

0 commit comments

Comments
 (0)