What tools support editing project.pbxproj files? - ios

I want to edit project.pbxproj straight up using command line (for CI server script)
what tools can allow me to do this?
I used to use PlistBuddy to edit the output Info.plist; however, what i really want to do is to edit this user defined field, which is used in multiple places, and i really don't want to have to hunt that down in every plist location

project.pbxproj is an old-style ASCII property list file, too. So you can use /usr/libexec/PlistBuddy to edit it.
Print some User-Defined key's value like this,
# Get the key A83311AA20DA4A80004B8C0E in your project.pbxproj
# LZD_NOTIFICATION_SERVICE_BUNDLE_ID is defined by me,
# Replace key paths with your own.
/usr/libexec/PlistBuddy -c 'print :objects:A83311AA20DA4A80004B8C0E:buildSettings:LZD_NOTIFICATION_SERVICE_BUNDLE_ID' LAAppAdapter.xcodeproj/project.pbxproj
Set its value like this,
/usr/libexec/PlistBuddy -c 'set :objects:A83311AA20DA4A80004B8C0E:buildSettings:LZD_NOTIFICATION_SERVICE_BUNDLE_ID com.dawnsong.notification-service' LAAppAdapter.xcodeproj/project.pbxproj
UPDATE
PlistBuddy will automatically convert project.pbxproj into a xml-format plist file since macOS Catalina (or some earlier version). It's better to move the setting item into xcconfig file instead since xcconfig is much smaller and simpler than project.pbxproj and not easy to make mistakes when editing with perl script.

I know this has been answered for a while, but since the original question is about tools supporting the manipulation of .pbxproj files, and many other people may be looking for the same information, here's how I do it. It took me quite a while to figure this out because I was very unfamiliar with Xcode when I started attempting this, so I hope this saves others the hours of grief I had to put in.
You can use the plutil command to transform the .pbxproj file from the legacy .plist format into an XML or JSON format you will be able to manipulate more easily. I'm using JSON. To do so, just run:
plutil -convert json project.pbxproj
This will convert the format of project.pbxproj, but be aware that -contrary to common sense- the output won't be another file with a JSON extention such as project.json. What will happen is that project.pbxproj will be converted to JSON format, but retain it's cryptic .pbxproj extension. So even though the file's format has been changed, Xcode will still pick it up and use it in its new JSON format.
Then you can change project.pbxproj with ease using any JSON manipulation tool of your choosing. I'm using Groovy's JsonSlurper class in a Groovy script.
Note I also explored the XML option, but I found the project.pbxproj file in XML format to be cumbersome to parse. The elements are not properly nested to allow for traversing the tree with ease. It's plagued with:
<key>someKey</key>
<dict>
<!--More elements which provide configuration for the key above-->
</dict>
So it's positional in nature. You have to look for the key element corresponding to the setting you want to manipulate and then jump to the dict element just after it. Which means you have to mount the children of each XML element into an array, in order to index them.

Here are 3 open-source tools which implement .pbxproj file editing:
https://github.com/CocoaPods/Xcodeproj (Ruby based)
https://github.com/apache/cordova-node-xcode (NodeJS based)
https://github.com/kronenthaler/mod-pbxproj (Python based)
Personally, I made the best experience with the NodeJS based tool. So far it has covered all our needs reliably.
In the following is listed an example javascript file update-project.js which sets the developer team ID, app entitlements, adds a GoogleService-Info.plist file to the project and checks it as part of the build target. Take it as an inspiration and adapt the scripts and its paths to your needs:
const fs = require('fs')
const xcode = require('xcode')
if (process.argv.length !== 3) {
console.error("Please pass the development team ID as the first argument")
process.exit(1)
}
const developmentTeamId = process.argv[2]
const path = 'ios/App/App.xcodeproj/project.pbxproj'
const project = xcode.project(path)
project.parse(error => {
const targetKey = project.findTargetKey('App')
const appGroupKey = project.findPBXGroupKey({path: 'App'})
project.addBuildProperty('CODE_SIGN_ENTITLEMENTS', 'App/App.entitlements')
project.addBuildProperty('DEVELOPMENT_TEAM', developmentTeamId)
project.addFile('App.entitlements', appGroupKey)
project.removeFile('GoogleService-Info.plist', appGroupKey)
const f = project.addFile('GoogleService-Info.plist', appGroupKey, {target: targetKey})
f.uuid = project.generateUuid()
project.addToPbxBuildFileSection(f)
project.addToPbxResourcesBuildPhase(f)
fs.writeFileSync(path, project.writeSync())
})
Above script can be executed with
yarn run update-project <arguments...>
given that update-project is registered in package.json:
{
...,
"scripts": {
...
"update-project": "node update-project.js"
},
...
}

