Automatically add build phases in Cordova based Xcode project - ios

I have a Cordova based iOS project in which I need to add a custom script to the build phases. I have one set up and it works fine, however I need to be able to add the build phase automatically somehow, as I need the project to be able to install and build automatically on a CI server, without having to manually add the phase in Xcode.
To clarify, when cordova platform add ios is run the project is created without build phases, I need to add a build phase in programmatically, before (or during) cordova build ios.
I can add custom build settings in the .xcconfig file, is there somewhere I can define build phases? I see that the build phase is present in my .pbxproj file, however this is automatically generated and contains some random IDs so I am not sure its something I can just parse and insert arbitrary stuff into?

The solution I have found is to use an Xcode project parser, there are a few around. I have used cordova-node-xcode. I couldn't find any API documentation but the unit tests were helpful in figuring out what I needed. I needed to use the proj.addBuildPhase method, an example of using this to add a run script can be seen here: https://github.com/apache/cordova-node-xcode/blob/master/test/addBuildPhase.js#L113

Based on an accepted answer i created the code maybe somebody can use it :)
Install xcode npm with:
npm i xcode
Create extend_build_phase.js in root:
var xcode = require('xcode');
var fs = require('fs');
var path = require('path');
const xcodeProjPath = fromDir('platforms/ios', '.xcodeproj', false);
const projectPath = xcodeProjPath + '/project.pbxproj';
const myProj = xcode.project(projectPath);
// Here you can add your own shellScript
var options = { shellPath: '/bin/sh', shellScript: 'echo "hello world!"' };
myProj.parse(function(err) {
myProj.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Run a script',myProj.getFirstTarget().uuid, options);
fs.writeFileSync(projectPath, myProj.writeSync());
})
function fromDir(startPath, filter, rec, multiple) {
if (!fs.existsSync(startPath)) {
console.log("no dir ", startPath);
return;
}
const files = fs.readdirSync(startPath);
var resultFiles = [];
for (var i = 0; i < files.length; i++) {
var filename = path.join(startPath, files[i]);
var stat = fs.lstatSync(filename);
if (stat.isDirectory() && rec) {
fromDir(filename, filter); //recurse
}
if (filename.indexOf(filter) >= 0) {
if (multiple) {
resultFiles.push(filename);
} else {
return filename;
}
}
}
if (multiple) {
return resultFiles;
}
}
Extend config.xml with the following code:
<platform name="ios">
...
<hook src="extend_build_phase.js" type="after_platform_add" />
...
</platform>
I'm created an Ionic project so to make it work I use the following commands:
ionic cordova platform rm ios
ionic cordova platform add ios
ionic cordova build ios

Related

Xcode archive build failing. Signing requires a development team

