|
1 | 1 | const fs = require('fs'); |
2 | 2 | const path = require('path'); |
| 3 | +const xml2js = require('xml2js'); |
3 | 4 |
|
4 | 5 | const azureSourceFolder = process.argv[2]; |
5 | 6 | const newDeps = process.argv[3]; |
6 | 7 | const unifiedDepsPath = path.join(azureSourceFolder, '.nuget', 'externals', 'UnifiedDependencies.xml'); |
7 | 8 | const tfsServerPath = path.join(azureSourceFolder, 'Tfs', 'Service', 'Deploy', 'components', 'TfsServer.Servicing.core.xml'); |
8 | | -const msPrefix = "Mseng.MS.TF.DistributedTask.Tasks."; |
9 | | -const directoryTag = new RegExp('<Directory (.*)>'); |
| 9 | +const msPrefix = 'Mseng.MS.TF.DistributedTask.Tasks.'; |
10 | 10 |
|
11 | | -function formDirectoryString(nugetTaskName) { |
| 11 | +/** |
| 12 | + * Helper function to check if the value is included in the array but not equal to the value in the array |
| 13 | + * E.g. We compared generated task names such as Mseng.MS.TF.DistributedTask.Tasks.AppCenterDistributeV1_Node20 |
| 14 | + * with basic Mseng.MS.TF.DistributedTask.Tasks.AppCenterDistributeV1 |
| 15 | + * If the name is included in the list of dependencies but not equal to the name in the list, we assume its a config |
| 16 | + */ |
| 17 | +const isIncludeButNotEqual = (arr, value) => arr.reduce((acc, item) => acc || (value.includes(item) && value !== item), false); |
| 18 | + |
| 19 | +function formDirectoryTag(nugetTaskName) { |
12 | 20 | const taskName = nugetTaskName.replace(msPrefix, ''); |
13 | | - |
14 | | - return ` <Directory Path="[ServicingDir]Tasks\\Individual\\${taskName}\\"> |
15 | | - <File Origin="nuget://Mseng.MS.TF.DistributedTask.Tasks.${taskName}/content/*" /> |
16 | | - </Directory>`; |
| 21 | + return { |
| 22 | + $: { |
| 23 | + Path: `[ServicingDir]Tasks\\Individual\\${taskName}\\` |
| 24 | + }, |
| 25 | + File: [ |
| 26 | + { |
| 27 | + $: { |
| 28 | + Origin: `nuget://Mseng.MS.TF.DistributedTask.Tasks.${taskName}/content/*` |
| 29 | + } |
| 30 | + } |
| 31 | + ] |
| 32 | + }; |
17 | 33 | } |
18 | 34 |
|
19 | | -function formatDeps(depArr) { |
20 | | - const newDepsDict = {}; |
| 35 | +/** |
| 36 | + * @typedef {Object} Dependencies |
| 37 | + * @property {string} name |
| 38 | + * @property {string} version |
| 39 | + * @property {string} depStr |
| 40 | + */ |
21 | 41 |
|
22 | | - depArr.forEach(newDep => { |
23 | | - // add to dictionary |
| 42 | +/** |
| 43 | + * The function to form a dictionary of dependencies |
| 44 | + * @param {Array} depArr - array of dependencies |
| 45 | + * @returns {Dependencies} - dictionary of dependencies |
| 46 | + */ |
| 47 | +function getDeps(depArr) { |
| 48 | + /** @type {Record<key, Dependencies>} deps */ |
| 49 | + const deps = {}; |
| 50 | + const getDependantConfigs = (arrKeys, packageName) => arrKeys.filter(key => key.includes(packageName) && key !== packageName); |
| 51 | + |
| 52 | + // first run we form structures |
| 53 | + for (let i = 0; i < depArr.length; i++) { |
| 54 | + const newDep = depArr[i]; |
24 | 55 | const depDetails = newDep.split('"'); |
| 56 | + |
25 | 57 | console.log(JSON.stringify(depDetails)); |
26 | 58 | const name = depDetails[1]; |
27 | 59 | const version = depDetails[3]; |
28 | 60 | console.log(name + ' ' + version); |
29 | | - newDepsDict[name] = version; |
30 | | - }); |
31 | 61 |
|
32 | | - return newDepsDict; |
33 | | -} |
| 62 | + if (!deps[name]) deps[name] = {}; |
34 | 63 |
|
35 | | -function findLastIndex(array, predicate) { |
36 | | - let l = array.length; |
| 64 | + const dep = deps[name]; |
37 | 65 |
|
38 | | - while (l--) { |
39 | | - if (predicate(array[l], l, array)) { |
40 | | - return l; |
41 | | - } |
| 66 | + dep.name = name; |
| 67 | + dep.version = version; |
| 68 | + dep.depStr = newDep; |
| 69 | + } |
| 70 | + |
| 71 | + const keys = Object.keys(deps); |
| 72 | + for (let dep in deps) { |
| 73 | + const configs = getDependantConfigs(keys, dep); |
| 74 | + if (!configs.length) continue; |
| 75 | + |
| 76 | + deps[dep].configs = []; |
| 77 | + configs.forEach(config => { |
| 78 | + const configDep = deps[config]; |
| 79 | + deps[dep].configs.push({ |
| 80 | + name: configDep.name, |
| 81 | + version: configDep.version, |
| 82 | + depStr: configDep.depStr |
| 83 | + }); |
| 84 | + |
| 85 | + delete deps[config]; |
| 86 | + }); |
42 | 87 | } |
43 | | - return -1; |
| 88 | + |
| 89 | + return deps; |
44 | 90 | } |
45 | 91 |
|
46 | | -/* Function updating existing deps version and also add new deps with postfix |
47 | | - * Example: If we have dependency with name |
48 | | - * Mseng.MS.TF.DistributedTask.Tasks.AndroidSigningV2 |
49 | | - * It will add Mseng.MS.TF.DistributedTask.Tasks.AndroidSigningV2_Node16 */ |
50 | | -function updateUnifiedDeps(pathToUnifiedDeps, pathToNewUnifiedDeps, outputPath) { |
51 | | - const currentDeps = fs.readFileSync(pathToUnifiedDeps, 'utf8'); |
52 | | - const newDeps = fs.readFileSync(pathToNewUnifiedDeps, 'utf8'); |
| 92 | +/** |
| 93 | + * The function removes all generated configs such as Node16/Node20 from the list of dependencies |
| 94 | + * @param {Array} depsArray - array of parsed dependencies from UnifiedDependencies.xml |
| 95 | + * @param {Object} depsForUpdate - dictionary of dependencies from getDeps method |
| 96 | + * @param {Object} updatedDeps - structure to track added/removed dependencies |
| 97 | + * @returns {Array} - updated array of dependencies and updatedDeps object { added: [], removed: [] |
| 98 | + */ |
| 99 | +function removeConfigsForTasks(depsArray, depsForUpdate, updatedDeps) { |
| 100 | + const newDepsArr = depsArray.slice(); |
| 101 | + const updatedDepsObj = Object.assign({}, updatedDeps); |
| 102 | + const basicDepsForUpdate = Object.keys(depsForUpdate).map(dep => dep.toLowerCase()); |
| 103 | + let index = 0; |
| 104 | + |
| 105 | + while (index < newDepsArr.length) { |
| 106 | + const currentDep = newDepsArr[index]; |
| 107 | + const depDetails = currentDep.split('"'); |
| 108 | + const name = depDetails[1]; |
| 109 | + if (!name) { |
| 110 | + index++; |
| 111 | + continue; |
| 112 | + } |
53 | 113 |
|
54 | | - const currentDepsArr = currentDeps.split('\n'); |
55 | | - const newDepsArr = newDeps.split('\n'); |
56 | | - const newDepsDict = formatDeps(newDepsArr); |
| 114 | + const basicName = name.toLowerCase(); |
| 115 | + |
| 116 | + if (isIncludeButNotEqual(basicDepsForUpdate, basicName)) { |
| 117 | + newDepsArr.splice(index, 1); |
| 118 | + updatedDepsObj.removed.push(name); |
| 119 | + continue; |
| 120 | + } |
| 121 | + |
| 122 | + index++; |
| 123 | + } |
57 | 124 |
|
58 | | - const updatedDeps = []; |
59 | | - // Tasks that was updated and should be presented in TfsServer.Servicing.core.xml |
60 | | - const changedTasks = []; |
| 125 | + return [newDepsArr, updatedDepsObj]; |
| 126 | +} |
61 | 127 |
|
62 | | - currentDepsArr.forEach(currentDep => { |
| 128 | +/** |
| 129 | + * The function updates task dependencies with configs such as Node16/Node20 |
| 130 | + * @param {Array} depsArray - array of parsed dependencies from UnifiedDependencies.xml |
| 131 | + * @param {Object} depsForUpdate - dictionary of dependencies from getDeps method |
| 132 | + * @param {Object} updatedDeps - structure to track added/removed dependencies |
| 133 | + * @returns {Array} - updated array of dependencies and updatedDeps object { added: [], removed: [] |
| 134 | + */ |
| 135 | +function updateConfigsForTasks(depsArray, depsForUpdate, updatedDeps) { |
| 136 | + const newDepsArr = depsArray.slice(); |
| 137 | + const updatedDepsObj = Object.assign({}, updatedDeps); |
| 138 | + const basicDepsForUpdate = new Set(Object.keys(depsForUpdate)); |
| 139 | + let index = 0; |
| 140 | + |
| 141 | + while (index < newDepsArr.length) { |
| 142 | + const currentDep = newDepsArr[index]; |
63 | 143 | const depDetails = currentDep.split('"'); |
64 | 144 | const name = depDetails[1]; |
65 | 145 |
|
66 | | - // find if there is a match in new (ignoring case) |
67 | | - if (name) { |
68 | | - const newDepsKey = Object.keys(newDepsDict).find(key => key.toLowerCase() === name.toLowerCase()); |
69 | | - if (newDepsKey && newDepsDict[newDepsKey]) { |
70 | | - // update the version |
71 | | - depDetails[3] = newDepsDict[newDepsKey]; |
72 | | - updatedDeps.push(depDetails.join('"')); |
73 | | - |
74 | | - changedTasks.push(newDepsKey); |
75 | | - delete newDepsDict[newDepsKey]; |
76 | | - } else { |
77 | | - updatedDeps.push(currentDep); |
78 | | - console.log(`"${currentDep}"`); |
79 | | - } |
80 | | - } else { |
81 | | - updatedDeps.push(currentDep); |
| 146 | + if (!name || !basicDepsForUpdate.has(name)) { |
| 147 | + index++; |
| 148 | + continue; |
82 | 149 | } |
83 | | - }); |
84 | | - |
85 | | - // add the new deps from the start |
86 | | - // working only for generated deps |
87 | | - |
88 | | - if (Object.keys(newDepsDict).length > 0) { |
89 | | - for (let packageName in newDepsDict) { |
90 | | - // new deps should include old packages completely |
91 | | - // Example: |
92 | | - // Mseng.MS.TF.DistributedTask.Tasks.AndroidSigningV2-Node16(packageName) should include |
93 | | - // Mseng.MS.TF.DistributedTask.Tasks.AndroidSigningV2(basePackageName) |
94 | | - const depToBeInserted = newDepsArr.find(dep => dep.includes(packageName)); |
95 | | - const pushingIndex = findLastIndex(updatedDeps, basePackage => { |
96 | | - if (!basePackage) return false; |
97 | | - |
98 | | - const depDetails = basePackage.split('"'); |
99 | | - const name = depDetails[1]; |
100 | | - return name && name.startsWith(msPrefix) && packageName.includes(name.split("_")[0]) |
101 | | - }); |
102 | 150 |
|
103 | | - if (pushingIndex !== -1) { |
104 | | - // We need to insert new package after the old one |
105 | | - updatedDeps.splice(pushingIndex + 1, 0, depToBeInserted); |
106 | | - changedTasks.push(packageName); |
107 | | - } |
| 151 | + newDepsArr.splice(index, 1, depsForUpdate[name].depStr); |
| 152 | + index++; |
| 153 | + |
| 154 | + if (depsForUpdate[name].configs) { |
| 155 | + depsForUpdate[name].configs |
| 156 | + .sort((a, b) => a.name > b.name) |
| 157 | + .forEach(config => { |
| 158 | + updatedDepsObj.added.push(config.name); |
| 159 | + newDepsArr.splice(index, 0, config.depStr); |
| 160 | + index++; |
| 161 | + }); |
108 | 162 | } |
109 | 163 | } |
110 | | - // write it as a new file where currentDeps is |
111 | | - fs.writeFileSync(outputPath, updatedDeps.join('\n')); |
| 164 | + |
| 165 | + return [newDepsArr, updatedDepsObj]; |
| 166 | +} |
| 167 | + |
| 168 | +/** |
| 169 | + * The main function for unified dependencies update |
| 170 | + * The function parses unified dependencies file and updates it with new dependencies/remove unused |
| 171 | + * Since the generated tasks can only be used and build with default version, if unified_deps.xml doesn't contain |
| 172 | + * the default version, the specific config (e.g. Node16) will be removed from the list of dependencies |
| 173 | + * @param {String} pathToUnifiedDeps - path to UnifiedDependencies.xml |
| 174 | + * @param {String} pathToNewUnifiedDeps - path to unified_deps.xml which contains dependencies updated on current week |
| 175 | + */ |
| 176 | +function updateUnifiedDeps(pathToUnifiedDeps, pathToNewUnifiedDeps) { |
| 177 | + const currentDeps = fs.readFileSync(pathToUnifiedDeps, 'utf8'); |
| 178 | + const newDeps = fs.readFileSync(pathToNewUnifiedDeps, 'utf8'); |
| 179 | + |
| 180 | + const newDepsArr = newDeps.split('\n'); |
| 181 | + const depsForUpdate = getDeps(newDepsArr); |
| 182 | + |
| 183 | + let depsArray = currentDeps.split('\n'); |
| 184 | + let updatedDeps = { added: [], removed: [] }; |
| 185 | + |
| 186 | + [depsArray, updatedDeps] = removeConfigsForTasks(depsArray, depsForUpdate, updatedDeps); |
| 187 | + [depsArray, updatedDeps] = updateConfigsForTasks(depsArray, depsForUpdate, updatedDeps); |
| 188 | + |
| 189 | + fs.writeFileSync(pathToUnifiedDeps, depsArray.join('\n')); |
112 | 190 | console.log('Updating Unified Dependencies file done.'); |
113 | | - return changedTasks; |
114 | | -}; |
| 191 | + return updatedDeps; |
| 192 | +} |
115 | 193 |
|
116 | | -/* Function to insert new tasks into TfsServer.Servicing.core.xml |
117 | | - * Only if the was modified/added into UnifiedDependencies.xml and not exists in the TfsServer.Servicing.core.xml file |
| 194 | +/** |
| 195 | + * The function update TfsServer.Servicing.core.xml with new dependencies |
| 196 | + * The function check the depsToUpdate which was created during updateUnifiedDeps |
| 197 | + * and add/remove dependencies from TfsServer.Servicing.core.xml |
| 198 | + * @param {String} pathToTfsCore - path to TfsServer.Servicing.core.xml |
| 199 | + * @param {Object} depsToUpdate - structure to track added/removed dependencies (formed in updateUnifiedDeps) |
118 | 200 | */ |
119 | | -function updateTfsServerDeps(pathToTfsCore, depsToUpdateArr, outputPath) { |
| 201 | +async function updateTfsServerDeps(pathToTfsCore, depsToUpdate) { |
120 | 202 | const tfsCore = fs.readFileSync(pathToTfsCore, 'utf8'); |
121 | | - const tfsToUpdate = tfsCore.split('\n'); |
122 | | - const tfsCoreLowerCase = tfsCore.toLowerCase(); |
123 | | - |
124 | | - const insertedIndex = tfsToUpdate.findIndex(tfsString => directoryTag.test(tfsString)); |
125 | | - depsToUpdateArr.forEach(dependencyName => { |
126 | | - const dependencyNameLower = dependencyName.toLowerCase(); |
127 | | - if (tfsCoreLowerCase.indexOf(dependencyNameLower) === -1) { |
128 | | - const insertedString = formDirectoryString(dependencyName); |
129 | | - tfsToUpdate.splice(insertedIndex, 0, insertedString); |
130 | | - console.log(`${insertedString}`); |
131 | | - } |
| 203 | + const tfxCoreJson = await xml2js.parseStringPromise(tfsCore); |
| 204 | + const depsToAdd = depsToUpdate.added.filter(dep => depsToUpdate.removed.indexOf(dep) === -1); |
| 205 | + const depsToRemove = depsToUpdate.removed.filter(dep => depsToUpdate.added.indexOf(dep) === -1); |
| 206 | + |
| 207 | + // removing dependencies |
| 208 | + for (let idx = 0; idx < tfxCoreJson.Component.Directory.length; idx++) { |
| 209 | + const directory = tfxCoreJson.Component.Directory[idx]; |
| 210 | + const files = directory.File; |
| 211 | + const needToRemove = files.filter(file => depsToRemove.findIndex(dep => file.$.Origin.includes(dep)) !== -1); |
| 212 | + if (needToRemove.length) { |
| 213 | + tfxCoreJson.Component.Directory.splice(idx, 1); |
| 214 | + idx--; |
| 215 | + } |
| 216 | + } |
| 217 | + |
| 218 | + depsToAdd.forEach(dep => { |
| 219 | + const directory = formDirectoryTag(dep); |
| 220 | + tfxCoreJson.Component.Directory.unshift(directory); |
| 221 | + }); |
| 222 | + |
| 223 | + const builder = new xml2js.Builder({ |
| 224 | + xmldec: { version: '1.0', encoding: 'utf-8' }, |
| 225 | + renderOpts: { pretty: true, indent: ' ', newline: '\n', allowEmpty: false, spacebeforeslash: ' ' } |
132 | 226 | }); |
| 227 | + const xml = builder.buildObject(tfxCoreJson); |
133 | 228 |
|
134 | | - fs.writeFileSync(outputPath, tfsToUpdate.join('\n')); |
| 229 | + fs.writeFileSync(pathToTfsCore, xml); |
135 | 230 | console.log('Inserting into Tfs Servicing Core file done.'); |
136 | 231 | } |
137 | 232 |
|
138 | | -const changedTasks = updateUnifiedDeps(unifiedDepsPath, newDeps, unifiedDepsPath); |
139 | | -updateTfsServerDeps(tfsServerPath, changedTasks, tfsServerPath); |
| 233 | +const changedTasks = updateUnifiedDeps(unifiedDepsPath, newDeps); |
| 234 | +updateTfsServerDeps(tfsServerPath, changedTasks); |
0 commit comments