Related

How to change Xcode project file and folder target membership and visibility using command line/bash script? [duplicate]

Is there a way to change file's target membership in Xcode project via command line?
Here's what I'm trying to do via Xcode's UI:
I also had to do this for CI. After lots of digging, I do not believe this is common enough for anyone to have written a tool to help with doing.
The only conclusion I came to was to edit the project.pbxproj file directly, which is never a great thing to do. None of the tools which claim to do this were of any help until I found this stackoverflow answer on editing the project.pbxproj file. Essentially, you can convert the project.pbxproj file into a JSON format using plutil -convert json project.pbxproj and use a JSON manipulation tool to make those files as headers then point them to be headers of whichever target you would like.
When converting the project.pbxproj into JSON format, be aware that Xcode will no longer be able to show you the project navigator for that project. It will still build and run, however, so this is really only useful if you're planning to do this right before building (such as for CI).
(EDIT: As of July 2022, Xcode will now properly read a JSON version of its .pbxproj to allow you to view your files in the project navigator. I'm not sure which version introduced this, but it is at least now possible with later versions of Xcode.)
The format project.pbxproj as JSON has nearly all the important data under the "objects" key. The file you want to be a header already has an entry with the key being the UUID for the file and a path value you can use to relate the UUID to your file. Here's an example of that format:
// UUID for your file
"65TYSSDXHSLP4UUOAD9D40C322AAGHM9": {
"path": "MyHeader.h", // Your file's name
"isa": "PBXFileReference",
"includeInIndex": "1",
"lastKnownFileType": "sourcecode.c.h",
"sourceTree": "<group>"
}
There's another entry to declare this file as a header, which has its own UUID and a reference to the UUID of your file:
// UUID for your file as a header
"YU3BSD39O9PT5RESDFV741D1": {
"isa": "PBXBuildFile",
"fileRef": "65TYSSDXHSLP4UUOAD9D40C322AAGHM9", // UUID for your file MyHeader.h
"settings": {
"ATTRIBUTES": [
"Public" // could also be Project or Private
]
}
}
Then finally, your target has a list of header files where you will want the UUID for the header reference to go.
"A82GAE9A5HUIO063IOPQAAQIUFGSNXZ": {
"isa": "PBXHeadersBuildPhase",
"buildActionMask": "2147483647",
"files": [
"YU3BSD39O9PT5RESDFV741D1" // UUID for your file as a header
],
"runOnlyForDeploymentPostprocessing": "0"
}
Again, changing the project.pbxproj file directly is never a great idea, but until there's a better tool for making these changes without using Xcode, it's the best I could find. If anyone else is aware of something I'm not, please let me know.

How can I pass a pointer to a file in helm upgrade command?

I have a truststore file(a binary file) that I need to provide during helm upgrade. This file is different for each target env(dev,qa,staging or prod). So I can only provide this file at time of deployment. helm upgrade --set-file does not take a binary file. This seem to be the issue I found here: https://github.com/helm/helm/issues/3276. This truststore files are stored in Jenkins Credential store.
As the command itself is described below:
--set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)
it is also important to know The Format and Limitations of
--set.
The error you see: Error: failed parsing --set-file data... means that the file you are trying to use does not meet the requirements. See the example below:
--set-file key=filepath is another variant of --set. It reads the
file and use its content as a value. An example use case of it is to
inject a multi-line text into values without dealing with indentation
in YAML. Say you want to create a brigade project with certain value
containing 5 lines JavaScript code, you might write a values.yaml
like:
defaultScript: |
const { events, Job } = require("brigadier")
function run(e, project) {
console.log("hello default script")
}
events.on("run", run)
Being embedded in a YAML, this makes it harder for you to use IDE
features and testing framework and so on that supports writing code.
Instead, you can use --set-file defaultScript=brigade.js with
brigade.js containing:
const { events, Job } = require("brigadier")
function run(e, project) {
console.log("hello default script")
}
events.on("run", run)
I hope it helps.

