1import json 2import os 3import shutil 4 5from datetime import datetime 6from dateutil.tz import gettz 7 8from contextlib import contextmanager 9from six.moves.urllib.parse import urlparse 10 11from conans import load 12from conans.client import tools 13from conans.client.cache.remote_registry import load_registry_txt, migrate_registry_file 14from conans.client.downloaders.file_downloader import FileDownloader 15from conans.client.tools import Git 16from conans.client.tools.files import unzip 17from conans.errors import ConanException 18from conans.util.files import mkdir, rmdir, walk, save, touch, remove 19from conans.client.cache.cache import ClientCache 20 21 22def _hide_password(resource): 23 """ 24 Hide password from url/file path 25 26 :param resource: string with url or file path 27 :return: resource with hidden password if present 28 """ 29 password = urlparse(resource).password 30 return resource.replace(password, "<hidden>") if password else resource 31 32 33def _handle_remotes(cache, remote_file): 34 # FIXME: Should we encourage to pass the remotes in json? 35 remotes, _ = load_registry_txt(load(remote_file)) 36 cache.registry.define(remotes) 37 38 39@contextmanager 40def tmp_config_install_folder(cache): 41 tmp_folder = os.path.join(cache.cache_folder, "tmp_config_install") 42 # necessary for Mac OSX, where the temp folders in /var/ are symlinks to /private/var/ 43 tmp_folder = os.path.realpath(tmp_folder) 44 rmdir(tmp_folder) 45 mkdir(tmp_folder) 46 try: 47 yield tmp_folder 48 finally: 49 rmdir(tmp_folder) 50 51 52def _process_git_repo(config, cache, output): 53 output.info("Trying to clone repo: %s" % config.uri) 54 with tmp_config_install_folder(cache) as tmp_folder: 55 with tools.chdir(tmp_folder): 56 try: 57 args = config.args or "" 58 git = Git(verify_ssl=config.verify_ssl, output=output) 59 git.clone(config.uri, args=args) 60 output.info("Repo cloned!") 61 except Exception as e: 62 raise ConanException("Can't clone repo: %s" % str(e)) 63 _process_folder(config, tmp_folder, cache, output) 64 65 66def _process_zip_file(config, zippath, cache, output, tmp_folder, first_remove=False): 67 unzip(zippath, tmp_folder, output=output) 68 if first_remove: 69 os.unlink(zippath) 70 _process_folder(config, tmp_folder, cache, output) 71 72 73def _handle_conan_conf(current_conan_conf, new_conan_conf_path): 74 current_conan_conf.read(new_conan_conf_path) 75 with open(current_conan_conf.filename, "w") as f: 76 current_conan_conf.write(f) 77 78 79def _filecopy(src, filename, dst): 80 # https://github.com/conan-io/conan/issues/6556 81 # This is just a local convenience for "conan config install", using copyfile to avoid 82 # copying with permissions that later cause bugs 83 src = os.path.join(src, filename) 84 dst = os.path.join(dst, filename) 85 # Clear the destination file 86 if os.path.exists(dst): 87 if os.path.isdir(dst): # dst was a directory and now src is a file 88 rmdir(dst) 89 else: 90 remove(dst) 91 shutil.copyfile(src, dst) 92 93 94def _process_file(directory, filename, config, cache, output, folder): 95 if filename == "settings.yml": 96 output.info("Installing settings.yml") 97 _filecopy(directory, filename, cache.cache_folder) 98 elif filename == "conan.conf": 99 output.info("Processing conan.conf") 100 _handle_conan_conf(cache.config, os.path.join(directory, filename)) 101 elif filename == "remotes.txt": 102 output.info("Defining remotes from remotes.txt") 103 _handle_remotes(cache, os.path.join(directory, filename)) 104 elif filename in ("registry.txt", "registry.json"): 105 try: 106 os.remove(cache.remotes_path) 107 except OSError: 108 pass 109 finally: 110 _filecopy(directory, filename, cache.cache_folder) 111 migrate_registry_file(cache, output) 112 elif filename == "remotes.json": 113 # Fix for Conan 2.0 114 raise ConanException("remotes.json install is not supported yet. Use 'remotes.txt'") 115 else: 116 # This is ugly, should be removed in Conan 2.0 117 if filename in ("README.md", "LICENSE.txt"): 118 output.info("Skip %s" % filename) 119 else: 120 relpath = os.path.relpath(directory, folder) 121 if config.target_folder: 122 target_folder = os.path.join(cache.cache_folder, config.target_folder, 123 relpath) 124 else: 125 target_folder = os.path.join(cache.cache_folder, relpath) 126 127 if os.path.exists(target_folder): 128 if os.path.isfile(target_folder): # Existed as a file and now should be a folder 129 remove(target_folder) 130 131 mkdir(target_folder) 132 output.info("Copying file %s to %s" % (filename, target_folder)) 133 _filecopy(directory, filename, target_folder) 134 135 136def _process_folder(config, folder, cache, output): 137 if not os.path.isdir(folder): 138 raise ConanException("No such directory: '%s'" % str(folder)) 139 if config.source_folder: 140 folder = os.path.join(folder, config.source_folder) 141 for root, dirs, files in walk(folder): 142 dirs[:] = [d for d in dirs if d != ".git"] 143 for f in files: 144 _process_file(root, f, config, cache, output, folder) 145 146 147def _process_download(config, cache, output, requester): 148 with tmp_config_install_folder(cache) as tmp_folder: 149 output.info("Trying to download %s" % _hide_password(config.uri)) 150 zippath = os.path.join(tmp_folder, os.path.basename(config.uri)) 151 try: 152 downloader = FileDownloader(requester=requester, output=output, verify=config.verify_ssl, 153 config_retry=None, config_retry_wait=None) 154 downloader.download(url=config.uri, file_path=zippath) 155 _process_zip_file(config, zippath, cache, output, tmp_folder, first_remove=True) 156 except Exception as e: 157 raise ConanException("Error while installing config from %s\n%s" % (config.uri, str(e))) 158 159 160class _ConfigOrigin(object): 161 def __init__(self, data): 162 self.type = data.get("type") 163 self.uri = data.get("uri") 164 self.verify_ssl = data.get("verify_ssl") 165 self.args = data.get("args") 166 self.source_folder = data.get("source_folder") 167 self.target_folder = data.get("target_folder") 168 169 def __eq__(self, other): 170 return (self.type == other.type and self.uri == other.uri and 171 self.args == other.args and self.source_folder == other.source_folder 172 and self.target_folder == other.target_folder) 173 174 def __ne__(self, other): 175 return not self.__eq__(other) 176 177 def json(self): 178 return {"type": self.type, 179 "uri": self.uri, 180 "verify_ssl": self.verify_ssl, 181 "args": self.args, 182 "source_folder": self.source_folder, 183 "target_folder": self.target_folder} 184 185 @staticmethod 186 def from_item(uri, config_type, verify_ssl, args, source_folder, target_folder): 187 config = _ConfigOrigin({}) 188 if config_type: 189 config.type = config_type 190 else: 191 if uri.endswith(".git"): 192 config.type = "git" 193 elif os.path.isdir(uri): 194 config.type = "dir" 195 elif os.path.isfile(uri): 196 config.type = "file" 197 elif uri.startswith("http"): 198 config.type = "url" 199 else: 200 raise ConanException("Unable to deduce type config install: %s" % uri) 201 config.source_folder = source_folder 202 config.target_folder = target_folder 203 config.args = args 204 config.verify_ssl = verify_ssl 205 if os.path.exists(uri): 206 uri = os.path.abspath(uri) 207 config.uri = uri 208 return config 209 210 211def _is_compressed_file(filename): 212 open(filename, "r") # Check if the file exist and can be opened 213 import zipfile 214 if zipfile.is_zipfile(filename): 215 return True 216 if (filename.endswith(".tar.gz") or filename.endswith(".tgz") or 217 filename.endswith(".tbz2") or filename.endswith(".tar.bz2") or 218 filename.endswith(".tar") or filename.endswith(".gz") or 219 filename.endswith(".tar.xz") or filename.endswith(".txz")): 220 return True 221 return False 222 223 224def _process_config(config, cache, output, requester): 225 try: 226 if config.type == "git": 227 _process_git_repo(config, cache, output) 228 elif config.type == "dir": 229 _process_folder(config, config.uri, cache, output) 230 elif config.type == "file": 231 if _is_compressed_file(config.uri): 232 with tmp_config_install_folder(cache) as tmp_folder: 233 _process_zip_file(config, config.uri, cache, output, tmp_folder) 234 else: 235 dirname, filename = os.path.split(config.uri) 236 _process_file(dirname, filename, config, cache, output, dirname) 237 elif config.type == "url": 238 _process_download(config, cache, output, requester=requester) 239 else: 240 raise ConanException("Unable to process config install: %s" % config.uri) 241 except Exception as e: 242 raise ConanException("Failed conan config install: %s" % str(e)) 243 244 245def _save_configs(configs_file, configs): 246 save(configs_file, json.dumps([config.json() for config in configs], 247 indent=True)) 248 249 250def _load_configs(configs_file): 251 try: 252 configs = json.loads(load(configs_file)) 253 except Exception as e: 254 raise ConanException("Error loading configs-install file: %s\n%s" 255 % (configs_file, str(e))) 256 return [_ConfigOrigin(config) for config in configs] 257 258 259def configuration_install(app, uri, verify_ssl, config_type=None, 260 args=None, source_folder=None, target_folder=None): 261 cache, output, requester = app.cache, app.out, app.requester 262 configs = [] 263 configs_file = cache.config_install_file 264 if os.path.isfile(configs_file): 265 configs = _load_configs(configs_file) 266 if uri is None: 267 if config_type or args or not verify_ssl: # Not the defaults 268 if not configs: 269 raise ConanException("Called config install without arguments") 270 # Modify the last one 271 config = configs[-1] 272 config.config_type = config_type or config.type 273 config.args = args or config.args 274 config.verify_ssl = verify_ssl or config.verify_ssl 275 _process_config(config, cache, output, requester) 276 _save_configs(configs_file, configs) 277 else: 278 if not configs: 279 raise ConanException("Called config install without arguments") 280 # Execute the previously stored ones 281 for config in configs: 282 output.info("Config install: %s" % _hide_password(config.uri)) 283 _process_config(config, cache, output, requester) 284 touch(cache.config_install_file) 285 else: 286 # Execute and store the new one 287 config = _ConfigOrigin.from_item(uri, config_type, verify_ssl, args, 288 source_folder, target_folder) 289 _process_config(config, cache, output, requester) 290 if config not in configs: 291 configs.append(config) 292 else: 293 configs = [(c if c != config else config) for c in configs] 294 _save_configs(configs_file, configs) 295 296 297def _is_scheduled_intervals(file, interval): 298 """ Check if time interval is bigger than last file change 299 300 :param file: file path to stat last change 301 :param interval: required time interval 302 :return: True if last change - current time is bigger than interval. Otherwise, False. 303 """ 304 timestamp = os.path.getmtime(file) 305 sched = datetime.fromtimestamp(timestamp, tz=gettz()) 306 sched += interval 307 now = datetime.now(gettz()) 308 return now > sched 309 310 311def is_config_install_scheduled(api): 312 """ Validate if the next config install is scheduled to occur now 313 314 When config_install_interval is not configured, config install should not run 315 When configs file is empty, scheduled config install should not run 316 When config_install_interval is configured, config install will respect the delta from: 317 last conan install execution (sched file) + config_install_interval value < now 318 319 :param api: Conan API instance 320 :return: True, if it should occur now. Otherwise, False. 321 """ 322 cache = ClientCache(api.cache_folder, api.out) 323 interval = cache.config.config_install_interval 324 config_install_file = cache.config_install_file 325 if interval is not None: 326 if not os.path.exists(config_install_file): 327 raise ConanException("config_install_interval defined, but no config_install file") 328 scheduled = _is_scheduled_intervals(config_install_file, interval) 329 if scheduled and not _load_configs(config_install_file): 330 api.out.warn("Skipping scheduled config install, " 331 "no config listed in config_install file") 332 os.utime(config_install_file, None) 333 else: 334 return scheduled 335