1# Copyright 2019 The Meson development team 2 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6 7# http://www.apache.org/licenses/LICENSE-2.0 8 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14 15import typing as T 16import hashlib 17import os 18from pathlib import Path, PurePath, PureWindowsPath 19 20from .. import mlog 21from . import ExtensionModule 22from ..mesonlib import ( 23 File, 24 FileOrString, 25 MesonException, 26 path_is_in_root, 27) 28from ..interpreterbase import FeatureNew, KwargInfo, typed_kwargs, typed_pos_args, noKwargs 29 30if T.TYPE_CHECKING: 31 from . import ModuleState 32 from ..interpreter import Interpreter 33 34 from typing_extensions import TypedDict 35 36 class ReadKwArgs(TypedDict): 37 """Keyword Arguments for fs.read.""" 38 39 encoding: str 40 41 42class FSModule(ExtensionModule): 43 44 def __init__(self, interpreter: 'Interpreter') -> None: 45 super().__init__(interpreter) 46 self.methods.update({ 47 'expanduser': self.expanduser, 48 'is_absolute': self.is_absolute, 49 'as_posix': self.as_posix, 50 'exists': self.exists, 51 'is_symlink': self.is_symlink, 52 'is_file': self.is_file, 53 'is_dir': self.is_dir, 54 'hash': self.hash, 55 'size': self.size, 56 'is_samepath': self.is_samepath, 57 'replace_suffix': self.replace_suffix, 58 'parent': self.parent, 59 'name': self.name, 60 'stem': self.stem, 61 'read': self.read, 62 }) 63 64 def _absolute_dir(self, state: 'ModuleState', arg: 'FileOrString') -> Path: 65 """ 66 make an absolute path from a relative path, WITHOUT resolving symlinks 67 """ 68 if isinstance(arg, File): 69 return Path(arg.absolute_path(state.source_root, self.interpreter.environment.get_build_dir())) 70 return Path(state.source_root) / Path(state.subdir) / Path(arg).expanduser() 71 72 def _resolve_dir(self, state: 'ModuleState', arg: 'FileOrString') -> Path: 73 """ 74 resolves symlinks and makes absolute a directory relative to calling meson.build, 75 if not already absolute 76 """ 77 path = self._absolute_dir(state, arg) 78 try: 79 # accommodate unresolvable paths e.g. symlink loops 80 path = path.resolve() 81 except Exception: 82 # return the best we could do 83 pass 84 return path 85 86 @noKwargs 87 @FeatureNew('fs.expanduser', '0.54.0') 88 @typed_pos_args('fs.expanduser', str) 89 def expanduser(self, state: 'ModuleState', args: T.Tuple[str], kwargs: T.Dict[str, T.Any]) -> str: 90 return str(Path(args[0]).expanduser()) 91 92 @noKwargs 93 @FeatureNew('fs.is_absolute', '0.54.0') 94 @typed_pos_args('fs.is_absolute', (str, File)) 95 def is_absolute(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> bool: 96 if isinstance(args[0], File): 97 FeatureNew('fs.is_absolute_file', '0.59.0').use(state.subproject) 98 return PurePath(str(args[0])).is_absolute() 99 100 @noKwargs 101 @FeatureNew('fs.as_posix', '0.54.0') 102 @typed_pos_args('fs.as_posix', str) 103 def as_posix(self, state: 'ModuleState', args: T.Tuple[str], kwargs: T.Dict[str, T.Any]) -> str: 104 """ 105 this function assumes you are passing a Windows path, even if on a Unix-like system 106 and so ALL '\' are turned to '/', even if you meant to escape a character 107 """ 108 return PureWindowsPath(args[0]).as_posix() 109 110 @noKwargs 111 @typed_pos_args('fs.exists', str) 112 def exists(self, state: 'ModuleState', args: T.Tuple[str], kwargs: T.Dict[str, T.Any]) -> bool: 113 return self._resolve_dir(state, args[0]).exists() 114 115 @noKwargs 116 @typed_pos_args('fs.is_symlink', (str, File)) 117 def is_symlink(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> bool: 118 if isinstance(args[0], File): 119 FeatureNew('fs.is_symlink_file', '0.59.0').use(state.subproject) 120 return self._absolute_dir(state, args[0]).is_symlink() 121 122 @noKwargs 123 @typed_pos_args('fs.is_file', str) 124 def is_file(self, state: 'ModuleState', args: T.Tuple[str], kwargs: T.Dict[str, T.Any]) -> bool: 125 return self._resolve_dir(state, args[0]).is_file() 126 127 @noKwargs 128 @typed_pos_args('fs.is_dir', str) 129 def is_dir(self, state: 'ModuleState', args: T.Tuple[str], kwargs: T.Dict[str, T.Any]) -> bool: 130 return self._resolve_dir(state, args[0]).is_dir() 131 132 @noKwargs 133 @typed_pos_args('fs.hash', (str, File), str) 134 def hash(self, state: 'ModuleState', args: T.Tuple['FileOrString', str], kwargs: T.Dict[str, T.Any]) -> str: 135 if isinstance(args[0], File): 136 FeatureNew('fs.hash_file', '0.59.0').use(state.subproject) 137 file = self._resolve_dir(state, args[0]) 138 if not file.is_file(): 139 raise MesonException(f'{file} is not a file and therefore cannot be hashed') 140 try: 141 h = hashlib.new(args[1]) 142 except ValueError: 143 raise MesonException('hash algorithm {} is not available'.format(args[1])) 144 mlog.debug('computing {} sum of {} size {} bytes'.format(args[1], file, file.stat().st_size)) 145 h.update(file.read_bytes()) 146 return h.hexdigest() 147 148 @noKwargs 149 @typed_pos_args('fs.size', (str, File)) 150 def size(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> int: 151 if isinstance(args[0], File): 152 FeatureNew('fs.size_file', '0.59.0').use(state.subproject) 153 file = self._resolve_dir(state, args[0]) 154 if not file.is_file(): 155 raise MesonException(f'{file} is not a file and therefore cannot be sized') 156 try: 157 return file.stat().st_size 158 except ValueError: 159 raise MesonException('{} size could not be determined'.format(args[0])) 160 161 @noKwargs 162 @typed_pos_args('fs.is_samepath', (str, File), (str, File)) 163 def is_samepath(self, state: 'ModuleState', args: T.Tuple['FileOrString', 'FileOrString'], kwargs: T.Dict[str, T.Any]) -> bool: 164 if isinstance(args[0], File) or isinstance(args[1], File): 165 FeatureNew('fs.is_samepath_file', '0.59.0').use(state.subproject) 166 file1 = self._resolve_dir(state, args[0]) 167 file2 = self._resolve_dir(state, args[1]) 168 if not file1.exists(): 169 return False 170 if not file2.exists(): 171 return False 172 try: 173 return file1.samefile(file2) 174 except OSError: 175 return False 176 177 @noKwargs 178 @typed_pos_args('fs.replace_suffix', (str, File), str) 179 def replace_suffix(self, state: 'ModuleState', args: T.Tuple['FileOrString', str], kwargs: T.Dict[str, T.Any]) -> str: 180 if isinstance(args[0], File): 181 FeatureNew('fs.replace_suffix_file', '0.59.0').use(state.subproject) 182 original = PurePath(str(args[0])) 183 new = original.with_suffix(args[1]) 184 return str(new) 185 186 @noKwargs 187 @typed_pos_args('fs.parent', (str, File)) 188 def parent(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> str: 189 if isinstance(args[0], File): 190 FeatureNew('fs.parent_file', '0.59.0').use(state.subproject) 191 original = PurePath(str(args[0])) 192 new = original.parent 193 return str(new) 194 195 @noKwargs 196 @typed_pos_args('fs.name', (str, File)) 197 def name(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> str: 198 if isinstance(args[0], File): 199 FeatureNew('fs.name_file', '0.59.0').use(state.subproject) 200 original = PurePath(str(args[0])) 201 new = original.name 202 return str(new) 203 204 @noKwargs 205 @typed_pos_args('fs.stem', (str, File)) 206 @FeatureNew('fs.stem', '0.54.0') 207 def stem(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: T.Dict[str, T.Any]) -> str: 208 if isinstance(args[0], File): 209 FeatureNew('fs.stem_file', '0.59.0').use(state.subproject) 210 original = PurePath(str(args[0])) 211 new = original.stem 212 return str(new) 213 214 @FeatureNew('fs.read', '0.57.0') 215 @typed_pos_args('fs.read', (str, File)) 216 @typed_kwargs('fs.read', KwargInfo('encoding', str, default='utf-8')) 217 def read(self, state: 'ModuleState', args: T.Tuple['FileOrString'], kwargs: 'ReadKwArgs') -> str: 218 """Read a file from the source tree and return its value as a decoded 219 string. 220 221 If the encoding is not specified, the file is assumed to be utf-8 222 encoded. Paths must be relative by default (to prevent accidents) and 223 are forbidden to be read from the build directory (to prevent build 224 loops) 225 """ 226 path = args[0] 227 encoding = kwargs['encoding'] 228 src_dir = self.interpreter.environment.source_dir 229 sub_dir = self.interpreter.subdir 230 build_dir = self.interpreter.environment.get_build_dir() 231 232 if isinstance(path, File): 233 if path.is_built: 234 raise MesonException( 235 'fs.read_file does not accept built files() objects') 236 path = os.path.join(src_dir, path.relative_name()) 237 else: 238 if sub_dir: 239 src_dir = os.path.join(src_dir, sub_dir) 240 path = os.path.join(src_dir, path) 241 242 path = os.path.abspath(path) 243 if path_is_in_root(Path(path), Path(build_dir), resolve=True): 244 raise MesonException('path must not be in the build tree') 245 try: 246 with open(path, encoding=encoding) as f: 247 data = f.read() 248 except UnicodeDecodeError: 249 raise MesonException(f'decoding failed for {path}') 250 # Reconfigure when this file changes as it can contain data used by any 251 # part of the build configuration (e.g. `project(..., version: 252 # fs.read_file('VERSION')` or `configure_file(...)` 253 self.interpreter.add_build_def_file(path) 254 return data 255 256 257def initialize(*args: T.Any, **kwargs: T.Any) -> FSModule: 258 return FSModule(*args, **kwargs) 259