Weird hidden characters in exported XLIFF file

Whenever I try to 'Export For Internationalization', the exported file contains a weird hidden character, making it unparsable for XLIFF editors. The problem seemed to be in the original .string files, somehow the weird character was inserted in those files. I have since deleted the weird character but whenever I export it still sneaks into the xliff file. I tried cleaning and rebuilding the project, restarting Xcode... none of that seems to work.
Is Xcode somehow using a cached version of the 'bad' .strings file containing the bad character?
Using Xcode
If I try Editor > Export For Localization
I get:
/usr/bin/xmllint exited with status 1
Using Terminal
When I run it from Terminal like so:
xcodebuild -exportLocalizations -localizationPath
/Users/Kymer/Downloads/Wolf -project Wolf.xcodeproj -exportLanguage fr
I get the following errors:
parser error : attributes construct error
parser error : Couldn't find end of Start Tag trans-unit
parser error : PCDATA invalid Char value 19
parser error : PCDATA invalid Char value 19
parser error : Opening and ending tag mismatch
parser error : invalid character in attribute value
parser error : attributes construct error
parser error : Couldn't find end of Start Tag
parser error : PCDATA invalid Char value
/Uxcodebuild: error: /usr/bin/xmllint exited with status 1
In both cases the exported xliff file contains the weird hidden character upon inspection with Sublime Text:
If I manually remove the bad characters the file is perfectly readable by xliff-editors but that's not a good long-term solution of course.
I found the problem: when exporting to an XLIFF file Xcode doesn't look at your .string files, it is all generated from the project itself (i.e. it looks at all NSLocalizedString calls and your storyboards). Which makes sense. I found the weird hidden character in one of my code files. Removing it from the source file fixed the export issue.
Easiest XLIFF workflow
I'll also mention this for future reference: the easiest way to add a new language to your project is to first use the command line:
cd to the your project and run:
xcodebuild -exportLocalizations -localizationPath <path> -project <projectname>.xcodeproj -exportLanguage <language code>
That creates a new XLIFF file and will correctly set the target language in the file (source language will be your base language). A translator can now easily add all necessary translations. Afterwards you can import the translated XLIFF file back into Xcode (select target and Editor > Import localizations). Xcode will then generate all necessary .string files.
Updating existing language: If you add new UI elements and want to update an existing localization language, you can simply export an existing localization (select target and Editor > Export for localization). That XLIFF file will contain all previous translations together with the new strings. A translator simply has to fill in the 'blank' lines. There's no need to touch the .string files yourself, because managing that manually is a pain (especially with the crazy Storyboard ID's).

how to set the path to where aapt add command adds the file

