@@ -10,16 +10,20 @@ import * as _ from "lodash";
1010import { injector } from "../common/yok" ;
1111
1212export class XcconfigService implements IXcconfigService {
13- constructor ( private $childProcess : IChildProcess , private $fs : IFileSystem ) { }
13+ private static readonly CONFLICT_MARKER = "NS_XCCONFIG_CONFLICTS:" ;
14+
15+ constructor (
16+ private $childProcess : IChildProcess ,
17+ private $fs : IFileSystem ,
18+ private $logger : ILogger ,
19+ ) { }
1420
1521 public getPluginsXcconfigFilePaths ( projectRoot : string ) : IStringDictionary {
1622 return {
17- [ Configurations . Debug . toLowerCase ( ) ] : this . getPluginsDebugXcconfigFilePath (
18- projectRoot
19- ) ,
20- [ Configurations . Release . toLowerCase ( ) ] : this . getPluginsReleaseXcconfigFilePath (
21- projectRoot
22- ) ,
23+ [ Configurations . Debug . toLowerCase ( ) ] :
24+ this . getPluginsDebugXcconfigFilePath ( projectRoot ) ,
25+ [ Configurations . Release . toLowerCase ( ) ] :
26+ this . getPluginsReleaseXcconfigFilePath ( projectRoot ) ,
2327 } ;
2428 }
2529
@@ -33,28 +37,79 @@ export class XcconfigService implements IXcconfigService {
3337
3438 public async mergeFiles (
3539 sourceFile : string ,
36- destinationFile : string
40+ destinationFile : string ,
3741 ) : Promise < void > {
3842 if ( ! this . $fs . exists ( destinationFile ) ) {
3943 this . $fs . writeFile ( destinationFile , "" ) ;
4044 }
4145
42- const escapedDestinationFile = destinationFile . replace ( / ' / g, "\\'" ) ;
43- const escapedSourceFile = sourceFile . replace ( / ' / g, "\\'" ) ;
44-
45- const mergeScript = `require 'xcodeproj';
46- userConfig = Xcodeproj::Config.new('${ escapedDestinationFile } ')
47- existingConfig = Xcodeproj::Config.new('${ escapedSourceFile } ')
48- userConfig.attributes.each do |key,|
49- existingConfig.attributes.delete(key) if (userConfig.attributes.key?(key) && existingConfig.attributes.key?(key))
46+ // A key already present in the destination wins, so the incoming one is
47+ // dropped. Report the drops whose values actually differ: a silently
48+ // discarded setting is otherwise indistinguishable from one that was
49+ // never written, which makes a plugin pinning e.g.
50+ // CLANG_CXX_LANGUAGE_STANDARD very hard to track down.
51+ //
52+ // The paths are passed as argv rather than interpolated: they come from
53+ // the project and node_modules layout, and a shell-interpolated command
54+ // would execute anything a directory name expands to.
55+ const mergeScript = `require 'xcodeproj'
56+ require 'json'
57+ destination, source = ARGV
58+ userConfig = Xcodeproj::Config.new(destination)
59+ existingConfig = Xcodeproj::Config.new(source)
60+ conflicts = []
61+ userConfig.attributes.each do |key, kept|
62+ if existingConfig.attributes.key?(key)
63+ ignored = existingConfig.attributes[key]
64+ conflicts << { 'key' => key, 'kept' => kept.to_s, 'ignored' => ignored.to_s } if ignored.to_s != kept.to_s
65+ existingConfig.attributes.delete(key)
66+ end
5067 end
51- userConfig.merge(existingConfig).save_as(Pathname.new('${ escapedDestinationFile } '))` ;
52- await this . $childProcess . exec ( `ruby -e "${ mergeScript } "` ) ;
68+ userConfig.merge(existingConfig).save_as(Pathname.new(destination))
69+ print '${ XcconfigService . CONFLICT_MARKER } ' + JSON.generate(conflicts)` ;
70+ const output = await this . $childProcess . execFile ( "ruby" , [
71+ "-e" ,
72+ mergeScript ,
73+ destinationFile ,
74+ sourceFile ,
75+ ] ) ;
76+ this . warnAboutConflicts ( sourceFile , output ) ;
77+ }
78+
79+ private warnAboutConflicts ( sourceFile : string , output : any ) : void {
80+ const text : string =
81+ output === null || output === undefined ? "" : `${ output } ` ;
82+ const markerIndex = text . lastIndexOf ( XcconfigService . CONFLICT_MARKER ) ;
83+ if ( markerIndex === - 1 ) {
84+ return ;
85+ }
86+
87+ let conflicts : { key : string ; kept : string ; ignored : string } [ ] ;
88+ try {
89+ conflicts = JSON . parse (
90+ text . substring ( markerIndex + XcconfigService . CONFLICT_MARKER . length ) ,
91+ ) ;
92+ } catch ( err ) {
93+ // Never let a reporting problem fail the merge itself.
94+ this . $logger . trace (
95+ `Unable to read xcconfig conflicts for ${ sourceFile } : ${ err } ` ,
96+ ) ;
97+ return ;
98+ }
99+
100+ for ( const conflict of conflicts || [ ] ) {
101+ this . $logger . warn (
102+ `Ignoring ${ conflict . key } = ${ conflict . ignored } from ${ sourceFile } : ` +
103+ `already set to ${ conflict . kept } by a higher precedence xcconfig. ` +
104+ `The app's App_Resources xcconfig is applied first, then each ` +
105+ `plugin's in dependency order.` ,
106+ ) ;
107+ }
53108 }
54109
55110 public readPropertyValue (
56111 xcconfigFilePath : string ,
57- propertyName : string
112+ propertyName : string ,
58113 ) : string {
59114 if ( this . $fs . exists ( xcconfigFilePath ) ) {
60115 const text = this . $fs . readText ( xcconfigFilePath ) ;
0 commit comments