[
https://issues.apache.org/jira/browse/CB-11117?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=15250434#comment-15250434
]
ASF GitHub Bot commented on CB-11117:
-------------------------------------
Github user jasongin commented on a diff in the pull request:
https://github.com/apache/cordova-lib/pull/429#discussion_r60460847
--- Diff: cordova-common/src/FileUpdater.js ---
@@ -0,0 +1,389 @@
+/**
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+*/
+
+"use strict";
+
+var fs = require("fs");
+var path = require("path");
+var shell = require("shelljs");
+var minimatch = require("minimatch");
+
+/**
+ * Updates a target file or directory with a source file or directory.
(Directory updates are
+ * not recursive.) Stats for target and source items must be passed in.
This is an internal
+ * helper function used by other methods in this module.
+ *
+ * @param {string|null} rootDir Root directory (such as a project) to
which target and source
+ * path parameters are relative, or null if the paths are absolute.
The rootDir is omitted
+ * from any logged paths, to make the logs easier to read.
+ * @param {string} targetPath Destination file or directory to be updated.
If it does not exist,
+ * it will be created.
+ * @param {fs.Stats|null} targetStats An instance of fs.Stats for the
target path, or null if
+ * the target does not exist.
+ * @param {string|null} sourcePath Source file or directory to be used to
update the
+ * destination. If the source is null, then the destination is deleted
if it exists.
+ * @param {fs.Stats|null} sourceStats An instance of fs.Stats for the
source path, or null if
+ * the source does not exist.
+ * @param {boolean} force If target and source are both files, and the
force flag is not
+ * set, then the file will not be copied unless the source is newer
than the target.
+ * @param {function} [log] Optional logging callback that takes a string
message describing any
+ * file operations that are performed.
+ * @return {boolean} true if any changes were made, or false if the force
flag is not set
+ * and everything was up to date
+ */
+function updatePathWithStats(
+ rootDir, targetPath, targetStats, sourcePath, sourceStats, force,
log) {
+ log = log || function(message) { };
+ var updated = false;
+
+ var targetFullPath = path.join(rootDir || "", targetPath);
+
+ if (sourceStats) {
+ var sourceFullPath = path.join(rootDir || "", sourcePath);
+
+ if (targetStats) {
+ // The target exists. But if the directory status doesn't
match the source, delete it.
+ if (targetStats.isDirectory() && !sourceStats.isDirectory()) {
+ log("rmdir " + targetPath + " (source is a file)");
+ shell.rm("-rf", targetFullPath);
+ targetStats = null;
+ updated = true;
+ } else if (!targetStats.isDirectory() &&
sourceStats.isDirectory()) {
+ log("delete " + targetPath + " (source is a directory)");
+ shell.rm("-f", targetFullPath);
+ targetStats = null;
+ updated = true;
+ }
+ }
+
+ if (!targetStats) {
+ if (sourceStats.isDirectory()) {
+ // The target directory does not exist, so it should be
created.
+ log("mkdir " + targetPath);
+ shell.mkdir("-p", targetFullPath);
+ updated = true;
+ } else if (sourceStats.isFile()) {
+ // The target file does not exist, so it should be copied
from the source.
+ log("copy " + sourcePath + " " + targetPath +
+ (!force ? " (new file)" : ""));
+ shell.cp("-f", sourceFullPath, targetFullPath);
+ updated = true;
+ }
+ } else if (sourceStats.isFile() && targetStats.isFile() &&
+ (force || sourceStats.mtime > targetStats.mtime)) {
+ // When the source and target paths both exist and are files,
update
+ // the file if the source is newer or if doing a forced update.
+ log("copy " + sourcePath + " " + targetPath +
+ (!force ? " (updated file)" : ""));
+ shell.cp("-f", sourceFullPath, targetFullPath);
+ updated = true;
+ }
+ } else if (targetStats) {
+ // The target exists but the source is null, so the target should
be deleted.
+ if (targetStats.isDirectory()) {
+ log("rmdir " + targetPath + " (no source)");
+ shell.rm("-rf", targetFullPath);
+ } else {
+ log("delete " + targetPath + " (no source)");
+ shell.rm("-f", targetFullPath);
+ }
+ updated = true;
+ }
+
+ return updated;
+}
+
+/**
+ * Updates a target file or directory with a source file or directory.
(Directory updates are
+ * not recursive.)
+ *
+ * @param {string|null} rootDir Root directory (such as a project) to
which target and source
+ * path parameters are relative, or null if the paths are absolute.
The rootDir is omitted
+ * from any logged paths, to make the logs easier to read.
+ * @param {string} targetPath Destination file or directory to be updated.
If it does not exist,
+ * it will be created.
+ * @param {string|null} sourcePath Source file or directory to be used to
update the
+ * destination. If the source is null, then the destination is deleted
if it exists.
+ * @param {boolean} force If target and source are both files, and the
force flag is not
+ * set, then the file will not be copied unless the source is newer
than the target.
+ * @param {function} [log] Optional logging callback that takes a string
message describing any
+ * file operations that are performed.
+ * @return {boolean} true if any changes were made, or false if the force
flag is not set
+ * and everything was up to date
+ */
+function updatePath(rootDir, targetPath, sourcePath, force, log) {
+ rootDir = rootDir || "";
+ if (typeof(rootDir) !== "string") {
+ throw new Error("A root directory path is required.");
+ }
+
+ if (!targetPath || typeof(targetPath) !== "string") {
+ throw new Error("A target path is required.");
+ }
+
+ if (sourcePath && typeof(sourcePath) !== "string") {
+ throw new Error("A source path (or null) is required.");
+ }
+
+ log = log || function(message) { };
+
+ var targetFullPath = path.join(rootDir, targetPath);
+ var targetStats = fs.existsSync(targetFullPath) ?
fs.statSync(targetFullPath) : null;
+ var sourceStats = null;
+
+ if (sourcePath) {
+ // A non-null source path was specified. It should exist.
+ var sourceFullPath = path.join(rootDir, sourcePath);
+ if (!fs.existsSync(sourceFullPath)) {
+ throw new Error("Source path does not exist: " + sourcePath);
+ }
+
+ sourceStats = fs.statSync(sourceFullPath);
+
+ // Create the target's parent directory if it doesn't exist.
+ var parentDir = path.dirname(targetFullPath);
+ if (!fs.existsSync(parentDir)) {
+ shell.mkdir("-p", parentDir);
+ }
+ }
+
+ return updatePathWithStats(
+ rootDir, targetPath, targetStats, sourcePath, sourceStats, force,
log);
+}
+
+/**
+ * Updates files and directories based on a mapping from target paths to
source paths. Targets
+ * with null sources in the map are deleted.
+ *
+ * @param {string|null} rootDir Root directory (such as a project) to
which target and source
+ * path parameters are relative, or null if the paths are absolute.
The rootDir is omitted
+ * from any logged paths, to make the logs easier to read.
+ * @param {object} pathMap A dictionary mapping from target paths to
source paths.
+ * @param {boolean} force If target and source are both files, and the
force flag is not
+ * set, then the file will not be copied unless the source is newer
than the target.
+ * @param {function} [log] Optional logging callback that takes a string
message describing any
+ * file operations that are performed.
+ * @return {boolean} true if any changes were made, or false if the force
flag is not set
+ * and everything was up to date
+ */
+function updatePaths(rootDir, pathMap, force, log) {
+ if (!pathMap || typeof(pathMap) !== "object") {
+ throw new Error("An object mapping from target paths to source
paths is required.");
+ }
+
+ var updated = false;
+
+ for (var targetPath in pathMap) {
+ var sourcePath = pathMap[targetPath];
+ updated = updatePath(rootDir, targetPath, sourcePath, force, log)
|| updated;
+ }
+
+ return updated;
+}
+
+/**
+ * Updates a target directory with merged files and subdirectories from
source directories.
+ *
+ * @param {string|null} rootDir Root directory (such as a project) to
which target and source
+ * path parameters are relative, or null if the paths are absolute.
The rootDir is omitted
+ * from any logged paths, to make the logs easier to read.
+ * @param {string} targetDir Destination directory to be updated. If it
does not exist, it will be
+ * created. If it exists, newer files from source directories will be
copied over, and files
+ * missing in the source directories will be deleted.
+ * @param {string|string[]} sourceDirs Source directory or array of source
directories to be
+ * merged into the target. The directories are listed in order of
precedence; files in
+ * directories later in the array supersede files in directories
earlier in the array
+ * (regardless of timestamps).
+ * @param {string|string[]|null} include Optional glob string or array of
glob strings that are
+ * tested against both target and source relative paths to determine
if they are include in
+ * the merge-and-update. If null, all items are included.
+ * @param {string|string[]|null} exclude Optional glob string or array of
glob strings that are
+ * tested against both target and source relative paths to determine
if they are excluded
+ * from the merge-and-update. Exclusions override inclusions. If null,
no items are excluded.
+ * @param {boolean} force If target and source are both files, and the
force flag is not
+ * set, then the file will not be copied unless the source is newer
than the target.
+ * @param {function} [log] Optional logging callback that takes a string
message describing any
+ * file operations that are performed.
+ * @return {boolean} true if any changes were made, or false if the force
flag is not set
+ * and everything was up to date
+ */
+function mergeAndUpdateDir(rootDir, targetDir, sourceDirs, include,
exclude, force, log) {
+ rootDir = rootDir || "";
+ if (typeof(rootDir) !== "string") {
+ throw new Error("A root directory path (or null) is required.");
+ }
+
+ if (!targetDir || typeof(targetDir) !== "string") {
+ throw new Error("A target directory path is required.");
+ }
+
+ if (typeof(sourceDirs) === "string") {
+ sourceDirs = [ sourceDirs ];
+ } else if (!Array.isArray(sourceDirs) || sourceDirs.length === 0) {
+ throw new Error("A source directory path or array of paths is
required.");
+ }
+
+ if (!include) {
+ include = [ "**" ];
+ } else if (typeof (include) === "string") {
+ include = [ include ];
+ } else if (!Array.isArray(include)) {
+ throw new Error("Include parameter must be a glob string or array
of glob strings.");
+ }
+
+ if (!exclude) {
+ exclude = [];
+ } else if (typeof (exclude) === "string") {
+ exclude = [ exclude ];
+ } else if (!Array.isArray(exclude)) {
+ throw new Error("Exclude parameter must be a glob string or array
of glob strings.");
+ }
+
+ // Scan the files in the target directory, if it exists.
+ var targetMap = {};
+ var targetFullPath = path.join(rootDir, targetDir);
+ if (fs.existsSync(targetFullPath)) {
+ targetMap = mapDirectory(rootDir, targetDir, include, exclude);
+ }
+
+ // Scan the files in each of the source directories.
+ var sourceMaps = [];
+ for (var i in sourceDirs) {
+ var sourceFullPath = path.join(rootDir, sourceDirs[i]);
+ if (!fs.existsSync(sourceFullPath)) {
+ throw new Error("Source directory does not exist: " +
sourceDirs[i]);
+ }
+ sourceMaps[i] = mapDirectory(rootDir, sourceDirs[i], include,
exclude);
+ }
+
+ var pathMap = mergePathMaps(targetDir, targetMap, sourceMaps);
+
+ var updated = false;
+
+ // Iterate in sorted order to ensure directories are created before
files under them.
+ Object.keys(pathMap).sort().forEach(function (subPath) {
+ var entry = pathMap[subPath];
+ updated = updatePathWithStats(
+ rootDir,
+ entry.targetPath,
+ entry.targetStats,
+ entry.sourcePath,
+ entry.sourceStats,
+ force,
+ log) || updated;
+ });
+
+ return updated;
+}
+
+/**
+ * Creates a dictionary map of all files and directories under a path.
+ */
+function mapDirectory(rootDir, subDir, include, exclude) {
+ var dirMap = { "": { subDir: subDir, stats:
fs.statSync(path.join(rootDir, subDir)) } };
+ mapSubdirectory(rootDir, subDir, "", include, exclude, dirMap);
+ return dirMap;
+
+ function mapSubdirectory(rootDir, subDir, relativeDir, include,
exclude, dirMap) {
+ var itemMapped = false;
+ var items = fs.readdirSync(path.join(rootDir, subDir,
relativeDir));
+ for (var i in items) {
+ var relativePath = path.join(relativeDir, items[i]);
+
+ // Skip any files or directories (and everything under) that
match an exclude glob.
+ if (matchGlobArray(relativePath, exclude)) {
+ continue;
+ }
+
+ // Stats obtained here (required at least to know where to
recurse in directories)
+ // are saved for later, where the modified times may also be
used. This minimizes
+ // the number of file I/O operations performed.
+ var fullPath = path.join(rootDir, subDir, relativePath);
+ var stats = fs.statSync(fullPath);
+
+ // Directories are included if either something under them is
included or they
+ // match an include glob. Files are included only if they
match an include glob.
+ if (stats.isDirectory() ?
+ (mapSubdirectory(rootDir, subDir, relativePath,
include, exclude, dirMap) ||
--- End diff --
I wrote it as in if..else originally, but then the two lines inside the
block below were duplicated. But I agree this is very hard to read as it is
now, so maybe the duplication is not as bad.
---
In reply to:
[60406393](https://github.com/apache/cordova-lib/pull/429#discussion_r60406393)
[](ancestors = 60406393)
> Preparing platforms should skip copying files which haven't changed
> -------------------------------------------------------------------
>
> Key: CB-11117
> URL: https://issues.apache.org/jira/browse/CB-11117
> Project: Apache Cordova
> Issue Type: Improvement
> Components: Android, iOS, Windows
> Reporter: Jason Ginchereau
> Assignee: Jason Ginchereau
>
> Many cordova CLI commands include a "prepare" operation, including 'cordova
> build', 'cordova run', 'cordova plugin add', and more. Every time each of
> those commands runs, the target platform is "prepared", which involves
> copying all files from the [<project>/www,
> <project>/platforms/<platform>/platform_www, <project>/merges/<platform>] to
> the platform's target www folder, as well as copying a bunch of icons and
> splash screens to platform-specific locations.
> For the very first prepare of a platform, all that file copying is necessary.
> But most of the time after that most of the files being copied have not
> changed and therefore don't really need to be copied again. So the typical
> developer inner loop (edit a few source files, build and run the app, repeat)
> is a lot slower than it could be for a Cordova project, especially one that
> includes a significant number of source files or resources.
> Instead, Cordova should be smart enough to skip copying of files that haven't
> changed, based on their last-modified timestamp. (But also there should still
> be a way to force a clean/full/non-incremental build if desired.)
--
This message was sent by Atlassian JIRA
(v6.3.4#6332)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]