I'm using aapt tool to remove some files from different folders of my apk. This works fine.
But when I want to add files to the apk, the aapt tool add command doesn't let me specify the path to where I want the file to be added, therefore I can add files only to the root folder of the apk.
This is strange because I don't think that developers would never want to add files to a subfolder of the apk (res folder for example). Is this possible with aapt or any other method? Cause removing files from any folder works fine, and adding file works only for the root folder of the apk. Can't use it for any other folder.
Thanks
The aapt tool retains the directory structure specified in the add command, if you want to add something to an existing folder in an apk you simply must have a similar folder on your system and must specify each file to add fully listing the directory. Example
$ aapt list test.apk
res/drawable-hdpi/pic1.png
res/drawable-hdpi/pic2.png
AndroidManifest.xml
$ aapt remove test.apk res/drawable-hdpi/pic1.png
$ aapt add test.apk res/drawable-hdpi/pic1.png
The pic1.png that will is added resides in a folder in the current working directory of the terminal res/drawable-hdpi/ , hope this answered your question
There is actually a bug in aapt that will make this randomly impossible. The way it is supposed to work is as the other answer claims: paths are kept, unless you pass -k. Let's see how this is implemented:
The flag that controls whether the path is ignored is mJunkPath:
bool mJunkPath;
This variable is in a class called Bundle, and is controlled by two accessors:
bool getJunkPath(void) const { return mJunkPath; }
void setJunkPath(bool val) { mJunkPath = val; }
If the user specified -k at the command line, it is set to true:
case 'k':
bundle.setJunkPath(true);
break;
And, when the data is being added to the file, it is checked:
if (bundle->getJunkPath()) {
String8 storageName = String8(fileName).getPathLeaf();
printf(" '%s' as '%s'...\n", fileName, storageName.string());
result = zip->add(fileName, storageName.string(),
bundle->getCompressionMethod(), NULL);
} else {
printf(" '%s'...\n", fileName);
result = zip->add(fileName, bundle->getCompressionMethod(), NULL);
}
Unfortunately, the one instance of Bundle used by the application is allocated in main on the stack, and there is no initialization of mJunkPath in the constructor, so the value of the variable is random; without a way to explicitly set it to false, on my system I (seemingly deterministically) am unable to add files at specified paths.
However, you can also just use zip, as an APK is simply a Zip file, and the zip tool works fine.
(For the record, I have not submitted the trivial fix for this as a patch to Android yet, if someone else wants to the world would likely be a better place. My experience with the Android code submission process was having to put up with an incredibly complex submission mechanism that in the end took six months for someone to get back to me, in some cases with minor modifications that could have just been made on their end were their submission process not so horribly complex. Given that there is a really easy workaround to this problem, I do not consider it important enough to bother with all of that again.)

best way to add license section to iOS settings bundle

