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