| OLD | NEW |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file | 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a | 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. | 3 // BSD-style license that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import 'dart:async'; | 5 import 'dart:async'; |
| 6 import 'dart:io'; | 6 import 'dart:io'; |
| 7 | 7 |
| 8 import 'package:path/path.dart' as p; | 8 import 'package:path/path.dart' as p; |
| 9 import 'package:barback/barback.dart'; | 9 import 'package:barback/barback.dart'; |
| 10 import 'package:pub_semver/pub_semver.dart'; | 10 import 'package:pub_semver/pub_semver.dart'; |
| 11 | 11 |
| 12 import 'barback/asset_environment.dart'; | 12 import 'barback/asset_environment.dart'; |
| 13 import 'entrypoint.dart'; | 13 import 'entrypoint.dart'; |
| 14 import 'exceptions.dart'; | 14 import 'exceptions.dart'; |
| 15 import 'executable.dart' as exe; | 15 import 'executable.dart' as exe; |
| 16 import 'io.dart'; | 16 import 'io.dart'; |
| 17 import 'lock_file.dart'; | 17 import 'lock_file.dart'; |
| 18 import 'log.dart' as log; | 18 import 'log.dart' as log; |
| 19 import 'package.dart'; | 19 import 'package.dart'; |
| 20 import 'pubspec.dart'; | 20 import 'pubspec.dart'; |
| 21 import 'sdk.dart' as sdk; | 21 import 'sdk.dart' as sdk; |
| 22 import 'solver/version_solver.dart'; | 22 import 'solver/version_solver.dart'; |
| 23 import 'source/cached.dart'; | 23 import 'source/cached.dart'; |
| 24 import 'source/git.dart'; | |
| 25 import 'source/hosted.dart'; | |
| 26 import 'source/path.dart'; | |
| 27 import 'system_cache.dart'; | 24 import 'system_cache.dart'; |
| 28 import 'utils.dart'; | 25 import 'utils.dart'; |
| 29 | 26 |
| 30 /// Maintains the set of packages that have been globally activated. | 27 /// Maintains the set of packages that have been globally activated. |
| 31 /// | 28 /// |
| 32 /// These have been hand-chosen by the user to make their executables in bin/ | 29 /// These have been hand-chosen by the user to make their executables in bin/ |
| 33 /// available to the entire system. This lets them access them even when the | 30 /// available to the entire system. This lets them access them even when the |
| 34 /// current working directory is not inside another entrypoint package. | 31 /// current working directory is not inside another entrypoint package. |
| 35 /// | 32 /// |
| 36 /// Only one version of a given package name can be globally activated at a | 33 /// Only one version of a given package name can be globally activated at a |
| (...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 73 /// | 70 /// |
| 74 /// [executables] is the names of the executables that should have binstubs. | 71 /// [executables] is the names of the executables that should have binstubs. |
| 75 /// If `null`, all executables in the package will get binstubs. If empty, no | 72 /// If `null`, all executables in the package will get binstubs. If empty, no |
| 76 /// binstubs will be created. | 73 /// binstubs will be created. |
| 77 /// | 74 /// |
| 78 /// if [overwriteBinStubs] is `true`, any binstubs that collide with | 75 /// if [overwriteBinStubs] is `true`, any binstubs that collide with |
| 79 /// existing binstubs in other packages will be overwritten by this one's. | 76 /// existing binstubs in other packages will be overwritten by this one's. |
| 80 /// Otherwise, the previous ones will be preserved. | 77 /// Otherwise, the previous ones will be preserved. |
| 81 Future activateGit(String repo, List<String> executables, | 78 Future activateGit(String repo, List<String> executables, |
| 82 {bool overwriteBinStubs}) async { | 79 {bool overwriteBinStubs}) async { |
| 83 var source = cache.sources["git"] as GitSource; | 80 var name = await cache.git.getPackageNameFromRepo(repo); |
| 84 var name = await source.getPackageNameFromRepo(repo); | |
| 85 // Call this just to log what the current active package is, if any. | 81 // Call this just to log what the current active package is, if any. |
| 86 _describeActive(name); | 82 _describeActive(name); |
| 87 | 83 |
| 88 // TODO(nweiz): Add some special handling for git repos that contain path | 84 // TODO(nweiz): Add some special handling for git repos that contain path |
| 89 // dependencies. Their executables shouldn't be cached, and there should | 85 // dependencies. Their executables shouldn't be cached, and there should |
| 90 // be a mechanism for redoing dependency resolution if a path pubspec has | 86 // be a mechanism for redoing dependency resolution if a path pubspec has |
| 91 // changed (see also issue 20499). | 87 // changed (see also issue 20499). |
| 92 await _installInCache( | 88 await _installInCache( |
| 93 GitSource.refFor(name, repo).withConstraint(VersionConstraint.any), | 89 cache.git.source.refFor(name, repo) |
| 90 .withConstraint(VersionConstraint.any), |
| 94 executables, overwriteBinStubs: overwriteBinStubs); | 91 executables, overwriteBinStubs: overwriteBinStubs); |
| 95 } | 92 } |
| 96 | 93 |
| 97 /// Finds the latest version of the hosted package with [name] that matches | 94 /// Finds the latest version of the hosted package with [name] that matches |
| 98 /// [constraint] and makes it the active global version. | 95 /// [constraint] and makes it the active global version. |
| 99 /// | 96 /// |
| 100 /// [executables] is the names of the executables that should have binstubs. | 97 /// [executables] is the names of the executables that should have binstubs. |
| 101 /// If `null`, all executables in the package will get binstubs. If empty, no | 98 /// If `null`, all executables in the package will get binstubs. If empty, no |
| 102 /// binstubs will be created. | 99 /// binstubs will be created. |
| 103 /// | 100 /// |
| 104 /// if [overwriteBinStubs] is `true`, any binstubs that collide with | 101 /// if [overwriteBinStubs] is `true`, any binstubs that collide with |
| 105 /// existing binstubs in other packages will be overwritten by this one's. | 102 /// existing binstubs in other packages will be overwritten by this one's. |
| 106 /// Otherwise, the previous ones will be preserved. | 103 /// Otherwise, the previous ones will be preserved. |
| 107 Future activateHosted(String name, VersionConstraint constraint, | 104 Future activateHosted(String name, VersionConstraint constraint, |
| 108 List<String> executables, {bool overwriteBinStubs}) async { | 105 List<String> executables, {bool overwriteBinStubs}) async { |
| 109 _describeActive(name); | 106 _describeActive(name); |
| 110 await _installInCache(HostedSource.refFor(name).withConstraint(constraint), | 107 await _installInCache( |
| 111 executables, overwriteBinStubs: overwriteBinStubs); | 108 cache.hosted.source.refFor(name).withConstraint(constraint), |
| 109 executables, |
| 110 overwriteBinStubs: overwriteBinStubs); |
| 112 } | 111 } |
| 113 | 112 |
| 114 /// Makes the local package at [path] globally active. | 113 /// Makes the local package at [path] globally active. |
| 115 /// | 114 /// |
| 116 /// [executables] is the names of the executables that should have binstubs. | 115 /// [executables] is the names of the executables that should have binstubs. |
| 117 /// If `null`, all executables in the package will get binstubs. If empty, no | 116 /// If `null`, all executables in the package will get binstubs. If empty, no |
| 118 /// binstubs will be created. | 117 /// binstubs will be created. |
| 119 /// | 118 /// |
| 120 /// if [overwriteBinStubs] is `true`, any binstubs that collide with | 119 /// if [overwriteBinStubs] is `true`, any binstubs that collide with |
| 121 /// existing binstubs in other packages will be overwritten by this one's. | 120 /// existing binstubs in other packages will be overwritten by this one's. |
| 122 /// Otherwise, the previous ones will be preserved. | 121 /// Otherwise, the previous ones will be preserved. |
| 123 Future activatePath(String path, List<String> executables, | 122 Future activatePath(String path, List<String> executables, |
| 124 {bool overwriteBinStubs}) async { | 123 {bool overwriteBinStubs}) async { |
| 125 var entrypoint = new Entrypoint(path, cache, isGlobal: true); | 124 var entrypoint = new Entrypoint(path, cache, isGlobal: true); |
| 126 | 125 |
| 127 // Get the package's dependencies. | 126 // Get the package's dependencies. |
| 128 await entrypoint.acquireDependencies(SolveType.GET); | 127 await entrypoint.acquireDependencies(SolveType.GET); |
| 129 var name = entrypoint.root.name; | 128 var name = entrypoint.root.name; |
| 130 | 129 |
| 131 // Call this just to log what the current active package is, if any. | 130 // Call this just to log what the current active package is, if any. |
| 132 _describeActive(name); | 131 _describeActive(name); |
| 133 | 132 |
| 134 // Write a lockfile that points to the local package. | 133 // Write a lockfile that points to the local package. |
| 135 var fullPath = canonicalize(entrypoint.root.dir); | 134 var fullPath = canonicalize(entrypoint.root.dir); |
| 136 var id = PathSource.idFor(name, entrypoint.root.version, fullPath); | 135 var id = cache.path.source.idFor(name, entrypoint.root.version, fullPath); |
| 137 | 136 |
| 138 // TODO(rnystrom): Look in "bin" and display list of binaries that | 137 // TODO(rnystrom): Look in "bin" and display list of binaries that |
| 139 // user can run. | 138 // user can run. |
| 140 _writeLockFile(name, new LockFile([id], cache.sources)); | 139 _writeLockFile(name, new LockFile([id], cache.sources)); |
| 141 | 140 |
| 142 var binDir = p.join(_directory, name, 'bin'); | 141 var binDir = p.join(_directory, name, 'bin'); |
| 143 if (dirExists(binDir)) deleteEntry(binDir); | 142 if (dirExists(binDir)) deleteEntry(binDir); |
| 144 | 143 |
| 145 _updateBinStubs(entrypoint.root, executables, | 144 _updateBinStubs(entrypoint.root, executables, |
| 146 overwriteBinStubs: overwriteBinStubs); | 145 overwriteBinStubs: overwriteBinStubs); |
| 147 } | 146 } |
| 148 | 147 |
| 149 /// Installs the package [dep] and its dependencies into the system cache. | 148 /// Installs the package [dep] and its dependencies into the system cache. |
| 150 Future _installInCache(PackageDep dep, List<String> executables, | 149 Future _installInCache(PackageDep dep, List<String> executables, |
| 151 {bool overwriteBinStubs}) async { | 150 {bool overwriteBinStubs}) async { |
| 152 // Create a dummy package with just [dep] so we can do resolution on it. | 151 // Create a dummy package with just [dep] so we can do resolution on it. |
| 153 var root = new Package.inMemory(new Pubspec("pub global activate", | 152 var root = new Package.inMemory(new Pubspec("pub global activate", |
| 154 dependencies: [dep], sources: cache.sources)); | 153 dependencies: [dep], sources: cache.sources)); |
| 155 | 154 |
| 156 // Resolve it and download its dependencies. | 155 // Resolve it and download its dependencies. |
| 157 var result = await resolveVersions(SolveType.GET, cache.sources, root); | 156 var result = await resolveVersions(SolveType.GET, cache, root); |
| 158 if (!result.succeeded) { | 157 if (!result.succeeded) { |
| 159 // If the package specified by the user doesn't exist, we want to | 158 // If the package specified by the user doesn't exist, we want to |
| 160 // surface that as a [DataError] with the associated exit code. | 159 // surface that as a [DataError] with the associated exit code. |
| 161 if (result.error.package != dep.name) throw result.error; | 160 if (result.error.package != dep.name) throw result.error; |
| 162 if (result.error is NoVersionException) dataError(result.error.message); | 161 if (result.error is NoVersionException) dataError(result.error.message); |
| 163 throw result.error; | 162 throw result.error; |
| 164 } | 163 } |
| 165 result.showReport(SolveType.GET); | 164 result.showReport(SolveType.GET); |
| 166 | 165 |
| 167 // Make sure all of the dependencies are locally installed. | 166 // Make sure all of the dependencies are locally installed. |
| 168 await Future.wait(result.packages.map(_cacheDependency)); | 167 await Future.wait(result.packages.map(_cacheDependency)); |
| 169 | 168 |
| 170 // Load the package graph from [result] so we don't need to re-parse all | 169 // Load the package graph from [result] so we don't need to re-parse all |
| 171 // the pubspecs. | 170 // the pubspecs. |
| 172 var entrypoint = new Entrypoint.fromSolveResult(root, cache, result, | 171 var entrypoint = new Entrypoint.fromSolveResult(root, cache, result, |
| 173 isGlobal: true); | 172 isGlobal: true); |
| 174 var snapshots = await _precompileExecutables(entrypoint, dep.name); | 173 var snapshots = await _precompileExecutables(entrypoint, dep.name); |
| 175 | 174 |
| 176 var lockFile = result.lockFile; | 175 var lockFile = result.lockFile; |
| 177 _writeLockFile(dep.name, lockFile); | 176 _writeLockFile(dep.name, lockFile); |
| 178 writeTextFile(_getPackagesFilePath(dep.name), lockFile.packagesFile()); | 177 writeTextFile(_getPackagesFilePath(dep.name), lockFile.packagesFile(cache)); |
| 179 | 178 |
| 180 _updateBinStubs(entrypoint.packageGraph.packages[dep.name], executables, | 179 _updateBinStubs(entrypoint.packageGraph.packages[dep.name], executables, |
| 181 overwriteBinStubs: overwriteBinStubs, snapshots: snapshots); | 180 overwriteBinStubs: overwriteBinStubs, snapshots: snapshots); |
| 182 } | 181 } |
| 183 | 182 |
| 184 /// Precompiles the executables for [package] and saves them in the global | 183 /// Precompiles the executables for [package] and saves them in the global |
| 185 /// cache. | 184 /// cache. |
| 186 /// | 185 /// |
| 187 /// Returns a map from executable name to path for the snapshots that were | 186 /// Returns a map from executable name to path for the snapshots that were |
| 188 /// successfully precompiled. | 187 /// successfully precompiled. |
| (...skipping 12 matching lines...) Expand all Loading... |
| 201 }); | 200 }); |
| 202 | 201 |
| 203 return environment.precompileExecutables(package, binDir); | 202 return environment.precompileExecutables(package, binDir); |
| 204 }); | 203 }); |
| 205 } | 204 } |
| 206 | 205 |
| 207 /// Downloads [id] into the system cache if it's a cached package. | 206 /// Downloads [id] into the system cache if it's a cached package. |
| 208 Future _cacheDependency(PackageId id) async { | 207 Future _cacheDependency(PackageId id) async { |
| 209 if (id.isRoot) return; | 208 if (id.isRoot) return; |
| 210 | 209 |
| 211 var source = cache.sources[id.source]; | 210 var source = cache.source(id.source); |
| 212 if (source is! CachedSource) return; | 211 if (source is! CachedSource) return; |
| 213 | 212 |
| 214 await source.downloadToSystemCache(id); | 213 await source.downloadToSystemCache(id); |
| 215 } | 214 } |
| 216 | 215 |
| 217 /// Finishes activating package [package] by saving [lockFile] in the cache. | 216 /// Finishes activating package [package] by saving [lockFile] in the cache. |
| 218 void _writeLockFile(String package, LockFile lockFile) { | 217 void _writeLockFile(String package, LockFile lockFile) { |
| 219 ensureDir(p.join(_directory, package)); | 218 ensureDir(p.join(_directory, package)); |
| 220 | 219 |
| 221 // TODO(nweiz): This cleans up Dart 1.6's old lockfile location. Remove it | 220 // TODO(nweiz): This cleans up Dart 1.6's old lockfile location. Remove it |
| 222 // when Dart 1.6 is old enough that we don't think anyone will have these | 221 // when Dart 1.6 is old enough that we don't think anyone will have these |
| 223 // lockfiles anymore (issue 20703). | 222 // lockfiles anymore (issue 20703). |
| 224 var oldPath = p.join(_directory, "$package.lock"); | 223 var oldPath = p.join(_directory, "$package.lock"); |
| 225 if (fileExists(oldPath)) deleteEntry(oldPath); | 224 if (fileExists(oldPath)) deleteEntry(oldPath); |
| 226 | 225 |
| 227 writeTextFile(_getLockFilePath(package), lockFile.serialize(cache.rootDir)); | 226 writeTextFile(_getLockFilePath(package), lockFile.serialize(cache.rootDir)); |
| 228 | 227 |
| 229 var id = lockFile.packages[package]; | 228 var id = lockFile.packages[package]; |
| 230 log.message('Activated ${_formatPackage(id)}.'); | 229 log.message('Activated ${_formatPackage(id)}.'); |
| 231 } | 230 } |
| 232 | 231 |
| 233 /// Shows the user the currently active package with [name], if any. | 232 /// Shows the user the currently active package with [name], if any. |
| 234 void _describeActive(String name) { | 233 void _describeActive(String name) { |
| 235 try { | 234 try { |
| 236 var lockFile = new LockFile.load(_getLockFilePath(name), cache.sources); | 235 var lockFile = new LockFile.load(_getLockFilePath(name), cache.sources); |
| 237 var id = lockFile.packages[name]; | 236 var id = lockFile.packages[name]; |
| 238 | 237 |
| 239 if (id.source == 'git') { | 238 if (id.source == 'git') { |
| 240 var url = GitSource.urlFromDescription(id.description); | 239 var url = cache.git.source.urlFromDescription(id.description); |
| 241 log.message('Package ${log.bold(name)} is currently active from Git ' | 240 log.message('Package ${log.bold(name)} is currently active from Git ' |
| 242 'repository "${url}".'); | 241 'repository "${url}".'); |
| 243 } else if (id.source == 'path') { | 242 } else if (id.source == 'path') { |
| 244 var path = PathSource.pathFromDescription(id.description); | 243 var path = cache.path.source.pathFromDescription(id.description); |
| 245 log.message('Package ${log.bold(name)} is currently active at path ' | 244 log.message('Package ${log.bold(name)} is currently active at path ' |
| 246 '"$path".'); | 245 '"$path".'); |
| 247 } else { | 246 } else { |
| 248 log.message('Package ${log.bold(name)} is currently active at version ' | 247 log.message('Package ${log.bold(name)} is currently active at version ' |
| 249 '${log.bold(id.version)}.'); | 248 '${log.bold(id.version)}.'); |
| 250 } | 249 } |
| 251 } on IOException { | 250 } on IOException { |
| 252 // If we couldn't read the lock file, it's not activated. | 251 // If we couldn't read the lock file, it's not activated. |
| 253 return null; | 252 return null; |
| 254 } | 253 } |
| (...skipping 41 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 296 ensureDir(p.dirname(lockFilePath)); | 295 ensureDir(p.dirname(lockFilePath)); |
| 297 new File(oldLockFilePath).renameSync(lockFilePath); | 296 new File(oldLockFilePath).renameSync(lockFilePath); |
| 298 } | 297 } |
| 299 | 298 |
| 300 // Remove the package itself from the lockfile. We put it in there so we | 299 // Remove the package itself from the lockfile. We put it in there so we |
| 301 // could find and load the [Package] object, but normally an entrypoint | 300 // could find and load the [Package] object, but normally an entrypoint |
| 302 // doesn't expect to be in its own lockfile. | 301 // doesn't expect to be in its own lockfile. |
| 303 var id = lockFile.packages[name]; | 302 var id = lockFile.packages[name]; |
| 304 lockFile = lockFile.removePackage(name); | 303 lockFile = lockFile.removePackage(name); |
| 305 | 304 |
| 306 var source = cache.sources[id.source]; | 305 var source = cache.source(id.source); |
| 307 var entrypoint; | 306 var entrypoint; |
| 308 if (source is CachedSource) { | 307 if (source is CachedSource) { |
| 309 // For cached sources, the package itself is in the cache and the | 308 // For cached sources, the package itself is in the cache and the |
| 310 // lockfile is the one we just loaded. | 309 // lockfile is the one we just loaded. |
| 311 entrypoint = new Entrypoint.inMemory( | 310 entrypoint = new Entrypoint.inMemory( |
| 312 cache.sources.load(id), lockFile, cache, isGlobal: true); | 311 cache.load(id), lockFile, cache, isGlobal: true); |
| 313 } else { | 312 } else { |
| 314 // For uncached sources (i.e. path), the ID just points to the real | 313 // For uncached sources (i.e. path), the ID just points to the real |
| 315 // directory for the package. | 314 // directory for the package. |
| 316 assert(id.source == "path"); | 315 assert(id.source == "path"); |
| 317 entrypoint = new Entrypoint( | 316 entrypoint = new Entrypoint( |
| 318 PathSource.pathFromDescription(id.description), cache, | 317 cache.path.source.pathFromDescription(id.description), cache, |
| 319 isGlobal: true); | 318 isGlobal: true); |
| 320 } | 319 } |
| 321 | 320 |
| 322 if (entrypoint.root.pubspec.environment.sdkVersion.allows(sdk.version)) { | 321 if (entrypoint.root.pubspec.environment.sdkVersion.allows(sdk.version)) { |
| 323 return entrypoint; | 322 return entrypoint; |
| 324 } | 323 } |
| 325 | 324 |
| 326 dataError("${log.bold(name)} ${entrypoint.root.version} doesn't support " | 325 dataError("${log.bold(name)} ${entrypoint.root.version} doesn't support " |
| 327 "Dart ${sdk.version}."); | 326 "Dart ${sdk.version}."); |
| 328 } | 327 } |
| (...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 401 throw new FormatException("Pubspec for activated package $name didn't " | 400 throw new FormatException("Pubspec for activated package $name didn't " |
| 402 "contain an entry for itself."); | 401 "contain an entry for itself."); |
| 403 } | 402 } |
| 404 | 403 |
| 405 return id; | 404 return id; |
| 406 } | 405 } |
| 407 | 406 |
| 408 /// Returns formatted string representing the package [id]. | 407 /// Returns formatted string representing the package [id]. |
| 409 String _formatPackage(PackageId id) { | 408 String _formatPackage(PackageId id) { |
| 410 if (id.source == 'git') { | 409 if (id.source == 'git') { |
| 411 var url = GitSource.urlFromDescription(id.description); | 410 var url = cache.sources.git.urlFromDescription(id.description); |
| 412 return '${log.bold(id.name)} ${id.version} from Git repository "$url"'; | 411 return '${log.bold(id.name)} ${id.version} from Git repository "$url"'; |
| 413 } else if (id.source == 'path') { | 412 } else if (id.source == 'path') { |
| 414 var path = PathSource.pathFromDescription(id.description); | 413 var path = cache.sources.path.pathFromDescription(id.description); |
| 415 return '${log.bold(id.name)} ${id.version} at path "$path"'; | 414 return '${log.bold(id.name)} ${id.version} at path "$path"'; |
| 416 } else { | 415 } else { |
| 417 return '${log.bold(id.name)} ${id.version}'; | 416 return '${log.bold(id.name)} ${id.version}'; |
| 418 } | 417 } |
| 419 } | 418 } |
| 420 | 419 |
| 421 /// Repairs any corrupted globally-activated packages and their binstubs. | 420 /// Repairs any corrupted globally-activated packages and their binstubs. |
| 422 /// | 421 /// |
| 423 /// Returns a pair of two lists of strings. The first indicates which packages | 422 /// Returns a pair of two lists of strings. The first indicates which packages |
| 424 /// were successfully re-activated; the second indicates which failed. | 423 /// were successfully re-activated; the second indicates which failed. |
| (...skipping 363 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 788 } | 787 } |
| 789 | 788 |
| 790 /// Returns the value of the property named [name] in the bin stub script | 789 /// Returns the value of the property named [name] in the bin stub script |
| 791 /// [source]. | 790 /// [source]. |
| 792 String _binStubProperty(String source, String name) { | 791 String _binStubProperty(String source, String name) { |
| 793 var pattern = new RegExp(quoteRegExp(name) + r": ([a-zA-Z0-9_-]+)"); | 792 var pattern = new RegExp(quoteRegExp(name) + r": ([a-zA-Z0-9_-]+)"); |
| 794 var match = pattern.firstMatch(source); | 793 var match = pattern.firstMatch(source); |
| 795 return match == null ? null : match[1]; | 794 return match == null ? null : match[1]; |
| 796 } | 795 } |
| 797 } | 796 } |
| OLD | NEW |