My iOS application uses a number of third party components licensed under Apache 2.0 and similar licenses, which requires me to include various bits of text, this kind of thing:
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
There seems to be a reasonable precedent for putting this information under a 'License' subentry in settings bundle (on the ipad facebook, pages, keynote, numbers and wikipanion all seem to do this).
I'm struggling a bit to actually achieve the same though; I seem to need to split the text up line by line and enter into xcode a line at a time (and xcode4 seems to have a crashing problem when editing the plists).
It seems like the kind of thing that there's almost certainly a somewhere script to do, or some simple way to do it that I've missed.
I think I've now managed to solve all the problems I was running into.
It seems to be best to use group element titles to hold the licenses (this is what Apple do in the iWork apps). There is however a limit on the length of these (and I've not yet discovered exactly what the limit is), so you need to break each license file into multiple strings.
You can create a line break within these by include a literal carriage return (ie. otherwise known as ^M, \r or 0x0A)
Make sure not to include any literal "s mid-text. If you do, some or all of the strings in the file will get silently ignored.
I've got a convenience script I use to help generate the .plist and .strings file, shown below.
To use it:
Create a 'licenses' directory under your project
Put script into that directory
Put each license into that directory, one per file, with filenames that end .license
Perform any necessary reformatting on the licenses. (eg. remove extra spaces at the beginning of lines, ensure that there are no line breaks mid-paragraph). There should be a blank line in-between each paragraph
Change to licenses directory & run the script
Edit your settings bundle Root.plist to include a child section called 'Acknowledgements'
Here's the script:
#!/usr/bin/perl -w
use strict;
my $out = "../Settings.bundle/en.lproj/Acknowledgements.strings";
my $plistout = "../Settings.bundle/Acknowledgements.plist";
unlink $out;
open(my $outfh, '>', $out) or die $!;
open(my $plistfh, '>', $plistout) or die $!;
print $plistfh <<'EOD';
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>StringsTable</key>
<string>Acknowledgements</string>
<key>PreferenceSpecifiers</key>
<array>
EOD
for my $i (sort glob("*.license"))
{
my $value=`cat $i`;
$value =~ s/\r//g;
$value =~ s/\n/\r/g;
$value =~ s/[ \t]+\r/\r/g;
$value =~ s/\"/\'/g;
my $key=$i;
$key =~ s/\.license$//;
my $cnt = 1;
my $keynum = $key;
for my $str (split /\r\r/, $value)
{
print $plistfh <<"EOD";
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>Title</key>
<string>$keynum</string>
</dict>
EOD
print $outfh "\"$keynum\" = \"$str\";\n";
$keynum = $key.(++$cnt);
}
}
print $plistfh <<'EOD';
</array>
</dict>
</plist>
EOD
close($outfh);
close($plistfh);
Setting up your Settings.bundle
If you haven't created a Settings.bundle, go to File --> New --> New File...
Under the Resource section, find the Settings Bundle. Use the default name and save it to the root of your project.
Expand the Settings.bundle group and select Root.plist. You will need to add a new section where its key will be Preference Items of type Array. Add the following information:
The Filename key points to the plist that was created by this script. You can change the title to what ever you want.
Execute Script At Build Time
Also, if you want this script to run whenever you build your project, you can add a build phase to your target:
Go to your project file
Select the target
Click the Build Phases tab
In the lower right corner of that pane, click on 'Add Build Phase'
Select 'Add Run Script'
Drag and drop your perl script into the section for your script. Modify to look something like this:
cd $SRCROOT/licenses ($SRCROOT points to the root of your project)
./yourScriptName.pl
After you have finished that, you can drag the Run Script build phase sooner in the build process. You'll want to move it up before Compile Sources so that the updates to your Settings Bundle get compiled and copied over.
Update for iOS 7: iOS 7 seems to handle the "Title" key different and is messing up the rendered text. To fix that the generated Acknowledgements.plist needs to use the "FooterText" key instead of "Title". This how to change the script:
for my $str (split /\r\r/, $value)
{
print $plistfh <<"EOD";
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
<key>FooterText</key> # <= here is the change
<string>$keynum</string>
</dict>
EOD
print $outfh "\"$keynum\" = \"$str\";\n";
$keynum = $key.(++$cnt);
}
Here's the same solution that #JosephH provided (without translations), but done in Python for anyone who prefers python over perl
import os
import sys
import plistlib
from copy import deepcopy
os.chdir(sys.path[0])
plist = {'PreferenceSpecifiers': [], 'StringsTable': 'Acknowledgements'}
base_group = {'Type': 'PSGroupSpecifier', 'FooterText': '', 'Title': ''}
for filename in os.listdir("."):
if filename.endswith(".license"):
current_file = open(filename, 'r')
group = deepcopy(base_group)
title = filename.split(".license")[0]
group['Title'] = title
group['FooterText'] = current_file.read()
plist['PreferenceSpecifiers'].append(group)
plistlib.writePlist(
plist,
"../Settings.bundle/Acknowledgements.plist"
)
As an alternative, for those using CocoaPods, it will generate an 'Acknowledgements' plist for each target specified in your Podfile which contains the License details for each Pod used in that target (assuming details have been specified in the Pod spec). The property list file that can be added to the iOS settings bundle.
There's also projects under way to allow this data to be converted and displayed within the app instead:
https://github.com/CocoaPods/cocoapods-install-metadata
https://github.com/cocoapods/CPDAcknowledgements
I thought I'd throw my iteration on Sean's awesome python code in the mix. The main difference is that it takes an input directory and then recursively searches it for LICENSE files. It derives the title value from the parent directory of the LICENSE file, so it plays well with cocoapods.
The motivation was to create a build script to automatically keep the legal section of my app up to date as I add or remove pods. It also does some other things like remove forced newlines from licenses so the paragraphs look a bit better on the devices.
https://github.com/carloe/LicenseGenerator-iOS
I made a script in Ruby inspiered by #JosephH script.
This version will, in my own opinion, better represent the individual open source projects.
Wisit iOS-AcknowledgementGenerator to download the script and sample project.
This is what acknowledgements will look like in your App:
This is an addendum to JosephH's answer. (I don't have the rep to comment)
I had to move
<key>StringsTable</key>
<string>Acknowledgements</string>
down to above the last </dict> in the Perl script.
Before this modification, the Acknowledgements Section in the App was empty and XCode couldn't read the resulting Acknowledgements.plist. ( "The data couldn’t be read because it isn’t in the correct format.")
(XCode 6.3.2 iOS 8.3)
The Python script from Sean in this thread works. But there a couple of basic things to know.
in Xcode, right click on the top of the Project Navigator tree, on the name of your project, and add a New Group. This puts a new folder in your project.
Add Sean's script there and make sure to save it as: Acknowledgements.py.
Make sure you have Python installed on your system. I'm using a Mac.
Add a first license file to the folder you created in 1. Make it simple like just having one word in the file, say: Testing. Save it in the folder as Test1.license.
Set up your Settings.bundle as per JosephH above.
Use your Terminal app to CD to the folder you created in 1.
Run the script. Type: python Acknowledgements.py. If you get no errors it will return right back to the Terminal prompt. Do all of this before adding any run script to the Build.
Build and run your app.
Double tap on the iPhone home button and kill Settings. It doesn't often pick up the Settings change for your app until Settings restarts.
After restarting Settings, go to your app and look to see if it worked.
If that all worked, slowly add more license files but run the script each time. You can get errors running the script because of certain characters in the file so the easy way to debug is to add a file, run the script, see if it worked and proceed. Else, edit any special characters out of the .license file.
I did not get the Run Build Script work per the instructions above. But this process works fine if you are not changing the .license files that often.
Ack Ack: Acknowledgement Plist Generator
A while back I've created a Python script that scans for license files and creates a nice Acknowledgements plist that you can use in your Settings.plist. It does a lot of the work for you.
https://github.com/Building42/AckAck
Features
Detects Carthage and CocoaPods folders
Detects existing plists for custom licenses
Cleans up the license texts by removing unnecessary new lines and line breaks
Provides many customization options (see --help for details)
Supports both Python v2 and v3
Install
wget https://raw.githubusercontent.com/Building42/AckAck/master/ackack.py
chmod +x ackack.py
Run
./ackack.py
Screenshot
If you have suggestions for improvements, feel free to post an issue or pull request on GitHub!
Aknowlist is a strong CocoaPod candidate that is actively maintained at the time of this writing. It automates this licensing if you are okay with housing the licenses in your app rather than the settings bundle. It worked great for the project I was working on.
I had to modify sean's script for modern python3:
import os
import sys
import plistlib
from copy import deepcopy
os.chdir(sys.path[0])
plist = {'PreferenceSpecifiers': [], 'StringsTable': 'Acknowledgements'}
base_group = {'Type': 'PSGroupSpecifier', 'FooterText': '', 'Title': ''}
for filename in os.listdir("."):
if filename.endswith(".license"):
with open(filename, 'r') as current_file:
group = deepcopy(base_group)
title = filename.split(".license")[0]
group['Title'] = title
group['FooterText'] = current_file.read()
plist['PreferenceSpecifiers'].append(group)
with open("Acknowledgements.plist", "wb") as f:
plistlib.dump(plist, f)
Wait, license notation is not a setting.
Edit:
I think license notice is not a setting. I think it is irrational to expect users who want to check the license notice to open the settings app. Therefore, I thought we should create a page for the license notice in the appropriate place in the app.

Resources