1 /** 2 * ae.sys.install.common 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.sys.install.common; 15 16 import std.algorithm; 17 import std.array; 18 import std.exception; 19 import std.file; 20 import std.path; 21 import std.process : environment; 22 import std.string; 23 24 import ae.net.ietf.url; 25 import ae.sys.archive; 26 import ae.sys.file; 27 import ae.sys.net; 28 import ae.sys.persistence; 29 import ae.utils.meta; 30 import ae.utils.path; 31 32 /// Base class for an installer - a process to acquire and set up some 33 /// third-party software or component to some temporary location, so 34 /// that we can then invoke and use it. 35 class Installer 36 { 37 /// Where all software will be unpacked 38 /// (current directory, by default). 39 static string installationDirectory = null; 40 41 /// Log sink 42 static void delegate(string) logger; 43 44 static protected void log(string s) 45 { 46 if (logger) logger(s); 47 } 48 49 /// Component name. Used for logging. 50 @property string name() { return this.classinfo.name.split(".")[$-1].chomp("Installer"); } 51 52 /// The subdirectory where this component will be installed. 53 @property string subdirectory() { return name.toLower(); } 54 55 /// Subdirectories (under the installation subdirectory) 56 /// containing executables which need to be added to PATH. 57 @property string[] binPaths() { return [""]; } 58 59 /// As above, but expanded to full absolute directory paths. 60 @property final string[] binDirs() { return binPaths.map!(binPath => buildPath(directory, binPath)).array; } 61 62 deprecated("Please use ae.utils.path.pathDirs") 63 @property static string[] pathDirs() { return ae.utils.path.pathDirs; } 64 65 /// The full installation directory. 66 @property final string directory() 67 { 68 return buildPath(installationDirectory, subdirectory); 69 } 70 71 /// The list of executable names required to be present. 72 /// Null if this component is never considered already 73 /// available on the system. 74 @property string[] requiredExecutables() { return null; } 75 76 deprecated("Please use ae.utils.path.haveExecutable") 77 /*protected*/ static bool haveExecutable(string name) { return ae.utils.path.haveExecutable(name); } 78 79 deprecated("Please use ae.utils.path.findExecutable") 80 static string findExecutable(string name, string[] dirs) { return ae.utils.path.findExecutable(name, dirs); } 81 82 /// Get the full path to an executable. 83 string exePath(string name) 84 { 85 return .findExecutable(name, installedLocally ? binDirs : .pathDirs); 86 } 87 88 /// Whether the component is installed locally. 89 @property bool installedLocally() 90 { 91 // The directory should only be created atomically upon 92 // the end of a successful installation, so an exists 93 // check is sufficient. 94 return directory.exists; 95 } 96 97 /// Whether the component is already present on the system. 98 @property bool availableOnSystem() 99 { 100 if (requiredExecutables is null) 101 return false; 102 103 return requiredExecutables.all!(.haveExecutable)(); 104 } 105 106 /// Whether the component is installed, locally or 107 /// already present on the system. 108 @property final bool available() 109 { 110 return installedLocally || availableOnSystem; 111 } 112 113 /// Install this component if necessary. 114 final void require() 115 { 116 if (!available) 117 install(); 118 119 assert(available); 120 121 if (installedLocally) 122 addToPath(); 123 } 124 125 /// Install this component locally, if it isn't already installed. 126 final void requireLocal(bool shouldAddToPath = true) 127 { 128 if (!installedLocally) 129 install(); 130 131 if (shouldAddToPath) 132 addToPath(); 133 } 134 135 private bool addedToPath; 136 137 /// Change this process's PATH environment variable to include the 138 /// path to this component's executable directories. 139 void addToPath() 140 { 141 if (addedToPath) 142 return; 143 foreach (binPath; binPaths) 144 { 145 auto path = buildPath(directory, binPath).absolutePath(); 146 log("Adding " ~ path ~ " to PATH."); 147 // Override any system installations 148 environment["PATH"] = path ~ pathSeparator ~ environment["PATH"]; 149 } 150 addedToPath = true; 151 } 152 153 private void install() 154 { 155 mkdirRecurse(installationDirectory); 156 157 log("Installing " ~ name ~ " to " ~ directory ~ "..."); 158 atomicInstallImpl(); 159 log("Done installing " ~ name ~ "."); 160 } 161 162 /// Install to `directory` atomically. 163 protected void atomicInstallImpl() 164 { 165 void installProxy(string target) { installImpl(target); } 166 atomic!installProxy(directory); 167 } 168 169 protected void installImpl(string target) 170 { 171 uninstallable(); 172 } 173 174 // ---------------------------------------------------- 175 176 final: 177 protected: 178 static string saveLocation(string url) 179 { 180 return buildPath(installationDirectory, url.fileNameFromURL()); 181 } 182 183 template cachedAction(alias fun, string fmt) 184 { 185 static void cachedAction(Args...)(Args args, string target) 186 { 187 if (target.exists) 188 return; 189 log(fmt.format(args, target)); 190 atomic!fun(args, target); 191 } 192 } 193 194 static string[string] urlDigests; 195 196 static void saveFile(string url, string target) 197 { 198 downloadFile(url, target); 199 auto pDigest = url in urlDigests; 200 if (pDigest) 201 { 202 auto digest = *pDigest; 203 if (!digest) 204 return; 205 log("Verifying " ~ target.baseName() ~ "..."); 206 207 import std.digest.sha, std.digest, std.stdio; 208 SHA1 sha; 209 sha.start(); 210 foreach (chunk; File(target, "rb").byChunk(0x10000)) 211 sha.put(chunk[]); 212 auto hash = sha.finish(); 213 enforce(hash.toHexString!(LetterCase.lower) == digest, "Could not verify integrity of " ~ target); 214 } 215 else 216 log("WARNING: Not verifying integrity of " ~ url ~ "."); 217 } 218 219 alias saveTo = cachedAction!(saveFile, "Downloading %s to %s..."); 220 alias save = withTarget!(saveLocation, saveTo); 221 222 static auto saveAs(string url, string fn) 223 { 224 auto target = buildPath(installationDirectory, fn); 225 ensurePathExists(target); 226 url.I!saveTo(target); 227 return target; 228 } 229 230 static string stripArchiveExtension(string fn) 231 { 232 fn = fn.stripExtension(); 233 if (fn.extension == ".tar") 234 fn = fn.stripExtension(); 235 return fn; 236 } 237 238 alias unpackTo = cachedAction!(ae.sys.archive.unpack, "Unpacking %s to %s..."); 239 alias unpack = withTarget!(stripArchiveExtension, unpackTo); 240 241 static string resolveRedirectImpl(string url) 242 { 243 return net.resolveRedirect(url); 244 } 245 static string resolveRedirect(string url) 246 { 247 alias P = PersistentMemoized!(resolveRedirectImpl, FlushPolicy.atThreadExit); 248 static P* p; 249 if (!p) 250 p = new P(buildPath(installationDirectory, "redirects.json")); 251 return (*p)(url); 252 } 253 254 void windowsOnly() 255 { 256 version(Windows) 257 return; 258 else 259 { 260 log(name ~ " is not installable on this platform."); 261 uninstallable(); 262 } 263 } 264 265 void uninstallable() 266 { 267 throw new Exception("Please install " ~ name ~ " and make sure it is on your PATH."); 268 } 269 } 270 271 /// Move a directory and its contents into another directory recursively, 272 /// overwriting any existing files. 273 package void moveInto(string source, string target) 274 { 275 foreach (de; source.dirEntries(SpanMode.shallow)) 276 { 277 auto targetPath = target.buildPath(de.baseName); 278 if (de.isDir && targetPath.exists) 279 de.moveInto(targetPath); 280 else 281 de.name.rename(targetPath); 282 } 283 source.rmdir(); 284 } 285 286 /// As above, but do not leave behind partially-merged 287 /// directories. In case of failure, both source and target 288 /// are deleted. 289 package void atomicMoveInto(string source, string target) 290 { 291 auto tmpSource = source ~ ".tmp"; 292 auto tmpTarget = target ~ ".tmp"; 293 if (tmpSource.exists) tmpSource.rmdirRecurse(); 294 if (tmpTarget.exists) tmpTarget.rmdirRecurse(); 295 source.rename(tmpSource); 296 target.rename(tmpTarget); 297 { 298 scope(failure) tmpSource.rmdirRecurse(); 299 scope(failure) tmpTarget.rmdirRecurse(); 300 tmpSource.moveInto(tmpTarget); 301 } 302 tmpTarget.rename(target); 303 }