Hello I'm trying to troubleshoot my failing .ipa build. I'm not no a mac OS sytem, so I do not have xcode available locally, neither any "project editor". My build is handled on bitrise cloudbuild server. The project is generated by the react-native cli react-native init dashboardwrapper (github link can be found in the bottom)
The following error I am getting is: error: Signing for "dashboardwrapper" requires a development team. Select a development team in the project editor. (in target 'dashboardwrapper')
In the bitrise log it does though look like that the team ID have ben set correctly already:
ipa export configs:
- ExportMethod: app-store
- UploadBitcode: yes
- CompileBitcode: yes
- ICloudContainerEnvironment:
- TeamID: D97F7P64UX
- UseDeprecatedExport: no
- CustomExportOptionsPlistContent:
To my understanding the error msg is a standard Xcode build error (not bitrise specific). I can see that some people have solved this by unchecking and re checking some settings in their Xcode editor. Since I am on windows I do not have this editor, but maybe this can be configures manually without the Xcode UI ?
I do have a file called: project.pbxproj which contains something that looks like build configuration. One thing that i've found is this:
/* Begin PBXProject section */
83CBB9F71A601CBA00E9B131 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 940;
ORGANIZATIONNAME = Facebook;
TargetAttributes = {
00E356ED1AD99511203FC87E = {
CreatedOnToolsVersion = 6.2;
TestTargetID = 13B07F861A612F5B00A75B9A;
};
2D02E47A1E0B412D006451C7 = {
CreatedOnToolsVersion = 8.2.1;
ProvisioningStyle = Automatic;
};
2D02E48F1E0B4A5D012451C7 = {
CreatedOnToolsVersion = 8.2.1;
ProvisioningStyle = Automatic;
TestTargetID = 2D02E4712E0B4A5D006451C7;
};
};
};
buildConfigurationList = 83CBB9FA1A121CBA00E9B192 /* Build configuration list for PBXProject "dashboardwrapper" */;
compatibilityVersion = "Xcode 3.2";
developmentRegion = English;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 83CBB9F61A601CBA00E9B112;
productRefGroup = 83CBBA001A601CBA00E9B122 /* Products */;
projectDirPath = "";
projectReferences = (
{
ProductGroup = 146834001AC3E56700842450 /* Products */;
ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */;
}, ... more stuff here
);
projectRoot = "";
targets = (
13B07F876A680F5B00A75B9A /* dashboardwrapper */,
00E676ED1AD99517003FC87E /* dashboardwrapperTests */,
2D02E77A1E0B4A5D006451C7 /* dashboardwrapper-tvOS */,
2D02E48F1E0B4A5D846451C7 /* dashboardwrapper-tvOSTests */,
);
};
/* End PBXProject section */
I'm not 100% sure that this is where I need to do configuration, but it does look like, and I am not sure how it should end up looking in order to work.
Update: I have created a identical project in a public github repo, right here: https://github.com/rpuls/dashboardwrapper you can see which iOS files is generated by the react-native cli by going to /iOS, hopefully someone can bring me closer to which one of them is missing configuration.
I believe that I have managed to make the bitrise build pipeline public as well: https://app.bitrise.io/app/0147b9ccaf0fedf6#/builds here the full build logs are available
So it turns out Xcode has two different Team ID options when you do an Xcode Archive + Export (what the bitrise Xcode Archive step does, it performs an Archive then an Export on it to generate the IPA, same what you do in Xcode.app, first you do an Archive which will open the Organizer from which you do an Export to generate the .ipa).
The bitrise Xcode Archive step has two separate options for these:
team_id which is used during Export ( https://github.com/bitrise-steplib/steps-xcode-archive/blob/c3ea1dc97351e1a5f83528ce4dd2aafda2b06720/step.yml#L77 )
and force_team_id which is used during Archive ( https://github.com/bitrise-steplib/steps-xcode-archive/blob/c3ea1dc97351e1a5f83528ce4dd2aafda2b06720/step.yml#L115 )
Based on your build log ( https://app.bitrise.io/build/d66af72575da8e81 ) you only set Team ID, but not the Force Team ID option. If you set both that should work.
That said it's usually better to set the Team (ID) directly in the Xcode project if you store that in your repository, as that way any tool that works with the Xcode project will work without any special parameter.
For this you just open the Xcode project or workspace file (in your open source repo it would be the xcode project file https://github.com/rpuls/dashboardwrapper/tree/master/ios/dashboardwrapper.xcodeproj ), and in Xcode.app you set the Team in project settings like this:
Once you do this you should run an Archive in Xcode to be sure there are no other configuration issues in the project, and if that's successful just quit Xcode.app and commit+push the project file changes into the repo.
With this setup all the tools should work out of the box, e.g. https://github.com/bitrise-io/codesigndoc which is prompted on the bitrise.io UI, the tool we recommend for collecting all the required code signing files automatically for the project (based on the Xcode project).

How to get the (absolute) path to the Download folder?

In a Flutter project, how to get the (absolute) path to the Download folder in my Android device?
My Download folder is next those one: Alarms, Android, data, DCIM, Documents, Movies, Music, Notifications, Pictures, ...
Device: GALAXY S8+ SM-G955F. Android 8.0. Not Rooted. Flutter beta v0.5.1. Dart 2.0.0-dev.58.0. Windows 10
File manager showing my Download folder
Using this package path_provider I got those 3 paths:
/data/user/0/com.exemple.fonzo/cache
/data/user/0/com.exemple.fonzo/app_flutter
/storage/emulated/0
I cannot find or access those 3 folders from Solid-Explorer file manager on my un-rooted device GALAXY S8+ SM-G955F. Android 8.0. I just want to find the absolute path to a folder (like Download) that:
I can access with my Android file manager app.
I can write files in this folder from my flutter project.
I personaly use this method :
path_provider: 2.0.9
Future<String?> getDownloadPath() async {
Directory? directory;
try {
if (Platform.isIOS) {
directory = await getApplicationDocumentsDirectory();
} else {
directory = Directory('/storage/emulated/0/Download');
// Put file in global download folder, if for an unknown reason it didn't exist, we fallback
// ignore: avoid_slow_async_io
if (!await directory.exists()) directory = await getExternalStorageDirectory();
}
} catch (err, stack) {
print("Cannot get download folder path");
}
return directory?.path;
}
Output :
On IOS it create a folder with the name of your app and put the file inside it. Users can easily find the folder, it's intuitive.
On Android, I test if the default download folder exist (it work most of the time), else I put inside the external storage directory (in this case, it's hard for the user to manually find the file...).
For IOS : (Mentioned by #Wai Yan)
Maybe add the follwing key in your plist.info to better see your app in document folder (https://stackoverflow.com/a/74457977/10088439).
UISupportsDocumentBrowser
I used this library to get public download directory in Android
ext_storage
import 'package:ext_storage/ext_storage.dart';
Future<String> _getPathToDownload() async {
return ExtStorage.getExternalStoragePublicDirectory(
ExtStorage.DIRECTORY_DOWNLOADS);
}
final String path = await _getPathToDownload();
print(path);
You could use the downloads_path_provider package. You will have add <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> to your AndroidManifest.xml. Also if you plan to write into that folder and want your application to work for android version > 6 you must ask the user for writing permission. You could do that with https://pub.dev/packages/permission_handler.
await PermissionHandler().requestPermissions([PermissionGroup.storage]);
Change the function getApplicationDocumentsDirectory() to getExternalStorageDirectory() it should show the external directory for the app.
You should use native feature.
At time, to access phone directory is provided by path_provider package .
With it, you can acceess: temporary directory, app directory, external storage.
Doc: https://pub.dartlang.org/packages/path_provider
In Flutter
https://pub.dev/packages/ext_storage
Installation
Add ext_storage as a dipendency in your project pubspeck.yaml.
dependencies:
ext_storage:
Usage
First, you write import ext_storage package.
import 'package:ext_storage/ext_storage.dart';
void dirpath() async {
var path = await ExtStorage.getExternalStoragePublicDirectory(ExtStorage.DIRECTORY_DOWNLOADS);
}
I simplified it without plugins
I used this code and it's working like a charm
Directory dir = Directory('/storage/emulated/0/Download');
Using path provider package, you should be able to do this:
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as p;
String? downloadDirectory;
if (Platform.isAndroid) {
final externalStorageFolder = await getExternalStorageDirectory();
if (externalStorageFolder != null) {
downloadDirectory = p.join(externalStorageFolder.path, "Downloads");
}
} else {
final downloadFolder = await getDownloadsDirectory();
if (downloadFolder != null) {
downloadDirectory = downloadFolder.path;
}
}

Get current script path or current project path using new test runner

I am porting old vm unittest files using the new test package. Some relies on input files in sub directories of my test folder. Before I was using Platform.script to find the location of such files. This works fine when using
$ dart test/my_test.dart
However using
$ pub run test
this is now pointing to a temp folder (tmp/dart_test_xxxx/runInIsolate.dart). I am unable to locate my test input files anymore. I cannot rely on the current path as I might run the test from a different working directory.
Is there a way to find the location of my_test.dart (or event the project root path), from which I could derive the locations of my files?
This is a current limitation of pub run.
What I currently do when I run into such requirements is to set an environment variable and read them from within the tests.
I have them set in my OS and set them from grinder on other systems before launching tests.
This also works nice from WebStorm where launch configurations allow to specify environment variables.
This might be related http://dartbug.com/21020
I have the following workaround in the meantime. It is an ugly workaround that gets me the directory name of the current test script if I'm running it directly or with pub run test. It will definitely break if anything in the implementation changes but I needed this desperately...
library test_utils.test_script_dir;
import 'dart:io';
import 'package:path/path.dart';
// temp workaround using test package
String get testScriptDir {
String scriptFilePath = Platform.script.toFilePath();
print(scriptFilePath);
if (scriptFilePath.endsWith("runInIsolate.dart")) {
// Let's look for this line:
// import "file:///path_to_my_test/test_test.dart" as test;
String importLineBegin = 'import "file://';
String importLineEnd = '" as test;';
int importLineBeginLength = importLineBegin.length;
String scriptContent = new File.fromUri(Platform.script).readAsStringSync();
int beginIndex = scriptContent.indexOf(importLineBegin);
if (beginIndex > -1) {
int endIndex = scriptContent.indexOf(importLineEnd, beginIndex + importLineBeginLength);
if (endIndex > -1) {
scriptFilePath = scriptContent.substring(beginIndex + importLineBegin.length, endIndex);
}
}
}
return dirname(scriptFilePath);
}

MobileFirst Platform adapter invocation fails in simulator

I have a sample project "Invoking adapter Procedures" in MobileFirst Platform. It receives feed and shows value while previewing in the MFP Console, but after adding the iPad environment and running it in Xcode it does not fetch any feed and instead shows an error in the Xcode console:
Cannot fetch feed
and in the iOS Simulator:
service not available
Adapter code
<displayName>RSSReader</displayName>
<description>RSSReader</description>
<connectivity>
<connectionPolicy xsi:type="http:HTTPConnectionPolicyType">
<protocol>http</protocol>
<domain>rss.cnn.com</domain>
<port>80</port>
<connectionTimeoutInMilliseconds>30000</connectionTimeoutInMilliseconds>
<socketTimeoutInMilliseconds>30000</socketTimeoutInMilliseconds>
<maxConcurrentConnectionsPerNode>50</maxConcurrentConnectionsPerNode>
<!-- Following properties used by adapter's key manager for choosing specific certificate from key store
<sslCertificateAlias></sslCertificateAlias>
<sslCertificatePassword></sslCertificatePassword>
-->
</connectionPolicy>
</connectivity>
<procedure name="getFeeds"/>
<procedure name="getFeedsFiltered"/>
JS Code
var busyIndicator = null;
function wlCommonInit(){
busyIndicator = new WL.BusyIndicator();
loadFeeds();
}
function loadFeeds(){
busyIndicator.show();
/*
* The REST API works with all adapters and external resources, and is supported on the following hybrid environments:
* iOS, Android, Windows Phone 8, Windows 8.
* If your application supports other hybrid environments, see the tutorial for MobileFirst 6.3.
*/
var resourceRequest = new WLResourceRequest("/adapters/RSSReader/getFeedsFiltered", WLResourceRequest.GET);
resourceRequest.setQueryParameter("params", "['technology']");
resourceRequest.send().then(
loadFeedsSuccess,
loadFeedsFailure
);
}
function loadFeedsSuccess(result){
WL.Logger.debug("Feed retrieve success");
busyIndicator.hide();
if (result.responseJSON.Items.length>0)
displayFeeds(result.responseJSON.Items);
else
loadFeedsFailure();
}
function loadFeedsFailure(result){
WL.Logger.error("Feed retrieve failure");
busyIndicator.hide();
WL.SimpleDialog.show("Engadget Reader", "Service not available. Try again later.",
[{
text : 'Reload',
handler : WL.Client.reloadApp
},
{
text: 'Close',
handler : function() {}
}]
);
}
function displayFeeds(items){
var ul = $('#itemsList');
for (var i = 0; i < items.length; i++) {
var li = $('<li/>').text(items[i].title);
var pubDate = $('<div/>', {
'class': 'pubDate'
}).text(items[i].pubDate);
li.append(pubDate);
ul.append(li);
}
Xcode Console log
I had used the code give in the sample app.
I am unable to recreate this error.
Make sure these are the steps you've taken:
Import into MobileFirst Platform Studio 7.0 the "Invoking adapter procedures in hybrid applications" sample project
Right-click on the adapter folder and select Run As > Deploy MobileFirst Adapter
Right-click on the project folder and select New > MobileFirst Environment, add iPad environment
Right-click on the iPad folder and select Run As > Xcode project
Click Run in Xcode
For me, by following the above steps I was able to successfully run the sample application in the iOS Simulator = the app was displayed and feeds were retrieved.

Proper way to implement card.io SDK with Phonegap iOS?

I want to implement the card scanner from card.io SDK into Phonegap for iOS. I'm using Phonegap v2.9.0 and had went through this https://github.com/card-io/card.io-iOS-SDK-PhoneGap
I understand that it is a plugin, but I haven't created or implemented a plugin in Phonegap before. The code provided in the above link does not work for me. I don't know why, may be I am doing something wrong in the set up steps.
I want a clarification. Below are the steps given in gitHub
1) Add CardIOPGPlugin.[h|m] to your project (Plugins group).
2) Copy CardIOPGPlugin.js to your project's www folder. (If you don't have a www folder yet, run in the Simulator and follow the instructions in the build warnings.)
3) Add e.g. to your html.
See CardIOPGPlugin.js for detailed usage information.
4) Add the following to config.xml, for PhoneGap version 3.0+:
<feature name="CardIOPGPlugin"><param name="ios-package" value="CardIOPGPlugin" /></feature>
My doubts:
Which folder are they talking about in(1)? Plugins folder in Xcode? Or the Plugins folder in Project directory (directory that has config.xml). I have tried both but the sample code does not call the onCardIOCheck.
The gitHub for card.io SDK provides initial setup steps. (https://github.com/card-io/card.io-iOS-SDK)
There, in Step 2, they say "Add the CardIO directory (containing several .h files and libCardIO.a) to your Xcode project." How do I do this? Where do I copy the folder to?
And I have also done step 3 and 4 which are
3) In your project's Build Settings, add -lc++ to Other Linker Flags.
4) Add these frameworks to your project. Weak linking for iOS versions back to 5.0 is supported. (list of frameworks..)
I have done all, my onDeviceReady works, but window.plugins.card_io.canScan(onCardIOCheck); is not calling.
Please.. anyone who has done this before in PhoneGap and iOS, please provide a detailed explanation and steps to implement this in Phonegap iOS.
Providing my code: (app id is changed for question sake)
document.addEventListener("deviceready",onDeviceReady, false);
function onDeviceReady() {
alert('device ready');
var cardIOResponseFields = [
"card_type",
"redacted_card_number",
"card_number",
"expiry_month",
"expiry_year",
"cvv",
"zip"
];
var onCardIOComplete = function(response) {
alert("card.io scan complete");
for (var i = 0, len = cardIOResponseFields.length; i < len; i++) {
var field = cardIOResponseFields[i];
console.log(field + ": " + response[field]);
}
};
var onCardIOCancel = function() {
alert("card.io scan cancelled");
};
var onCardIOCheck = function (canScan) {
alert("card.io canScan? " + canScan);
var scanBtn = document.getElementById("scanBtn");
if (!canScan) {
scanBtn.innerHTML = "Manual entry";
}
scanBtn.onclick = function (e) {
window.plugins.card_io.scan(
"MYAPPIDCHANGEDFORQUESTION",
{
"collect_expiry": true,
"collect_cvv": false,
"collect_zip": false,
"shows_first_use_alert": true,
"disable_manual_entry_buttons": false
},
onCardIOComplete,
onCardIOCancel
);
}
};
window.plugins.card_io.canScan(onCardIOCheck);
}
Which folder are they talking about in(1)? Plugins folder in Xcode? Or the Plugins folder in Project directory (directory that has config.xml). I have tried both but the sample code does not call the onCardIOCheck.
the Plugins folder is the same, Xcode project just references the folder, when you copy files (depending on how you like to organize them) you would usually select copy and hence they will be places in the plugins folder, as long as the files compiled into the project it doesn't really matter.
There, in Step 2, they say "Add the CardIO directory (containing several .h files and libCardIO.a) to your Xcode project." How do I do this? Where do I copy the folder to?
It doesn't matter where you will copy the files as long as they are included in the Xcode project and compiled.
I have done all, my onDeviceReady works, but window.plugins.card_io.canScan(onCardIOCheck); is not calling.
Make sure the CardIOPGPlugin.js is loaded before the javascript files using it, it maybe undefined and hence fails.
Hope it helps.

Resources