Cross-device sync implementation #505

Merged
akinwale merged 38 commits from sync into master 2019-04-22 14:42:48 +02:00
396 changed files with 15157 additions and 12618 deletions

View file

@ -22,6 +22,7 @@ build apk:
- rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9 - rm -rf ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
- ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9 - ln -s ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21 ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-9
- cp -f $CI_PROJECT_DIR/scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh - cp -f $CI_PROJECT_DIR/scripts/build-target-python.sh ~/.buildozer/android/crystax-ndk-10.3.2/build/tools/build-target-python.sh
- cp -f $CI_PROJECT_DIR/scripts/mangled-glibc-syscalls.h ~/.buildozer/android/crystax-ndk-10.3.2/platforms/android-21/arch-arm/usr/include/crystax/bionic/libc/include/sys/mangled-glibc-syscalls.h
- rm ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz - rm ~/.buildozer/android/crystax-ndk-10.3.2-linux-x86_64.tar.xz
- git secret reveal - git secret reveal
- mv buildozer.spec.travis buildozer.spec - mv buildozer.spec.travis buildozer.spec

View file

@ -1,5 +1,5 @@
# LBRY Android # LBRY Android
[![Build Status](https://travis-ci.org/lbryio/lbry-android.svg?branch=master)](https://travis-ci.org/lbryio/lbry-android) [![pipeline status](https://ci.lbry.tech/lbry/lbry-android/badges/master/pipeline.svg)](https://ci.lbry.tech/lbry/lbry-android/commits/master)
An Android browser and wallet for the [LBRY](https://lbry.io) network. This app bundles [lbrynet-daemon](https://github.com/lbryio/lbry) as a background service with a UI layer built with React Native. The APK is built using buildozer and the Gradle build tool. An Android browser and wallet for the [LBRY](https://lbry.io) network. This app bundles [lbrynet-daemon](https://github.com/lbryio/lbry) as a background service with a UI layer built with React Native. The APK is built using buildozer and the Gradle build tool.

View file

@ -1,6 +1,7 @@
{ {
"presets": ["react-native"], "presets": ["module:metro-react-native-babel-preset"],
"plugins": [ "plugins": [
"@babel/plugin-proposal-nullish-coalescing-operator",
["module-resolver", { ["module-resolver", {
root: ["./src"], root: ["./src"],
}], }],

7180
app/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -8,23 +8,24 @@
"dependencies": { "dependencies": {
"base-64": "^0.1.0", "base-64": "^0.1.0",
"@expo/vector-icons": "^8.1.0", "@expo/vector-icons": "^8.1.0",
"lbry-redux": "lbryio/lbry-redux", "lbry-redux": "lbryio/lbry-redux#sync",
"lbryinc": "lbryio/lbryinc", "lbryinc": "lbryio/lbryinc#sync",
"lodash": ">=4.17.11", "lodash": ">=4.17.11",
"merge": ">=1.2.1", "merge": ">=1.2.1",
"moment": "^2.22.1", "moment": "^2.22.1",
"react": "16.2.0", "react": "16.8.6",
"react-native": "0.55.3", "react-native": "0.59.3",
"@react-native-community/async-storage": "^1.2.2",
"react-native-country-picker-modal": "^0.6.2", "react-native-country-picker-modal": "^0.6.2",
"react-native-exception-handler": "2.9.0", "react-native-exception-handler": "2.9.0",
"react-native-fast-image": "^5.0.3", "react-native-fast-image": "^5.0.3",
"rn-fetch-blob": "^0.10.15", "react-native-gesture-handler": "^1.1.0",
"react-native-image-zoom-viewer": "^2.2.5", "react-native-image-zoom-viewer": "^2.2.5",
"react-native-phone-input": "lbryio/react-native-phone-input", "react-native-phone-input": "lbryio/react-native-phone-input",
"react-native-vector-icons": "^5.0.0", "react-native-vector-icons": "^5.0.0",
"react-native-video": "lbryio/react-native-video#exoplayer-lbry-android", "react-native-video": "lbryio/react-native-video#exoplayer-lbry-android",
"react-navigation": "^2.18.3", "react-navigation": "^3.6.1",
"react-navigation-redux-helpers": "^2.0.9", "react-navigation-redux-helpers": "^3.0.0",
"react-redux": "^5.0.3", "react-redux": "^5.0.3",
"redux": "^3.6.0", "redux": "^3.6.0",
"redux-logger": "3.0.6", "redux-logger": "3.0.6",
@ -32,10 +33,13 @@
"redux-persist-filesystem-storage": "^1.3.2", "redux-persist-filesystem-storage": "^1.3.2",
"redux-persist-transform-compress": "^4.2.0", "redux-persist-transform-compress": "^4.2.0",
"redux-persist-transform-filter": "0.0.10", "redux-persist-transform-filter": "0.0.10",
"redux-thunk": "^2.2.0" "redux-thunk": "^2.2.0",
"rn-fetch-blob": "^0.10.15"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.4.3",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"babel-preset-react-native": "5.0.2",
"babel-preset-stage-2": "^6.18.0", "babel-preset-stage-2": "^6.18.0",
"babel-plugin-module-resolver": "^3.1.1", "babel-plugin-module-resolver": "^3.1.1",
"flow-babel-webpack-plugin": "^1.1.1" "flow-babel-webpack-plugin": "^1.1.1"

View file

@ -14,19 +14,19 @@ import TransactionHistoryPage from 'page/transactionHistory';
import WalletPage from 'page/wallet'; import WalletPage from 'page/wallet';
import SearchInput from 'component/searchInput'; import SearchInput from 'component/searchInput';
import { import {
createAppContainer,
createDrawerNavigator, createDrawerNavigator,
createStackNavigator, createStackNavigator,
NavigationActions NavigationActions
} from 'react-navigation'; } from 'react-navigation';
import { import {
addListener, addListener,
reduxifyNavigator, createReduxContainer,
createReactNavigationReduxMiddleware, createReactNavigationReduxMiddleware,
} from 'react-navigation-redux-helpers'; } from 'react-navigation-redux-helpers';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
AppState, AppState,
AsyncStorage,
BackHandler, BackHandler,
Linking, Linking,
NativeModules, NativeModules,
@ -37,6 +37,7 @@ import { doDeleteCompleteBlobs } from 'redux/actions/file';
import { selectDrawerStack } from 'redux/selectors/drawer'; import { selectDrawerStack } from 'redux/selectors/drawer';
import { SETTINGS, doDismissToast, doToast, selectToast } from 'lbry-redux'; import { SETTINGS, doDismissToast, doToast, selectToast } from 'lbry-redux';
import { import {
doGetSync,
doUserCheckEmailVerified, doUserCheckEmailVerified,
doUserEmailVerify, doUserEmailVerify,
doUserEmailVerifyFailure, doUserEmailVerifyFailure,
@ -48,6 +49,7 @@ import {
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { decode as atob } from 'base-64'; import { decode as atob } from 'base-64';
import { dispatchNavigateBack, dispatchNavigateToUri } from 'utils/helper'; import { dispatchNavigateBack, dispatchNavigateToUri } from 'utils/helper';
import AsyncStorage from '@react-native-community/async-storage';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import Constants from 'constants'; import Constants from 'constants';
import Icon from 'react-native-vector-icons/FontAwesome5'; import Icon from 'react-native-vector-icons/FontAwesome5';
@ -195,7 +197,7 @@ const drawer = createDrawerNavigator({
} }
}); });
export const AppNavigator = new createStackNavigator({ const mainStackNavigator = new createStackNavigator({
FirstRun: { FirstRun: {
screen: FirstRunScreen, screen: FirstRunScreen,
navigationOptions: { navigationOptions: {
@ -214,12 +216,11 @@ export const AppNavigator = new createStackNavigator({
}, { }, {
headerMode: 'none' headerMode: 'none'
}); });
export const AppNavigator = mainStackNavigator;
export const reactNavigationMiddleware = createReactNavigationReduxMiddleware( export const reactNavigationMiddleware = createReactNavigationReduxMiddleware(
"root",
state => state.nav, state => state.nav,
); );
const App = reduxifyNavigator(AppNavigator, "root"); const App = createReduxContainer(mainStackNavigator, "root");
const appMapStateToProps = (state) => ({ const appMapStateToProps = (state) => ({
state: state.nav, state: state.nav,
}); });
@ -280,13 +281,18 @@ class AppWithNavigationState extends React.Component {
} }
componentDidUpdate() { componentDidUpdate() {
const { user } = this.props; const { dispatch, user } = this.props;
if (this.state.verifyPending && this.emailVerifyCheckInterval > 0 && user && user.has_verified_email) { if (this.state.verifyPending && this.emailVerifyCheckInterval > 0 && user && user.has_verified_email) {
clearInterval(this.emailVerifyCheckInterval); clearInterval(this.emailVerifyCheckInterval);
AsyncStorage.setItem(Constants.KEY_EMAIL_VERIFY_PENDING, 'false'); AsyncStorage.setItem(Constants.KEY_EMAIL_VERIFY_PENDING, 'false');
this.setState({ verifyPending: false }); this.setState({ verifyPending: false });
ToastAndroid.show('Your email address was successfully verified.', ToastAndroid.LONG); ToastAndroid.show('Your email address was successfully verified.', ToastAndroid.LONG);
// upon successful email verification, check wallet sync
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
dispatch(doGetSync(walletPassword));
});
} }
} }

View file

@ -1,6 +1,7 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { import {
doUserEmailNew, doUserEmailNew,
doUserEmailToVerify,
doUserResendVerificationEmail, doUserResendVerificationEmail,
selectEmailNewErrorMessage, selectEmailNewErrorMessage,
selectEmailNewIsPending, selectEmailNewIsPending,
@ -17,6 +18,7 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
addUserEmail: email => dispatch(doUserEmailNew(email)), addUserEmail: email => dispatch(doUserEmailNew(email)),
setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
notify: data => dispatch(doToast(data)), notify: data => dispatch(doToast(data)),
resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email)) resendVerificationEmail: email => dispatch(doUserResendVerificationEmail(email))
}); });

View file

@ -2,42 +2,43 @@
import React from 'react'; import React from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
AsyncStorage,
Text, Text,
TextInput, TextInput,
TouchableOpacity, TouchableOpacity,
View View
} from 'react-native'; } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import Icon from 'react-native-vector-icons/FontAwesome5'; import Icon from 'react-native-vector-icons/FontAwesome5';
import Button from '../button'; import Button from 'component/button';
import Colors from '../../styles/colors'; import Colors from 'styles/colors';
import Constants from '../../constants'; import Constants from 'constants';
import Link from '../link'; import Link from 'component/link';
import rewardStyle from '../../styles/reward'; import rewardStyle from 'styles/reward';
class EmailRewardSubcard extends React.PureComponent { class EmailRewardSubcard extends React.PureComponent {
state = { constructor(props) {
super(props);
this.state = {
email: null, email: null,
emailAlreadySet: false, emailAlreadySet: false,
previousEmail: null, previousEmail: null,
verfiyStarted: false verfiyStarted: false
}; };
}
componentDidMount() { componentDidMount() {
const { emailToVerify } = this.props; const { setEmailToVerify } = this.props;
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => { AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => setEmailToVerify(email));
if (email && email.trim().length > 0) {
this.setState({ email, emailAlreadySet: true, previousEmail: email });
} else {
this.setState({ email: emailToVerify, previousEmail: emailToVerify });
}
});
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const { emailNewErrorMessage, emailNewPending } = nextProps; const { emailNewErrorMessage, emailNewPending, emailToVerify } = nextProps;
const { notify } = this.props; const { notify } = this.props;
if (emailToVerify && emailToVerify.trim().length > 0 && !this.state.email && !this.state.previousEmail) {
this.setState({ email: emailToVerify, previousEmail: emailToVerify, emailAlreadySet: true });
}
if (this.state.verifyStarted && !emailNewPending) { if (this.state.verifyStarted && !emailNewPending) {
if (emailNewErrorMessage) { if (emailNewErrorMessage) {
notify({ message: String(emailNewErrorMessage), isError: true }); notify({ message: String(emailNewErrorMessage), isError: true });

View file

@ -1,10 +1,10 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectBalance } from 'lbry-redux'; import { selectTotalBalance } from 'lbry-redux';
import { selectUnclaimedRewardValue } from 'lbryinc';
import FloatingWalletBalance from './view'; import FloatingWalletBalance from './view';
import { doRewardList, selectUnclaimedRewardValue, selectFetchingRewards, selectUser } from 'lbryinc';
const select = state => ({ const select = state => ({
balance: selectBalance(state), balance: selectTotalBalance(state),
unclaimedRewardAmount: selectUnclaimedRewardValue(state), unclaimedRewardAmount: selectUnclaimedRewardValue(state),
}); });

View file

@ -18,19 +18,18 @@ class FloatingWalletBalance extends React.PureComponent<Props> {
return ( return (
<View style={[floatingButtonStyle.view, floatingButtonStyle.bottomRight]}> <View style={[floatingButtonStyle.view, floatingButtonStyle.bottomRight]}>
<TouchableOpacity style={floatingButtonStyle.container}
onPress={() => navigation && navigation.navigate({ routeName: 'WalletStack' })}>
{isNaN(balance) && <ActivityIndicator size="small" color={Colors.White} />}
<Text style={floatingButtonStyle.text}>
{(balance || balance === 0) && (formatCredits(parseFloat(balance), 2) + ' LBC')}
</Text>
</TouchableOpacity>
{unclaimedRewardAmount > 0 && {unclaimedRewardAmount > 0 &&
<TouchableOpacity style={floatingButtonStyle.pendingContainer} <TouchableOpacity style={floatingButtonStyle.pendingContainer}
onPress={() => navigation && navigation.navigate({ routeName: 'Rewards' })} > onPress={() => navigation && navigation.navigate({ routeName: 'Rewards' })} >
<Icon name="award" size={18} style={floatingButtonStyle.rewardIcon} /> <Icon name="award" size={18} style={floatingButtonStyle.rewardIcon} />
<Text style={floatingButtonStyle.text}>{unclaimedRewardAmount}</Text> <Text style={floatingButtonStyle.text}>{unclaimedRewardAmount}</Text>
</TouchableOpacity>} </TouchableOpacity>}
<TouchableOpacity style={floatingButtonStyle.container}
onPress={() => navigation && navigation.navigate({ routeName: 'WalletStack' })}>
{isNaN(balance) && <ActivityIndicator size="small" color={Colors.White} />}
{(!isNaN(balance) || balance === 0) && (
<Text style={floatingButtonStyle.text}>{(formatCredits(parseFloat(balance), 2) + ' LBC')}</Text>)}
</TouchableOpacity>
</View> </View>
); );
} }

View file

@ -2,7 +2,6 @@
import React from 'react'; import React from 'react';
import { import {
ActivityIndicator, ActivityIndicator,
AsyncStorage,
DeviceEventEmitter, DeviceEventEmitter,
NativeModules, NativeModules,
StyleSheet, StyleSheet,
@ -11,6 +10,7 @@ import {
TouchableOpacity, TouchableOpacity,
View View
} from 'react-native'; } from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import Button from 'component/button'; import Button from 'component/button';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
import Constants from 'constants'; import Constants from 'constants';

View file

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import { AsyncStorage, NativeModules, Text, TouchableOpacity, View } from 'react-native'; import { NativeModules, Text, TouchableOpacity, View } from 'react-native';
import Button from '../../component/button'; import AsyncStorage from '@react-native-community/async-storage';
import rewardStyle from '../../styles/reward'; import Button from 'component/button';
import rewardStyle from 'styles/reward';
class RewardSummary extends React.Component { class RewardSummary extends React.Component {
static itemKey = 'rewardSummaryDismissed'; static itemKey = 'rewardSummaryDismissed';

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator, SectionList, Text, View } from 'react-native'; import { ActivityIndicator, SectionList, Text, View } from 'react-native';
import { normalizeURI } from 'lbry-redux'; import { normalizeURI } from 'lbry-redux';
import { navigateToUri } from 'utils/helper';
import SubscribeButton from 'component/subscribeButton'; import SubscribeButton from 'component/subscribeButton';
import SuggestedSubscriptionItem from 'component/suggestedSubscriptionItem'; import SuggestedSubscriptionItem from 'component/suggestedSubscriptionItem';
import Colors from 'styles/colors'; import Colors from 'styles/colors';
@ -36,7 +37,9 @@ class SuggestedSubscriptions extends React.PureComponent {
const channelUri = normalizeURI(titleParts[1]); const channelUri = normalizeURI(titleParts[1]);
return ( return (
<View style={subscriptionsStyle.titleRow}> <View style={subscriptionsStyle.titleRow}>
<Link style={subscriptionsStyle.channelTitle} text={channelName} href={channelUri} /> <Link style={subscriptionsStyle.channelTitle} text={channelName} onPress={() => {
navigateToUri(navigation, normalizeURI(channelUri));
}} />
<SubscribeButton style={subscriptionsStyle.subscribeButton} uri={channelUri} name={channelName} /> <SubscribeButton style={subscriptionsStyle.subscribeButton} uri={channelUri} name={channelName} />
</View> </View>
) )

View file

@ -101,9 +101,9 @@ class UriBar extends React.PureComponent {
} }
let style = [uriBarStyle.overlay]; let style = [uriBarStyle.overlay];
if (this.state.focused) {
style.push(uriBarStyle.inFocus); // TODO: Add optional setting to enable URI / search bar suggestions
} /*if (this.state.focused) { style.push(uriBarStyle.inFocus); }*/
return ( return (
<View style={style}> <View style={style}>
@ -152,7 +152,6 @@ class UriBar extends React.PureComponent {
} }
} }
}}/> }}/>
</View>
{(this.state.focused && !this.state.directSearch) && ( {(this.state.focused && !this.state.directSearch) && (
<View style={uriBarStyle.suggestions}> <View style={uriBarStyle.suggestions}>
<FlatList style={uriBarStyle.suggestionList} <FlatList style={uriBarStyle.suggestionList}
@ -167,6 +166,7 @@ class UriBar extends React.PureComponent {
/>)} /> />)} />
</View>)} </View>)}
</View> </View>
</View>
); );
} }
} }

View file

@ -1,9 +1,9 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { selectBalance } from 'lbry-redux'; import { selectTotalBalance } from 'lbry-redux';
import WalletBalance from './view'; import WalletBalance from './view';
const select = state => ({ const select = state => ({
balance: selectBalance(state), balance: selectTotalBalance(state),
}); });
export default connect(select, null)(WalletBalance); export default connect(select, null)(WalletBalance);

View file

@ -1,10 +1,10 @@
// @flow // @flow
import React from 'react'; import React from 'react';
import { Image, Text, View } from 'react-native'; import { Image, Text, View } from 'react-native';
import { formatCredits } from 'lbry-redux' import { Lbry, formatCredits } from 'lbry-redux'
import Address from '../address'; import Address from 'component/address';
import Button from '../button'; import Button from 'component/button';
import walletStyle from '../../styles/wallet'; import walletStyle from 'styles/wallet';
type Props = { type Props = {
balance: number, balance: number,

View file

@ -1,5 +1,11 @@
const Constants = { const Constants = {
FIRST_RUN_PAGE_WELCOME: "welcome",
FIRST_RUN_PAGE_EMAIL_COLLECT: "email-collect",
FIRST_RUN_PAGE_WALLET: "wallet",
FIRST_RUN_PAGE_SKIP_ACCOUNT: "skip-account",
KEY_FIRST_RUN_EMAIL: "firstRunEmail", KEY_FIRST_RUN_EMAIL: "firstRunEmail",
KEY_FIRST_RUN_PASSWORD: "firstRunPassword",
KEY_SHOULD_VERIFY_EMAIL: "shouldVerifyEmail", KEY_SHOULD_VERIFY_EMAIL: "shouldVerifyEmail",
KEY_EMAIL_VERIFY_PENDING: "emailVerifyPending", KEY_EMAIL_VERIFY_PENDING: "emailVerifyPending",

View file

@ -4,7 +4,6 @@ import { Provider, connect } from 'react-redux';
import { import {
AppRegistry, AppRegistry,
AppState, AppState,
AsyncStorage,
Text, Text,
View, View,
NativeModules NativeModules
@ -25,6 +24,7 @@ import {
homepageReducer, homepageReducer,
rewardsReducer, rewardsReducer,
subscriptionsReducer, subscriptionsReducer,
syncReducer,
userReducer userReducer
} from 'lbryinc'; } from 'lbryinc';
import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import { createStore, applyMiddleware, compose, combineReducers } from 'redux';
@ -32,6 +32,7 @@ import { createLogger } from 'redux-logger';
import { AppNavigator } from 'component/AppNavigator'; import { AppNavigator } from 'component/AppNavigator';
import { persistStore, autoRehydrate } from 'redux-persist'; import { persistStore, autoRehydrate } from 'redux-persist';
import AppWithNavigationState, { reactNavigationMiddleware } from './component/AppNavigator'; import AppWithNavigationState, { reactNavigationMiddleware } from './component/AppNavigator';
import AsyncStorage from '@react-native-community/async-storage';
import FilesystemStorage from 'redux-persist-filesystem-storage'; import FilesystemStorage from 'redux-persist-filesystem-storage';
import createCompressor from 'redux-persist-transform-compress'; import createCompressor from 'redux-persist-transform-compress';
import createFilter from 'redux-persist-transform-filter'; import createFilter from 'redux-persist-transform-filter';
@ -40,6 +41,7 @@ import drawerReducer from 'redux/reducers/drawer';
import settingsReducer from 'redux/reducers/settings'; import settingsReducer from 'redux/reducers/settings';
import thunk from 'redux-thunk'; import thunk from 'redux-thunk';
const globalExceptionHandler = (error, isFatal) => { const globalExceptionHandler = (error, isFatal) => {
if (error && NativeModules.Mixpanel) { if (error && NativeModules.Mixpanel) {
NativeModules.Mixpanel.logException(isFatal, error.message ? error.message : "No message", error); NativeModules.Mixpanel.logException(isFatal, error.message ? error.message : "No message", error);
@ -47,7 +49,6 @@ const globalExceptionHandler = (error, isFatal) => {
}; };
setJSExceptionHandler(globalExceptionHandler, true); setJSExceptionHandler(globalExceptionHandler, true);
function isFunction(object) { function isFunction(object) {
return typeof object === 'function'; return typeof object === 'function';
} }
@ -99,6 +100,7 @@ const reducers = combineReducers({
settings: settingsReducer, settings: settingsReducer,
search: searchReducer, search: searchReducer,
subscriptions: subscriptionsReducer, subscriptions: subscriptionsReducer,
sync: syncReducer,
user: userReducer, user: userReducer,
wallet: walletReducer wallet: walletReducer
}); });

View file

@ -3,7 +3,6 @@ import NavigationActions from 'react-navigation';
import { import {
Alert, Alert,
ActivityIndicator, ActivityIndicator,
AsyncStorage,
Linking, Linking,
NativeModules, NativeModules,
SectionList, SectionList,
@ -11,6 +10,7 @@ import {
View View
} from 'react-native'; } from 'react-native';
import { Lbry, normalizeURI, parseURI } from 'lbry-redux'; import { Lbry, normalizeURI, parseURI } from 'lbry-redux';
import AsyncStorage from '@react-native-community/async-storage';
import moment from 'moment'; import moment from 'moment';
import Constants from 'constants'; import Constants from 'constants';
import Colors from 'styles/colors'; import Colors from 'styles/colors';

View file

@ -591,6 +591,10 @@ class FilePage extends React.PureComponent {
show={DateTime.SHOW_DATE} /> show={DateTime.SHOW_DATE} />
</View> </View>
<View style={filePageStyle.subscriptionRow}> <View style={filePageStyle.subscriptionRow}>
<Button style={[filePageStyle.actionButton, filePageStyle.tipButton]}
theme={"light"}
icon={"gift"}
onPress={() => this.setState({ showTipView: true })} />
<SubscribeButton <SubscribeButton
style={filePageStyle.actionButton} style={filePageStyle.actionButton}
uri={fullChannelUri} uri={fullChannelUri}
@ -603,14 +607,9 @@ class FilePage extends React.PureComponent {
</View> </View>
} }
{(this.state.showDescription && description && description.length > 0) && <View style={filePageStyle.divider} />} {this.state.showTipView && <View style={filePageStyle.divider} />}
{(this.state.showDescription && description) && {this.state.showTipView &&
<Text style={filePageStyle.description} selectable={true}>{this.linkify(description)}</Text>} <View style={filePageStyle.tipCard}>
<View onLayout={this.setRelatedContentPosition} />
<RelatedContent navigation={navigation} uri={uri} />
</ScrollView>
{this.state.showTipView && <View style={filePageStyle.tipCard}>
<View style={filePageStyle.row}> <View style={filePageStyle.row}>
<View style={filePageStyle.amountRow}> <View style={filePageStyle.amountRow}>
<TextInput ref={ref => this.tipAmountInput = ref} <TextInput ref={ref => this.tipAmountInput = ref}
@ -621,12 +620,20 @@ class FilePage extends React.PureComponent {
<Text style={[filePageStyle.text, filePageStyle.currency]}>LBC</Text> <Text style={[filePageStyle.text, filePageStyle.currency]}>LBC</Text>
</View> </View>
<Link style={[filePageStyle.link, filePageStyle.cancelTipLink]} text={'Cancel'} onPress={() => this.setState({ showTipView: false })} /> <Link style={[filePageStyle.link, filePageStyle.cancelTipLink]} text={'Cancel'} onPress={() => this.setState({ showTipView: false })} />
<Button text={'Send tip'} <Button text={'Send a tip'}
style={[filePageStyle.button, filePageStyle.sendButton]} style={[filePageStyle.button, filePageStyle.sendButton]}
disabled={!canSendTip} disabled={!canSendTip}
onPress={this.handleSendTip} /> onPress={this.handleSendTip} />
</View> </View>
</View>} </View>}
{(this.state.showDescription && description && description.length > 0) && <View style={filePageStyle.divider} />}
{(this.state.showDescription && description) &&
<Text style={filePageStyle.description} selectable={true}>{this.linkify(description)}</Text>}
<View onLayout={this.setRelatedContentPosition} />
<RelatedContent navigation={navigation} uri={uri} />
</ScrollView>
</View> </View>
)} )}
{!this.state.fullscreenMode && <FloatingWalletBalance navigation={navigation} />} {!this.state.fullscreenMode && <FloatingWalletBalance navigation={navigation} />}

View file

@ -2,7 +2,6 @@ import React from 'react';
import { Lbry } from 'lbry-redux'; import { Lbry } from 'lbry-redux';
import { import {
ActivityIndicator, ActivityIndicator,
AsyncStorage,
Linking, Linking,
NativeModules, NativeModules,
Platform, Platform,
@ -10,9 +9,10 @@ import {
TextInput, TextInput,
View View
} from 'react-native'; } from 'react-native';
import Colors from '../../../styles/colors'; import AsyncStorage from '@react-native-community/async-storage';
import Constants from '../../../constants'; import Colors from 'styles/colors';
import firstRunStyle from '../../../styles/firstRun'; import Constants from 'constants';
import firstRunStyle from 'styles/firstRun';
class EmailCollectPage extends React.PureComponent { class EmailCollectPage extends React.PureComponent {
static MAX_STATUS_TRIES = 30; static MAX_STATUS_TRIES = 30;
@ -52,7 +52,7 @@ class EmailCollectPage extends React.PureComponent {
this.setState({ authenticationStarted: true, authenticationFailed: false }); this.setState({ authenticationStarted: true, authenticationFailed: false });
NativeModules.VersionInfo.getAppVersion().then(appVersion => { NativeModules.VersionInfo.getAppVersion().then(appVersion => {
Lbry.status().then(info => { Lbry.status().then(info => {
authenticate(appVersion, Platform.OS) authenticate(appVersion, Platform.OS);
}).catch(error => { }).catch(error => {
if (this.state.statusTries >= EmailCollectPage.MAX_STATUS_TRIES) { if (this.state.statusTries >= EmailCollectPage.MAX_STATUS_TRIES) {
this.setState({ authenticationFailed: true }); this.setState({ authenticationFailed: true });
@ -98,9 +98,7 @@ class EmailCollectPage extends React.PureComponent {
} else { } else {
content = ( content = (
<View onLayout={onEmailViewLayout}> <View onLayout={onEmailViewLayout}>
<Text style={firstRunStyle.title}>Rewards.</Text> <Text style={firstRunStyle.title}>Setup account</Text>
<Text style={firstRunStyle.paragraph}>You can earn LBRY Credits (LBC) rewards by completing various tasks in the app.</Text>
<Text style={firstRunStyle.paragraph}>Please provide a valid email address below to be able to claim your rewards.</Text>
<TextInput style={firstRunStyle.emailInput} <TextInput style={firstRunStyle.emailInput}
placeholder={this.state.placeholder} placeholder={this.state.placeholder}
underlineColorAndroid="transparent" underlineColorAndroid="transparent"
@ -117,7 +115,8 @@ class EmailCollectPage extends React.PureComponent {
} }
}} }}
/> />
<Text style={firstRunStyle.infoParagraph}>This information is disclosed only to LBRY, Inc. and not to the LBRY network. It is only required to earn LBRY rewards and may be used to sync usage data across devices.</Text> <Text style={firstRunStyle.paragraph}>An account will allow you to earn rewards and keep your content and settings synced.</Text>
<Text style={firstRunStyle.infoParagraph}>This information is disclosed only to LBRY, Inc. and not to the LBRY network.</Text>
</View> </View>
); );
} }

View file

@ -0,0 +1,52 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
ActivityIndicator,
AsyncStorage,
Linking,
NativeModules,
Platform,
Switch,
Text,
TextInput,
View
} from 'react-native';
import Colors from 'styles/colors';
import Constants from 'constants';
import Icon from 'react-native-vector-icons/FontAwesome5';
import firstRunStyle from 'styles/firstRun';
class SkipAccountPage extends React.PureComponent {
state = {
confirmed: false
};
render() {
const { onSkipAccountViewLayout, onSkipSwitchChanged } = this.props;
const content = (
<View onLayout={onSkipAccountViewLayout}>
<View style={firstRunStyle.row}>
<Icon name="exclamation-triangle" style={firstRunStyle.titleIcon} size={32} color={Colors.White} />
<Text style={firstRunStyle.title}>Are you sure?</Text>
</View>
<Text style={firstRunStyle.paragraph}>Without an account, you will not receive rewards, sync and backup services, or security updates.</Text>
<View style={[firstRunStyle.row, firstRunStyle.confirmContainer]}>
<View style={firstRunStyle.rowSwitch}>
<Switch value={this.state.confirmed} onValueChange={value => { this.setState({ confirmed: value }); onSkipSwitchChanged(value); }} />
</View>
<Text style={firstRunStyle.rowParagraph}>I understand that by uninstalling LBRY I will lose any balances or published content with no recovery option.</Text>
</View>
</View>
);
return (
<View style={firstRunStyle.container}>
{content}
</View>
);
}
}
export default SkipAccountPage;

View file

@ -0,0 +1,75 @@
import React from 'react';
import { Lbry } from 'lbry-redux';
import {
ActivityIndicator,
Linking,
NativeModules,
Platform,
Text,
TextInput,
View
} from 'react-native';
import AsyncStorage from '@react-native-community/async-storage';
import Colors from 'styles/colors';
import Constants from 'constants';
import firstRunStyle from 'styles/firstRun';
class WalletPage extends React.PureComponent {
state = {
password: null,
placeholder: 'password',
statusTries: 0
};
handleChangeText = (text) => {
// save the value to the state email
const { onPasswordChanged } = this.props;
this.setState({ password: text });
if (onPasswordChanged) {
onPasswordChanged(text);
}
if (NativeModules.UtilityModule) {
NativeModules.UtilityModule.setSecureValue(Constants.KEY_FIRST_RUN_PASSWORD, text);
// simply set any string value to indicate that a passphrase was set on first run
AsyncStorage.setItem(Constants.KEY_FIRST_RUN_PASSWORD, "true");
}
}
render() {
const { onPasswordChanged, onWalletViewLayout } = this.props;
const content = (
<View onLayout={onWalletViewLayout}>
<Text style={firstRunStyle.title}>Password</Text>
<Text style={firstRunStyle.paragraph}>Please enter a password to secure your account and wallet.</Text>
<TextInput style={firstRunStyle.passwordInput}
placeholder={this.state.placeholder}
underlineColorAndroid="transparent"
secureTextEntry={true}
value={this.state.password}
onChangeText={text => this.handleChangeText(text)}
onFocus={() => {
if (!this.state.password || this.state.password.length === 0) {
this.setState({ placeholder: '' });
}
}}
onBlur={() => {
if (!this.state.password || this.state.password.length === 0) {
this.setState({ placeholder: 'password' });
}
}}
/>
<Text style={firstRunStyle.infoParagraph}>Note: for wallet security purposes, LBRY is unable to reset your password.</Text>
</View>
);
return (
<View style={firstRunStyle.container}>
{content}
</View>
);
}
}
export default WalletPage;

View file

@ -1,18 +1,15 @@
import React from 'react'; import React from 'react';
import { Lbry } from 'lbry-redux'; import { Lbry } from 'lbry-redux';
import { View, Text, Linking } from 'react-native'; import { View, Text, Linking } from 'react-native';
import Colors from '../../../styles/colors'; import Colors from 'styles/colors';
import firstRunStyle from '../../../styles/firstRun'; import firstRunStyle from 'styles/firstRun';
class WelcomePage extends React.PureComponent { class WelcomePage extends React.PureComponent {
render() { render() {
return ( return (
<View style={firstRunStyle.container}> <View style={firstRunStyle.container}>
<Text style={firstRunStyle.title}>Welcome to LBRY.</Text> <Text style={firstRunStyle.title}>Welcome to LBRY.</Text>
<Text style={firstRunStyle.paragraph}>LBRY is a decentralized peer-to-peer content sharing platform where <Text style={firstRunStyle.paragraph}>LBRY is a community-controlled content platform where you can find and publish videos, music, books, and more.</Text>
you can upload and download videos, music, ebooks and other forms of digital content.</Text>
<Text style={firstRunStyle.paragraph}>We make use of a blockchain which needs to be synchronized before
you can use the app. Synchronization may take a while because this is the first app launch.</Text>
</View> </View>
); );
} }

View file

@ -2,7 +2,6 @@ import React from 'react';
import { Lbry } from 'lbry-redux'; import { Lbry } from 'lbry-redux';
import { import {
ActivityIndicator, ActivityIndicator,
AsyncStorage,
Linking, Linking,
NativeModules, NativeModules,
Text, Text,
@ -10,16 +9,21 @@ import {
View View
} from 'react-native'; } from 'react-native';
import { NavigationActions, StackActions } from 'react-navigation'; import { NavigationActions, StackActions } from 'react-navigation';
import Colors from '../../styles/colors'; import AsyncStorage from '@react-native-community/async-storage';
import Constants from '../../constants'; import Colors from 'styles/colors';
import Constants from 'constants';
import WalletPage from './internal/wallet-page';
import WelcomePage from './internal/welcome-page'; import WelcomePage from './internal/welcome-page';
import EmailCollectPage from './internal/email-collect-page'; import EmailCollectPage from './internal/email-collect-page';
import firstRunStyle from '../../styles/firstRun'; import SkipAccountPage from './internal/skip-account-page';
import firstRunStyle from 'styles/firstRun';
class FirstRunScreen extends React.PureComponent { class FirstRunScreen extends React.PureComponent {
static pages = [ static pages = [
'welcome', Constants.FIRST_RUN_PAGE_WELCOME,
'email-collect' Constants.FIRST_RUN_PAGE_EMAIL_COLLECT,
Constants.FIRST_RUN_PAGE_WALLET,
Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT,
]; ];
state = { state = {
@ -28,7 +32,9 @@ class FirstRunScreen extends React.PureComponent {
isFirstRun: false, isFirstRun: false,
launchUrl: null, launchUrl: null,
showSkip: false, showSkip: false,
showBottomContainer: true skipAccountConfirmed: false,
showBottomContainer: true,
walletPassword: null
}; };
componentDidMount() { componentDidMount() {
@ -63,8 +69,8 @@ class FirstRunScreen extends React.PureComponent {
if (emailNewErrorMessage) { if (emailNewErrorMessage) {
notify ({ message: String(emailNewErrorMessage), isError: true }); notify ({ message: String(emailNewErrorMessage), isError: true });
} else { } else {
// Request successful. Navigate to discover. // Request successful. Navigate to next page (wallet).
this.closeFinalPage(); this.showNextPage();
} }
} }
} }
@ -80,14 +86,40 @@ class FirstRunScreen extends React.PureComponent {
navigation.dispatch(resetAction); navigation.dispatch(resetAction);
} }
handleLeftButtonPressed = () => {
// Go to setup account page when "Setup account" is pressed
if (Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage) {
return this.showPage(Constants.FIRST_RUN_PAGE_EMAIL_COLLECT);
}
// Go to skip account page when "No, thanks" is pressed
if (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === this.state.currentPage) {
this.showPage(Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT);
}
}
handleContinuePressed = () => { handleContinuePressed = () => {
const { notify } = this.props;
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage); const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
if (this.state.currentPage !== 'email-collect' && if (Constants.FIRST_RUN_PAGE_WALLET === this.state.currentPage) {
pageIndex === (FirstRunScreen.pages.length - 1)) { if (!this.state.walletPassword || this.state.walletPassword.trim().length < 6) {
return notify({ message: 'Your wallet password should be at least 6 characters long' });
}
this.closeFinalPage();
return;
}
if (Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage && !this.state.skipAccountConfirmed) {
notify({ message: 'Please confirm that you want to use LBRY without creating an account.' });
return;
}
if (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT !== this.state.currentPage && pageIndex === (FirstRunScreen.pages.length - 1)) {
this.closeFinalPage(); this.closeFinalPage();
} else { } else {
// TODO: Actions and page verification for specific pages // TODO: Actions and page verification for specific pages
if (this.state.currentPage === 'email-collect') { if (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === this.state.currentPage) {
// handle email collect // handle email collect
this.handleEmailCollectPageContinue(); this.handleEmailCollectPageContinue();
} else { } else {
@ -98,21 +130,10 @@ class FirstRunScreen extends React.PureComponent {
handleEmailCollectPageContinue() { handleEmailCollectPageContinue() {
const { notify, addUserEmail } = this.props; const { notify, addUserEmail } = this.props;
const pageIndex = FirstRunScreen.pages.indexOf(this.state.currentPage);
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => { AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => {
if (!email || email.trim().length === 0) {
// no email provided. Skip.
if (this.state.currentPage === 'email-collect' && pageIndex === (FirstRunScreen.pages.length - 1)) {
this.closeFinalPage();
} else {
this.showNextPage();
}
return;
}
// validate the email // validate the email
if (email.indexOf('@') === -1) { if (!email || email.indexOf('@') === -1) {
return notify({ return notify({
message: 'Please provide a valid email address to continue.', message: 'Please provide a valid email address to continue.',
}); });
@ -133,6 +154,13 @@ class FirstRunScreen extends React.PureComponent {
} }
} }
showPage(pageName) {
const pageIndex = FirstRunScreen.pages.indexOf(pageName);
if (pageIndex > -1) {
this.setState({ currentPage: pageName });
}
}
closeFinalPage() { closeFinalPage() {
// Final page. Let the app know that first run experience is completed. // Final page. Let the app know that first run experience is completed.
if (NativeModules.FirstRun) { if (NativeModules.FirstRun) {
@ -144,7 +172,7 @@ class FirstRunScreen extends React.PureComponent {
} }
onEmailChanged = (email) => { onEmailChanged = (email) => {
if ('email-collect' == this.state.currentPage) { if (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT == this.state.currentPage) {
this.setState({ showSkip: (!email || email.trim().length === 0) }); this.setState({ showSkip: (!email || email.trim().length === 0) });
} else { } else {
this.setState({ showSkip: false }); this.setState({ showSkip: false });
@ -158,6 +186,18 @@ class FirstRunScreen extends React.PureComponent {
}); });
} }
onWalletPasswordChanged = (password) => {
this.setState({ walletPassword: password });
}
onWalletViewLayout = () => {
this.setState({ showBottomContainer: true });
}
onSkipSwitchChanged = (checked) => {
this.setState({ skipAccountConfirmed: checked });
}
render() { render() {
const { const {
authenticate, authenticate,
@ -169,15 +209,31 @@ class FirstRunScreen extends React.PureComponent {
} = this.props; } = this.props;
let page = null; let page = null;
if (this.state.currentPage === 'welcome') { switch (this.state.currentPage) {
// show welcome page case 'welcome':
page = (<WelcomePage />); page = (<WelcomePage />);
} else if (this.state.currentPage === 'email-collect') { break;
page = (<EmailCollectPage authenticating={authenticating}
case 'email-collect':
skhameneh commented 2019-04-19 20:36:16 +02:00 (Migrated from github.com)
Review

Use a switch

Use a switch
page = (<EmailCollectPage
authenticating={authenticating}
authToken={authToken} authToken={authToken}
authenticate={authenticate} authenticate={authenticate}
onEmailChanged={this.onEmailChanged} onEmailChanged={this.onEmailChanged}
onEmailViewLayout={this.onEmailViewLayout} />); onEmailViewLayout={this.onEmailViewLayout} />);
break;
case 'wallet':
page = (<WalletPage
onWalletViewLayout={this.onWalletViewLayout}
onPasswordChanged={this.onWalletPasswordChanged} />);
break;
case 'skip-account':
page = (<SkipAccountPage
onSkipAccountViewLayout={this.onSkipAccountViewLayout}
onSkipSwitchChanged={this.onSkipSwitchChanged} />);
break;
} }
return ( return (
@ -188,10 +244,25 @@ class FirstRunScreen extends React.PureComponent {
{emailNewPending && {emailNewPending &&
<ActivityIndicator size="small" color={Colors.White} style={firstRunStyle.pageWaiting} />} <ActivityIndicator size="small" color={Colors.White} style={firstRunStyle.pageWaiting} />}
<View style={firstRunStyle.buttonRow}>
{([Constants.FIRST_RUN_PAGE_WELCOME, Constants.FIRST_RUN_PAGE_WALLET].indexOf(this.state.currentPage) > -1) && <View />}
{Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage &&
<TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}>
<Text style={firstRunStyle.buttonText}>« Setup account</Text>
</TouchableOpacity>}
{!emailNewPending && (Constants.FIRST_RUN_PAGE_EMAIL_COLLECT === this.state.currentPage) &&
<TouchableOpacity style={firstRunStyle.leftButton} onPress={this.handleLeftButtonPressed}>
<Text style={firstRunStyle.smallLeftButtonText}>No, thanks »</Text>
</TouchableOpacity>}
{!emailNewPending && {!emailNewPending &&
<TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}> <TouchableOpacity style={firstRunStyle.button} onPress={this.handleContinuePressed}>
<Text style={firstRunStyle.buttonText}>{this.state.showSkip ? 'Skip': 'Continue'}</Text> {Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT === this.state.currentPage &&
<Text style={firstRunStyle.smallButtonText}>Use LBRY »</Text>}
{Constants.FIRST_RUN_PAGE_SKIP_ACCOUNT !== this.state.currentPage &&
<Text style={firstRunStyle.buttonText}>{Constants.FIRST_RUN_PAGE_WALLET === this.state.currentPage ? 'Use LBRY' : 'Continue'} »</Text>}
</TouchableOpacity>} </TouchableOpacity>}
</View>
</View>} </View>}
</View> </View>
); );

View file

@ -53,9 +53,18 @@ class SearchPage extends React.PureComponent {
<UriBar value={searchQuery} <UriBar value={searchQuery}
navigation={navigation} navigation={navigation}
onSearchSubmitted={this.handleSearchSubmitted} /> onSearchSubmitted={this.handleSearchSubmitted} />
{isSearching &&
<View style={searchStyle.busyContainer}>
<ActivityIndicator size="large" color={Colors.LbryGreen} style={searchStyle.loading} />
</View>}
{!isSearching && (!uris || uris.length === 0) && {!isSearching && (!uris || uris.length === 0) &&
<Text style={searchStyle.noResultsText}>No results to display.</Text>} <Text style={searchStyle.noResultsText}>No results to display.</Text>}
<ScrollView style={searchStyle.scrollContainer} contentContainerStyle={searchStyle.scrollPadding}> {!isSearching &&
<ScrollView
style={searchStyle.scrollContainer}
contentContainerStyle={searchStyle.scrollPadding}
skhameneh commented 2019-04-19 20:38:00 +02:00 (Migrated from github.com)
Review

Duplicated conditional, use fragments?

Duplicated conditional, use fragments?
keyboardShouldPersistTaps={'handled'}>
{this.state.currentUri && {this.state.currentUri &&
<FileListItem <FileListItem
key={this.state.currentUri} key={this.state.currentUri}
@ -65,15 +74,14 @@ class SearchPage extends React.PureComponent {
navigation={navigation} navigation={navigation}
onPress={() => navigateToUri(navigation, this.state.currentUri)} onPress={() => navigateToUri(navigation, this.state.currentUri)}
/>} />}
{!isSearching && uris && uris.length ? ( {(uris && uris.length) ? (
uris.map(uri => <FileListItem key={uri} uris.map(uri => <FileListItem key={uri}
uri={uri} uri={uri}
style={searchStyle.resultItem} style={searchStyle.resultItem}
navigation={navigation} navigation={navigation}
onPress={() => navigateToUri(navigation, uri)}/>) onPress={() => navigateToUri(navigation, uri)}/>)
) : null } ) : null }
</ScrollView> </ScrollView>}
{isSearching && <ActivityIndicator size="large" color={Colors.LbryGreen} style={searchStyle.loading} /> }
<FloatingWalletBalance navigation={navigation} /> <FloatingWalletBalance navigation={navigation} />
</View> </View>
); );

View file

@ -1,11 +1,12 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doBalanceSubscribe, doUpdateBlockHeight, doToast } from 'lbry-redux'; import { doTotalBalanceSubscribe, doUpdateBlockHeight, doToast } from 'lbry-redux';
import { import {
doAuthenticate, doAuthenticate,
doBlackListedOutpointsSubscribe, doBlackListedOutpointsSubscribe,
doCheckSubscriptionsInit, doCheckSubscriptionsInit,
doFetchMySubscriptions, doFetchMySubscriptions,
doFetchRewardedContent, doFetchRewardedContent,
doGetSync,
doUserEmailToVerify, doUserEmailToVerify,
doUserEmailVerify, doUserEmailVerify,
doUserEmailVerifyFailure, doUserEmailVerifyFailure,
@ -22,12 +23,13 @@ const select = state => ({
const perform = dispatch => ({ const perform = dispatch => ({
authenticate: (appVersion, os) => dispatch(doAuthenticate(appVersion, os)), authenticate: (appVersion, os) => dispatch(doAuthenticate(appVersion, os)),
balanceSubscribe: () => dispatch(doBalanceSubscribe()), totalBalanceSubscribe: () => dispatch(doTotalBalanceSubscribe()),
blacklistedOutpointsSubscribe: () => dispatch(doBlackListedOutpointsSubscribe()), blacklistedOutpointsSubscribe: () => dispatch(doBlackListedOutpointsSubscribe()),
checkSubscriptionsInit: () => dispatch(doCheckSubscriptionsInit()), checkSubscriptionsInit: () => dispatch(doCheckSubscriptionsInit()),
deleteCompleteBlobs: () => dispatch(doDeleteCompleteBlobs()), deleteCompleteBlobs: () => dispatch(doDeleteCompleteBlobs()),
fetchRewardedContent: () => dispatch(doFetchRewardedContent()), fetchRewardedContent: () => dispatch(doFetchRewardedContent()),
fetchSubscriptions: (callback) => dispatch(doFetchMySubscriptions(callback)), fetchSubscriptions: (callback) => dispatch(doFetchMySubscriptions(callback)),
getSync: password => dispatch(doGetSync(password)),
notify: data => dispatch(doToast(data)), notify: data => dispatch(doToast(data)),
setEmailToVerify: email => dispatch(doUserEmailToVerify(email)), setEmailToVerify: email => dispatch(doUserEmailToVerify(email)),
updateBlockHeight: () => dispatch(doUpdateBlockHeight()), updateBlockHeight: () => dispatch(doUpdateBlockHeight()),

View file

@ -2,7 +2,6 @@ import React from 'react';
import { Lbry } from 'lbry-redux'; import { Lbry } from 'lbry-redux';
import { import {
ActivityIndicator, ActivityIndicator,
AsyncStorage,
Linking, Linking,
NativeModules, NativeModules,
Platform, Platform,
@ -12,11 +11,12 @@ import {
} from 'react-native'; } from 'react-native';
import { NavigationActions, StackActions } from 'react-navigation'; import { NavigationActions, StackActions } from 'react-navigation';
import { decode as atob } from 'base-64'; import { decode as atob } from 'base-64';
import { navigateToUri } from '../../utils/helper'; import { navigateToUri } from 'utils/helper';
import AsyncStorage from '@react-native-community/async-storage';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Colors from '../../styles/colors'; import Colors from 'styles/colors';
import Constants from '../../constants'; import Constants from 'constants';
import splashStyle from '../../styles/splash'; import splashStyle from 'styles/splash';
const BLOCK_HEIGHT_INTERVAL = 1000 * 60 * 2.5; // every 2.5 minutes const BLOCK_HEIGHT_INTERVAL = 1000 * 60 * 2.5; // every 2.5 minutes
@ -63,24 +63,8 @@ class SplashScreen extends React.PureComponent {
}); });
} }
componentWillReceiveProps(nextProps) { navigateToMain = () => {
const { const { navigation } = this.props;
emailToVerify,
navigation,
setEmailToVerify,
verifyUserEmail,
verifyUserEmailFailure
} = this.props;
const { user } = nextProps;
if (this.state.daemonReady && this.state.shouldAuthenticate && user && user.id) {
this.setState({ shouldAuthenticate: false }, () => {
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => {
if (email) {
setEmailToVerify(email);
}
// user is authenticated, navigate to the main view
const resetAction = StackActions.reset({ const resetAction = StackActions.reset({
index: 0, index: 0,
actions: [ actions: [
@ -116,11 +100,65 @@ class SplashScreen extends React.PureComponent {
navigateToUri(navigation, launchUrl); navigateToUri(navigation, launchUrl);
} }
} }
}
componentWillReceiveProps(nextProps) {
const {
emailToVerify,
getSync,
setEmailToVerify,
verifyUserEmail,
verifyUserEmailFailure
} = this.props;
const { user } = nextProps;
if (this.state.daemonReady && this.state.shouldAuthenticate && user && user.id) {
this.setState({ shouldAuthenticate: false }, () => {
AsyncStorage.getItem(Constants.KEY_FIRST_RUN_EMAIL).then(email => {
if (email) {
setEmailToVerify(email);
}
// user is authenticated, navigate to the main view
if (user.has_verified_email) {
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => {
getSync(walletPassword);
this.navigateToMain();
});
return;
}
this.navigateToMain();
}); });
}); });
} }
} }
finishSplashScreen = () => {
const {
authenticate,
totalBalanceSubscribe,
blacklistedOutpointsSubscribe,
checkSubscriptionsInit,
updateBlockHeight,
navigation,
notify
} = this.props;
Lbry.resolve({ urls: 'lbry://one' }).then(() => {
// Leave the splash screen
totalBalanceSubscribe();
blacklistedOutpointsSubscribe();
checkSubscriptionsInit();
updateBlockHeight();
setInterval(() => { updateBlockHeight(); }, BLOCK_HEIGHT_INTERVAL);
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
this.setState({ shouldAuthenticate: true });
authenticate(appVersion, Platform.OS);
});
});
}
_updateStatusCallback(status) { _updateStatusCallback(status) {
const { deleteCompleteBlobs, fetchSubscriptions } = this.props; const { deleteCompleteBlobs, fetchSubscriptions } = this.props;
const startupStatus = status.startup_status; const startupStatus = status.startup_status;
@ -141,33 +179,40 @@ class SplashScreen extends React.PureComponent {
isRunning: true, isRunning: true,
}); });
// fetch subscriptions, so that we can check for new content after resolve AsyncStorage.getItem(Constants.KEY_FIRST_RUN_PASSWORD).then(passwordSet => {
Lbry.resolve({ urls: 'lbry://one' }).then(() => { if ("true" === passwordSet) {
// Leave the splash screen // encrypt the wallet
const { NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(password => {
authenticate, if (!password || password.trim().length === 0) {
balanceSubscribe, this.finishSplashScreen();
blacklistedOutpointsSubscribe, return;
checkSubscriptionsInit, }
updateBlockHeight,
navigation,
notify
} = this.props;
balanceSubscribe(); Lbry.account_encrypt({ new_password: password }).then((result) => {
blacklistedOutpointsSubscribe(); AsyncStorage.removeItem(Constants.KEY_FIRST_RUN_PASSWORD);
checkSubscriptionsInit(); Lbry.account_unlock({ password }).then(() => this.finishSplashScreen());
updateBlockHeight();
setInterval(() => { updateBlockHeight(); }, BLOCK_HEIGHT_INTERVAL);
NativeModules.VersionInfo.getAppVersion().then(appVersion => {
this.setState({ shouldAuthenticate: true });
authenticate(appVersion, Platform.OS);
}); });
}); });
return; return;
} }
// For now, automatically unlock the wallet if a password is set so that downloads work
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(password => {
if (password && password.trim().length > 0) {
// unlock the wallet and then finish the splash screen
Lbry.account_unlock({ password }).then(() => this.finishSplashScreen());
return;
}
this.finishSplashScreen();
});
});
return;
}
const blockchainHeaders = status.blockchain_headers; const blockchainHeaders = status.blockchain_headers;
const walletStatus = status.wallet; const walletStatus = status.wallet;

View file

@ -2,7 +2,6 @@ import React from 'react';
import NavigationActions from 'react-navigation'; import NavigationActions from 'react-navigation';
import { import {
ActivityIndicator, ActivityIndicator,
AsyncStorage,
FlatList, FlatList,
NativeModules, NativeModules,
SectionList, SectionList,
@ -12,6 +11,7 @@ import {
} from 'react-native'; } from 'react-native';
import { buildURI, parseURI } from 'lbry-redux'; import { buildURI, parseURI } from 'lbry-redux';
import { uriFromFileInfo } from 'utils/helper'; import { uriFromFileInfo } from 'utils/helper';
import AsyncStorage from '@react-native-community/async-storage';
import moment from 'moment'; import moment from 'moment';
import Button from 'component/button'; import Button from 'component/button';
import Colors from 'styles/colors'; import Colors from 'styles/colors';

View file

@ -2,13 +2,13 @@ import React from 'react';
import NavigationActions from 'react-navigation'; import NavigationActions from 'react-navigation';
import { import {
ActivityIndicator, ActivityIndicator,
AsyncStorage,
NativeModules, NativeModules,
FlatList, FlatList,
Text, Text,
View View
} from 'react-native'; } from 'react-native';
import { normalizeURI } from 'lbry-redux'; import { normalizeURI } from 'lbry-redux';
import AsyncStorage from '@react-native-community/async-storage';
import moment from 'moment'; import moment from 'moment';
import FileItem from '/component/fileItem'; import FileItem from '/component/fileItem';
import discoverStyle from 'styles/discover'; import discoverStyle from 'styles/discover';

View file

@ -2,14 +2,17 @@ import { connect } from 'react-redux';
import { doSetClientSetting } from 'redux/actions/settings'; import { doSetClientSetting } from 'redux/actions/settings';
import { makeSelectClientSetting } from 'redux/selectors/settings'; import { makeSelectClientSetting } from 'redux/selectors/settings';
import { doPushDrawerStack } from 'redux/actions/drawer'; import { doPushDrawerStack } from 'redux/actions/drawer';
import { doGetSync, selectUser } from 'lbryinc';
import Constants from 'constants'; import Constants from 'constants';
import WalletPage from './view'; import WalletPage from './view';
const select = state => ({ const select = state => ({
user: selectUser(state),
understandsRisks: makeSelectClientSetting(Constants.SETTING_ALPHA_UNDERSTANDS_RISKS)(state), understandsRisks: makeSelectClientSetting(Constants.SETTING_ALPHA_UNDERSTANDS_RISKS)(state),
}); });
const perform = dispatch => ({ const perform = dispatch => ({
getSync: password => dispatch(doGetSync(password)),
setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)), setClientSetting: (key, value) => dispatch(doSetClientSetting(key, value)),
pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_WALLET)) pushDrawerStack: () => dispatch(doPushDrawerStack(Constants.DRAWER_ROUTE_WALLET))
}); });

View file

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { ScrollView, Text, View } from 'react-native'; import { NativeModules, ScrollView, Text, View } from 'react-native';
import TransactionListRecent from 'component/transactionListRecent'; import TransactionListRecent from 'component/transactionListRecent';
import WalletAddress from 'component/walletAddress'; import WalletAddress from 'component/walletAddress';
import WalletBalance from 'component/walletBalance'; import WalletBalance from 'component/walletBalance';
@ -13,6 +13,11 @@ import walletStyle from 'styles/wallet';
class WalletPage extends React.PureComponent { class WalletPage extends React.PureComponent {
componentDidMount() { componentDidMount() {
this.props.pushDrawerStack(); this.props.pushDrawerStack();
const { user, getSync } = this.props;
if (user && user.has_verified_email) {
NativeModules.UtilityModule.getSecureValue(Constants.KEY_FIRST_RUN_PASSWORD).then(walletPassword => getSync(walletPassword));
}
} }
render() { render() {

View file

@ -246,11 +246,8 @@ const filePageStyle = StyleSheet.create({
color: "rgba(64, 184, 154, .2)" color: "rgba(64, 184, 154, .2)"
}, },
tipCard: { tipCard: {
backgroundColor: Colors.White,
position: 'absolute',
top: containedMediaHeightWithControls - 16,
width: '100%', width: '100%',
paddingTop: 8, marginTop: -12,
paddingBottom: 8, paddingBottom: 8,
paddingLeft: 16, paddingLeft: 16,
paddingRight: 16 paddingRight: 16
@ -298,6 +295,9 @@ const filePageStyle = StyleSheet.create({
fontFamily: 'Inter-UI-Regular', fontFamily: 'Inter-UI-Regular',
fontSize: 16, fontSize: 16,
lineHeight: 24 lineHeight: 24
},
tipButton: {
marginRight: 8
} }
}); });

View file

@ -2,6 +2,11 @@ import { StyleSheet } from 'react-native';
import Colors from './colors'; import Colors from './colors';
const firstRunStyle = StyleSheet.create({ const firstRunStyle = StyleSheet.create({
row: {
flexDirection: 'row',
marginLeft: 32,
marginRight: 32,
},
screenContainer: { screenContainer: {
flex: 1, flex: 1,
backgroundColor: Colors.LbryGreen backgroundColor: Colors.LbryGreen
@ -37,6 +42,21 @@ const firstRunStyle = StyleSheet.create({
marginBottom: 20, marginBottom: 20,
color: Colors.White color: Colors.White
}, },
confirmContainer: {
marginTop: 36
},
rowParagraph: {
fontFamily: 'Inter-UI-Regular',
fontSize: 16,
lineHeight: 24,
color: Colors.White,
flex: 0.7
},
rowSwitch: {
justifyContent: 'flex-start',
flex: 0.2,
marginRight: 8
},
emailInput: { emailInput: {
fontFamily: 'Inter-UI-Regular', fontFamily: 'Inter-UI-Regular',
fontSize: 24, fontSize: 24,
@ -46,15 +66,30 @@ const firstRunStyle = StyleSheet.create({
marginBottom: 20, marginBottom: 20,
textAlign: 'center' textAlign: 'center'
}, },
passwordInput: {
fontFamily: 'Inter-UI-Regular',
fontSize: 24,
lineHeight: 24,
marginLeft: 32,
marginRight: 32,
marginBottom: 20,
textAlign: 'center'
},
leftButton: { leftButton: {
flex: 1, flex: 1,
alignSelf: 'flex-start', alignSelf: 'flex-end',
paddingBottom: 16,
marginLeft: 32, marginLeft: 32,
marginRight: 32 marginRight: 32
}, },
bottomContainer: { bottomContainer: {
flex: 1 flex: 1
}, },
buttonRow: {
flex: 1,
flexDirection: 'row',
justifyContent: 'space-between'
},
actionButton: { actionButton: {
backgroundColor: Colors.White, backgroundColor: Colors.White,
alignSelf: 'center', alignSelf: 'center',
@ -71,14 +106,29 @@ const firstRunStyle = StyleSheet.create({
}, },
buttonText: { buttonText: {
fontFamily: 'Inter-UI-Regular', fontFamily: 'Inter-UI-Regular',
fontSize: 24, fontSize: 18,
color: Colors.White color: Colors.White
}, },
smallButtonText: {
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
color: Colors.White,
marginBottom: -2
},
smallLeftButtonText: {
fontFamily: 'Inter-UI-Regular',
fontSize: 14,
color: Colors.White,
marginBottom: 6
},
waiting: { waiting: {
marginBottom: 24 marginBottom: 24
}, },
pageWaiting: { pageWaiting: {
alignSelf: 'center' alignSelf: 'center'
},
titleIcon: {
marginTop: 8
} }
}); });

View file

@ -30,9 +30,9 @@ const floatingButtonStyle = StyleSheet.create({
pendingContainer: { pendingContainer: {
borderRadius: 24, borderRadius: 24,
padding: 14, padding: 14,
paddingLeft: 70, paddingLeft: 20,
paddingRight: 20, paddingRight: 70,
marginLeft: -60, marginRight: -60,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: Colors.BrighterLbryGreen, backgroundColor: Colors.BrighterLbryGreen,

View file

@ -4,15 +4,16 @@ import Colors from 'styles/colors';
const searchStyle = StyleSheet.create({ const searchStyle = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
justifyContent: 'center',
alignItems: 'center'
}, },
scrollContainer: { scrollContainer: {
flex: 1, flex: 1,
width: '100%',
height: '100%',
marginTop: 60 marginTop: 60
}, },
busyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
},
scrollPadding: { scrollPadding: {
paddingBottom: 16 paddingBottom: 16
}, },

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements # (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy # comma seperated e.g. requirements = sqlite3,kivy
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, miniupnpc==1.9, gmpy, appdirs==1.4.3, argparse==1.2.1, docopt, base58==1.0.0, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2, pyyaml, qrcode==5.2.2, requests, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, txJSON-RPC, zope.interface==4.3.3, protobuf==3.6.1, keyring==10.4.0, txupnp, git+https://github.com/lbryio/lbryschema.git#egg=lbryschema, git+https://github.com/lbryio/lbry.git@v0.32.4#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#egg=aioupnp, asn1crypto, treq==17.8.0, funcsigs, mock, pbr, pyopenssl, twisted, idna, Automat, hyperlink, PyHamcrest, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, idna_ssl==1.1.0, typing_extensions==3.6.5, yarl, chardet==3.0.4, async_timeout==3.0.1, aiorpcX==0.9.0, git+https://github.com/lbryio/torba#egg=torba, coincurve requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2018.11.29, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, git+https://github.com/lbryio/lbry.git@v0.34.0#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, git+https://github.com/lbryio/torba#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru
# (str) Custom source folders for requirements # (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes # Sets custom source for any requirements with recipes
@ -89,7 +89,7 @@ fullscreen = 0
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE
# (int) Android API to use # (int) Android API to use
android.api = 27 android.api = 28
# (int) Minimum API required # (int) Minimum API required
android.minapi = 21 android.minapi = 21
@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # bootstrap)
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828 android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828
# (str) python-for-android branch to use, defaults to master # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements # (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy # comma seperated e.g. requirements = sqlite3,kivy
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, miniupnpc==1.9, gmpy, appdirs==1.4.3, argparse==1.2.1, docopt, base58==1.0.0, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2, pyyaml, qrcode==5.2.2, requests, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, txJSON-RPC, zope.interface==4.3.3, protobuf==3.6.1, keyring==10.4.0, txupnp, git+https://github.com/lbryio/lbryschema.git#egg=lbryschema, git+https://github.com/lbryio/lbry.git@v0.32.4#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#egg=aioupnp, asn1crypto, treq==17.8.0, funcsigs, mock, pbr, pyopenssl, twisted, idna, Automat, hyperlink, PyHamcrest, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, idna_ssl==1.1.0, typing_extensions==3.6.5, yarl, chardet==3.0.4, async_timeout==3.0.1, aiorpcX==0.9.0, git+https://github.com/lbryio/torba#egg=torba, coincurve requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2018.11.29, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, git+https://github.com/lbryio/lbry.git@v0.34.0#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, git+https://github.com/lbryio/torba#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru
# (str) Custom source folders for requirements # (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes # Sets custom source for any requirements with recipes
@ -89,7 +89,7 @@ fullscreen = 0
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE
# (int) Android API to use # (int) Android API to use
android.api = 27 android.api = 28
# (int) Minimum API required # (int) Minimum API required
android.minapi = 21 android.minapi = 21
@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # bootstrap)
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828 android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828
# (str) python-for-android branch to use, defaults to master # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable

View file

@ -36,7 +36,7 @@ version.filename = %(source.dir)s/main.py
# (list) Application requirements # (list) Application requirements
# comma seperated e.g. requirements = sqlite3,kivy # comma seperated e.g. requirements = sqlite3,kivy
requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro, pyjnius, certifi==2018.4.16, constantly, incremental, miniupnpc==1.9, gmpy, appdirs==1.4.3, argparse==1.2.1, docopt, base58==1.0.0, colorama==0.3.7, dnspython==1.12.0, ecdsa==0.13, envparse, jsonrpclib==0.1.7, jsonschema==2.5.1, pbkdf2, pyyaml, qrcode==5.2.2, requests, seccure==0.3.1.3, attrs==18.1.0, pyasn1, pyasn1-modules, service_identity==16.0.0, six==1.9.0, txJSON-RPC, zope.interface==4.3.3, protobuf==3.6.1, keyring==10.4.0, txupnp, git+https://github.com/lbryio/lbryschema.git#egg=lbryschema, git+https://github.com/lbryio/lbry.git@v0.32.4#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#egg=aioupnp, asn1crypto, treq==17.8.0, funcsigs, mock, pbr, pyopenssl, twisted, idna, Automat, hyperlink, PyHamcrest, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, idna_ssl==1.1.0, typing_extensions==3.6.5, yarl, chardet==3.0.4, async_timeout==3.0.1, aiorpcX==0.9.0, git+https://github.com/lbryio/torba#egg=torba, coincurve requirements = python3crystax, openssl, sqlite3, hostpython3crystax, android, distro==1.4.0, pyjnius, certifi==2018.11.29, appdirs==1.4.3, docopt==0.6.2, base58==1.0.0, colorama==0.3.7, ecdsa==0.13, jsonschema==2.6.0, pbkdf2==1.3, pyyaml, protobuf==3.6.1, keyring==10.4.0, git+https://github.com/lbryio/lbry.git@v0.34.0#egg=lbrynet, git+https://github.com/lbryio/aioupnp.git#egg=aioupnp, asn1crypto, mock, netifaces, cryptography, aiohttp==3.5.4, multidict==4.5.2, yarl==1.3.0, chardet==3.0.4, async_timeout==3.0.1, git+https://github.com/lbryio/torba#egg=torba, coincurve, msgpack==0.6.1, six, attrs==18.2.0, pylru
# (str) Custom source folders for requirements # (str) Custom source folders for requirements
# Sets custom source for any requirements with recipes # Sets custom source for any requirements with recipes
@ -89,7 +89,7 @@ fullscreen = 0
android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE android.permissions = ACCESS_NETWORK_STATE,BLUETOOTH,INTERNET,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE
# (int) Android API to use # (int) Android API to use
android.api = 27 android.api = 28
# (int) Minimum API required # (int) Minimum API required
android.minapi = 21 android.minapi = 21
@ -148,7 +148,7 @@ android.react_src = ./app
# (list) Gradle dependencies to add (currently works only with sdl2_gradle # (list) Gradle dependencies to add (currently works only with sdl2_gradle
# bootstrap) # bootstrap)
android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.55.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828 android.gradle_dependencies = com.android.support:support-v4:27.1.1, com.android.support:support-media-compat:27.1.1, com.android.support:appcompat-v7:27.1.1, com.facebook.react:react-native:0.59.3, com.mixpanel.android:mixpanel-android:5+, com.google.android.gms:play-services-gcm:11.0.4+, com.facebook.fresco:fresco:1.9.0, com.facebook.fresco:animated-gif:1.9.0, com.squareup.picasso:picasso:2.71828
# (str) python-for-android branch to use, defaults to master # (str) python-for-android branch to use, defaults to master
#p4a.branch = stable #p4a.branch = stable

View file

@ -1,10 +1,10 @@
from os.path import (join, dirname)
from os import environ, uname
import sys
from distutils.spawn import find_executable from distutils.spawn import find_executable
from os import environ
from os.path import (exists, join, dirname, split)
from glob import glob
from pythonforandroid.logger import warning
from pythonforandroid.recipe import Recipe from pythonforandroid.recipe import Recipe
from pythonforandroid.util import BuildInterruptingException, build_platform
class Arch(object): class Arch(object):
@ -19,6 +19,12 @@ class Arch(object):
super(Arch, self).__init__() super(Arch, self).__init__()
self.ctx = ctx self.ctx = ctx
# Allows injecting additional linker paths used by any recipe.
# This can also be modified by recipes (like the librt recipe)
# to make sure that some sort of global resource is available &
# linked for all others.
self.extra_global_link_paths = []
def __str__(self): def __str__(self):
return self.arch return self.arch
@ -30,24 +36,65 @@ class Arch(object):
d.format(arch=self)) d.format(arch=self))
for d in self.ctx.include_dirs] for d in self.ctx.include_dirs]
def get_env(self, with_flags_in_cc=True): @property
def target(self):
target_data = self.command_prefix.split('-')
return '-'.join(
[target_data[0], 'none', target_data[1], target_data[2]])
def get_env(self, with_flags_in_cc=True, clang=False):
env = {} env = {}
env["CFLAGS"] = " ".join([ cflags = [
"-DANDROID", "-mandroid", "-fomit-frame-pointer", '-DANDROID',
"--sysroot", self.ctx.ndk_platform]) '-fomit-frame-pointer',
'-D__ANDROID_API__={}'.format(self.ctx.ndk_api)]
if not clang:
cflags.append('-mandroid')
else:
cflags.append('-target ' + self.target)
toolchain = '{android_host}-{toolchain_version}'.format(
android_host=self.ctx.toolchain_prefix,
toolchain_version=self.ctx.toolchain_version)
toolchain = join(self.ctx.ndk_dir, 'toolchains', toolchain,
'prebuilt', build_platform)
cflags.append('-gcc-toolchain {}'.format(toolchain))
env['CFLAGS'] = ' '.join(cflags)
# Link the extra global link paths first before anything else
# (such that overriding system libraries with them is possible)
env['LDFLAGS'] = ' ' + " ".join([
"-L'" + l.replace("'", "'\"'\"'") + "'" # no shlex.quote in py2
for l in self.extra_global_link_paths
]) + ' '
sysroot = join(self.ctx._ndk_dir, 'sysroot')
if exists(sysroot):
# post-15 NDK per
# https://android.googlesource.com/platform/ndk/+/ndk-r15-release/docs/UnifiedHeaders.md
env['CFLAGS'] += ' -isystem {}/sysroot/usr/include/{}'.format(
self.ctx.ndk_dir, self.ctx.toolchain_prefix)
env['CFLAGS'] += ' -I{}/sysroot/usr/include/{}'.format(
self.ctx.ndk_dir, self.command_prefix)
else:
sysroot = self.ctx.ndk_platform
env['CFLAGS'] += ' -I{}'.format(self.ctx.ndk_platform)
env['CFLAGS'] += ' -isysroot {} '.format(sysroot)
env['CFLAGS'] += '-I' + join(self.ctx.get_python_install_dir(),
'include/python{}'.format(
self.ctx.python_recipe.version[0:3])
)
env['LDFLAGS'] += '--sysroot={} '.format(self.ctx.ndk_platform)
env["CXXFLAGS"] = env["CFLAGS"] env["CXXFLAGS"] = env["CFLAGS"]
env["LDFLAGS"] = " ".join(['-lm', '-L' + self.ctx.get_libs_dir(self.arch)]) env["LDFLAGS"] += " ".join(['-lm', '-L' + self.ctx.get_libs_dir(self.arch)])
if self.ctx.ndk == 'crystax': if self.ctx.ndk == 'crystax':
env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch) env['LDFLAGS'] += ' -L{}/sources/crystax/libs/{} -lcrystax'.format(self.ctx.ndk_dir, self.arch)
py_platform = sys.platform
if py_platform in ['linux2', 'linux3']:
py_platform = 'linux'
toolchain_prefix = self.ctx.toolchain_prefix toolchain_prefix = self.ctx.toolchain_prefix
toolchain_version = self.ctx.toolchain_version toolchain_version = self.ctx.toolchain_version
command_prefix = self.command_prefix command_prefix = self.command_prefix
@ -63,53 +110,71 @@ class Arch(object):
env['NDK_CCACHE'] = self.ctx.ccache env['NDK_CCACHE'] = self.ctx.ccache
env.update({k: v for k, v in environ.items() if k.startswith('CCACHE_')}) env.update({k: v for k, v in environ.items() if k.startswith('CCACHE_')})
cc = find_executable('{command_prefix}-gcc'.format( if clang:
command_prefix=command_prefix), path=environ['PATH']) llvm_dirname = split(
glob(join(self.ctx.ndk_dir, 'toolchains', 'llvm*'))[-1])[-1]
clang_path = join(self.ctx.ndk_dir, 'toolchains', llvm_dirname,
'prebuilt', build_platform, 'bin')
environ['PATH'] = '{clang_path}:{path}'.format(
clang_path=clang_path, path=environ['PATH'])
exe = join(clang_path, 'clang')
execxx = join(clang_path, 'clang++')
else:
exe = '{command_prefix}-gcc'.format(command_prefix=command_prefix)
execxx = '{command_prefix}-g++'.format(command_prefix=command_prefix)
cc = find_executable(exe, path=environ['PATH'])
if cc is None: if cc is None:
print('Searching path are: {!r}'.format(environ['PATH'])) print('Searching path are: {!r}'.format(environ['PATH']))
warning('Couldn\'t find executable for CC. This indicates a ' raise BuildInterruptingException(
'Couldn\'t find executable for CC. This indicates a '
'problem locating the {} executable in the Android ' 'problem locating the {} executable in the Android '
'NDK, not that you don\'t have a normal compiler ' 'NDK, not that you don\'t have a normal compiler '
'installed. Exiting.') 'installed. Exiting.'.format(exe))
exit(1)
if with_flags_in_cc: if with_flags_in_cc:
env['CC'] = '{ccache}{command_prefix}-gcc {cflags}'.format( env['CC'] = '{ccache}{exe} {cflags}'.format(
command_prefix=command_prefix, exe=exe,
ccache=ccache, ccache=ccache,
cflags=env['CFLAGS']) cflags=env['CFLAGS'])
env['CXX'] = '{ccache}{command_prefix}-g++ {cxxflags}'.format( env['CXX'] = '{ccache}{execxx} {cxxflags}'.format(
command_prefix=command_prefix, execxx=execxx,
ccache=ccache, ccache=ccache,
cxxflags=env['CXXFLAGS']) cxxflags=env['CXXFLAGS'])
else: else:
env['CC'] = '{ccache}{command_prefix}-gcc'.format( env['CC'] = '{ccache}{exe}'.format(
command_prefix=command_prefix, exe=exe,
ccache=ccache) ccache=ccache)
env['CXX'] = '{ccache}{command_prefix}-g++'.format( env['CXX'] = '{ccache}{execxx}'.format(
command_prefix=command_prefix, execxx=execxx,
ccache=ccache) ccache=ccache)
env['AR'] = '{}-ar'.format(command_prefix) env['AR'] = '{}-ar'.format(command_prefix)
env['RANLIB'] = '{}-ranlib'.format(command_prefix) env['RANLIB'] = '{}-ranlib'.format(command_prefix)
env['LD'] = '{}-ld'.format(command_prefix) env['LD'] = '{}-ld'.format(command_prefix)
# env['LDSHARED'] = join(self.ctx.root_dir, 'tools', 'liblink') env['LDSHARED'] = env["CC"] + " -pthread -shared " +\
# env['LDSHARED'] = env['LD'] "-Wl,-O1 -Wl,-Bsymbolic-functions "
if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax:
# For crystax python, we can't use the host python headers:
env["CFLAGS"] += ' -I{}/sources/python/{}/include/python/'.\
format(self.ctx.ndk_dir, self.ctx.python_recipe.version[0:3])
env['STRIP'] = '{}-strip --strip-unneeded'.format(command_prefix) env['STRIP'] = '{}-strip --strip-unneeded'.format(command_prefix)
env['MAKE'] = 'make -j5' env['MAKE'] = 'make -j5'
env['READELF'] = '{}-readelf'.format(command_prefix) env['READELF'] = '{}-readelf'.format(command_prefix)
env['NM'] = '{}-nm'.format(command_prefix) env['NM'] = '{}-nm'.format(command_prefix)
hostpython_recipe = Recipe.get_recipe('hostpython2', self.ctx) hostpython_recipe = Recipe.get_recipe(
'host' + self.ctx.python_recipe.name, self.ctx)
# AND: This hardcodes python version 2.7, needs fixing
env['BUILDLIB_PATH'] = join( env['BUILDLIB_PATH'] = join(
hostpython_recipe.get_build_dir(self.arch), hostpython_recipe.get_build_dir(self.arch),
'build', 'lib.linux-{}-2.7'.format(uname()[-1])) 'build', 'lib.{}-{}'.format(
build_platform, self.ctx.python_recipe.major_minor_version_string)
)
env['PATH'] = environ['PATH'] env['PATH'] = environ['PATH']
env['ARCH'] = self.arch env['ARCH'] = self.arch
env['NDK_API'] = 'android-{}'.format(str(self.ctx.ndk_api))
if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax: if self.ctx.python_recipe and self.ctx.python_recipe.from_crystax:
env['CRYSTAX_PYTHON_VERSION'] = self.ctx.python_recipe.version env['CRYSTAX_PYTHON_VERSION'] = self.ctx.python_recipe.version
@ -123,12 +188,18 @@ class ArchARM(Arch):
command_prefix = 'arm-linux-androideabi' command_prefix = 'arm-linux-androideabi'
platform_dir = 'arch-arm' platform_dir = 'arch-arm'
@property
def target(self):
target_data = self.command_prefix.split('-')
return '-'.join(
['armv7a', 'none', target_data[1], target_data[2]])
class ArchARMv7_a(ArchARM): class ArchARMv7_a(ArchARM):
arch = 'armeabi-v7a' arch = 'armeabi-v7a'
def get_env(self, with_flags_in_cc=True): def get_env(self, with_flags_in_cc=True, clang=False):
env = super(ArchARMv7_a, self).get_env(with_flags_in_cc) env = super(ArchARMv7_a, self).get_env(with_flags_in_cc, clang=clang)
env['CFLAGS'] = (env['CFLAGS'] + env['CFLAGS'] = (env['CFLAGS'] +
(' -march=armv7-a -mfloat-abi=softfp ' (' -march=armv7-a -mfloat-abi=softfp '
'-mfpu=vfp -mthumb')) '-mfpu=vfp -mthumb'))
@ -142,8 +213,8 @@ class Archx86(Arch):
command_prefix = 'i686-linux-android' command_prefix = 'i686-linux-android'
platform_dir = 'arch-x86' platform_dir = 'arch-x86'
def get_env(self, with_flags_in_cc=True): def get_env(self, with_flags_in_cc=True, clang=False):
env = super(Archx86, self).get_env(with_flags_in_cc) env = super(Archx86, self).get_env(with_flags_in_cc, clang=clang)
env['CFLAGS'] = (env['CFLAGS'] + env['CFLAGS'] = (env['CFLAGS'] +
' -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32') ' -march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32')
env['CXXFLAGS'] = env['CFLAGS'] env['CXXFLAGS'] = env['CFLAGS']
@ -152,12 +223,12 @@ class Archx86(Arch):
class Archx86_64(Arch): class Archx86_64(Arch):
arch = 'x86_64' arch = 'x86_64'
toolchain_prefix = 'x86' toolchain_prefix = 'x86_64'
command_prefix = 'x86_64-linux-android' command_prefix = 'x86_64-linux-android'
platform_dir = 'arch-x86' platform_dir = 'arch-x86_64'
def get_env(self, with_flags_in_cc=True): def get_env(self, with_flags_in_cc=True, clang=False):
env = super(Archx86_64, self).get_env(with_flags_in_cc) env = super(Archx86_64, self).get_env(with_flags_in_cc, clang=clang)
env['CFLAGS'] = (env['CFLAGS'] + env['CFLAGS'] = (env['CFLAGS'] +
' -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel') ' -march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel')
env['CXXFLAGS'] = env['CFLAGS'] env['CXXFLAGS'] = env['CFLAGS']
@ -170,8 +241,8 @@ class ArchAarch_64(Arch):
command_prefix = 'aarch64-linux-android' command_prefix = 'aarch64-linux-android'
platform_dir = 'arch-arm64' platform_dir = 'arch-arm64'
def get_env(self, with_flags_in_cc=True): def get_env(self, with_flags_in_cc=True, clang=False):
env = super(ArchAarch_64, self).get_env(with_flags_in_cc) env = super(ArchAarch_64, self).get_env(with_flags_in_cc, clang=clang)
incpath = ' -I' + join(dirname(__file__), 'includes', 'arm64-v8a') incpath = ' -I' + join(dirname(__file__), 'includes', 'arm64-v8a')
env['EXTRA_CFLAGS'] = incpath env['EXTRA_CFLAGS'] = incpath
env['CFLAGS'] += incpath env['CFLAGS'] += incpath

View file

@ -1,17 +1,39 @@
from os.path import (join, dirname, isdir, splitext, basename) from os.path import (join, dirname, isdir, normpath, splitext, basename)
from os import listdir from os import listdir, walk, sep
import sh import sh
import shlex
import glob import glob
import json
import importlib import importlib
import os
import shutil
from pythonforandroid.logger import (warning, shprint, info, logger, from pythonforandroid.logger import (warning, shprint, info, logger,
debug) debug)
from pythonforandroid.util import (current_directory, ensure_dir, from pythonforandroid.util import (current_directory, ensure_dir,
temp_directory, which) temp_directory)
from pythonforandroid.recipe import Recipe from pythonforandroid.recipe import Recipe
def copy_files(src_root, dest_root, override=True):
for root, dirnames, filenames in walk(src_root):
for filename in filenames:
subdir = normpath(root.replace(src_root, ""))
if subdir.startswith(sep): # ensure it is relative
subdir = subdir[1:]
dest_dir = join(dest_root, subdir)
if not os.path.exists(dest_dir):
os.makedirs(dest_dir)
src_file = join(root, filename)
dest_file = join(dest_dir, filename)
if os.path.isfile(src_file):
if override and os.path.exists(dest_file):
os.unlink(dest_file)
if not os.path.exists(dest_file):
shutil.copy(src_file, dest_file)
else:
os.makedirs(dest_file)
class Bootstrap(object): class Bootstrap(object):
'''An Android project template, containing recipe stuff for '''An Android project template, containing recipe stuff for
compilation and templated fields for APK info. compilation and templated fields for APK info.
@ -27,7 +49,11 @@ class Bootstrap(object):
dist_name = None dist_name = None
distribution = None distribution = None
recipe_depends = ['sdl2'] # All bootstraps should include Python in some way:
recipe_depends = [
("python2", "python2legacy", "python3", "python3crystax"),
'android',
]
can_be_chosen_automatically = True can_be_chosen_automatically = True
'''Determines whether the bootstrap can be chosen as one that '''Determines whether the bootstrap can be chosen as one that
@ -78,6 +104,9 @@ class Bootstrap(object):
def get_dist_dir(self, name): def get_dist_dir(self, name):
return join(self.ctx.dist_dir, name) return join(self.ctx.dist_dir, name)
def get_common_dir(self):
return os.path.abspath(join(self.bootstrap_dir, "..", 'common'))
@property @property
def name(self): def name(self):
modname = self.__class__.__module__ modname = self.__class__.__module__
@ -87,9 +116,10 @@ class Bootstrap(object):
'''Ensure that a build dir exists for the recipe. This same single '''Ensure that a build dir exists for the recipe. This same single
dir will be used for building all different archs.''' dir will be used for building all different archs.'''
self.build_dir = self.get_build_dir() self.build_dir = self.get_build_dir()
shprint(sh.cp, '-r', self.common_dir = self.get_common_dir()
join(self.bootstrap_dir, 'build'), copy_files(join(self.bootstrap_dir, 'build'), self.build_dir)
self.build_dir) copy_files(join(self.common_dir, 'build'), self.build_dir,
override=False)
if self.ctx.symlink_java_src: if self.ctx.symlink_java_src:
info('Symlinking java src instead of copying') info('Symlinking java src instead of copying')
shprint(sh.rm, '-r', join(self.build_dir, 'src')) shprint(sh.rm, '-r', join(self.build_dir, 'src'))
@ -102,26 +132,15 @@ class Bootstrap(object):
fileh.write('target=android-{}'.format(self.ctx.android_api)) fileh.write('target=android-{}'.format(self.ctx.android_api))
def prepare_dist_dir(self, name): def prepare_dist_dir(self, name):
# self.dist_dir = self.get_dist_dir(name)
ensure_dir(self.dist_dir) ensure_dir(self.dist_dir)
def run_distribute(self): def run_distribute(self):
# print('Default bootstrap being used doesn\'t know how ' self.distribution.save_info(self.dist_dir)
# 'to distribute...failing.')
# exit(1)
with current_directory(self.dist_dir):
info('Saving distribution info')
with open('dist_info.json', 'w') as fileh:
json.dump({'dist_name': self.ctx.dist_name,
'bootstrap': self.ctx.bootstrap.name,
'archs': [arch.arch for arch in self.ctx.archs],
'recipes': self.ctx.recipe_build_order + self.ctx.python_modules},
fileh)
@classmethod @classmethod
def list_bootstraps(cls): def list_bootstraps(cls):
'''Find all the available bootstraps and return them.''' '''Find all the available bootstraps and return them.'''
forbidden_dirs = ('__pycache__', ) forbidden_dirs = ('__pycache__', 'common')
bootstraps_dir = join(dirname(__file__), 'bootstraps') bootstraps_dir = join(dirname(__file__), 'bootstraps')
for name in listdir(bootstraps_dir): for name in listdir(bootstraps_dir):
if name in forbidden_dirs: if name in forbidden_dirs:
@ -152,7 +171,7 @@ class Bootstrap(object):
for recipe in recipes: for recipe in recipes:
try: try:
recipe = Recipe.get_recipe(recipe, ctx) recipe = Recipe.get_recipe(recipe, ctx)
except IOError: except ValueError:
conflicts = [] conflicts = []
else: else:
conflicts = recipe.conflicts conflicts = recipe.conflicts
@ -160,7 +179,7 @@ class Bootstrap(object):
for conflict in conflicts]): for conflict in conflicts]):
ok = False ok = False
break break
if ok: if ok and bs not in acceptable_bootstraps:
acceptable_bootstraps.append(bs) acceptable_bootstraps.append(bs)
info('Found {} acceptable bootstraps: {}'.format( info('Found {} acceptable bootstraps: {}'.format(
len(acceptable_bootstraps), len(acceptable_bootstraps),
@ -249,16 +268,22 @@ class Bootstrap(object):
info('Python was loaded from CrystaX, skipping strip') info('Python was loaded from CrystaX, skipping strip')
return return
env = arch.get_env() env = arch.get_env()
strip = which('arm-linux-androideabi-strip', env['PATH']) tokens = shlex.split(env['STRIP'])
if strip is None: strip = sh.Command(tokens[0])
warning('Can\'t find strip in PATH...') if len(tokens) > 1:
return strip = strip.bake(tokens[1:])
strip = sh.Command(strip)
filens = shprint(sh.find, join(self.dist_dir, 'private'), libs_dir = join(self.dist_dir, '_python_bundle',
join(self.dist_dir, 'libs'), '_python_bundle', 'modules')
if self.ctx.python_recipe.name == 'python2legacy':
libs_dir = join(self.dist_dir, 'private')
filens = shprint(sh.find, libs_dir, join(self.dist_dir, 'libs'),
'-iname', '*.so', _env=env).stdout.decode('utf-8') '-iname', '*.so', _env=env).stdout.decode('utf-8')
logger.info('Stripping libraries in private dir') logger.info('Stripping libraries in private dir')
for filen in filens.split('\n'): for filen in filens.split('\n'):
if not filen:
continue # skip the last ''
try: try:
strip(filen, _env=env) strip(filen, _env=env)
except sh.ErrorReturnCode_1: except sh.ErrorReturnCode_1:

View file

@ -0,0 +1,22 @@
# This file is used to override default values used by the Ant build system.
#
# This file must be checked into Version Control Systems, as it is
# integral to the build system of your project.
# This file is only used by the Ant script.
# You can use this to override default values such as
# 'source.dir' for the location of your java source folder and
# 'out.dir' for the location of your output folder.
# You can also use it define how the release builds are signed by declaring
# the following properties:
# 'key.store' for the location of your keystore and
# 'key.alias' for the name of the key to use.
# The password will be asked during the build when you use the 'release' target.
source.absolute.dir = tmp-src
resource.absolute.dir = src/main/res
asset.absolute.dir = src/main/assets

View file

@ -0,0 +1,795 @@
#!/usr/bin/env python2.7
from __future__ import print_function
import json
from os.path import (
dirname, join, isfile, realpath,
relpath, split, exists, basename
)
from os import listdir, makedirs, remove
import os
import shlex
import shutil
import subprocess
import sys
import tarfile
import tempfile
import time
from zipfile import ZipFile
from distutils.version import LooseVersion
from fnmatch import fnmatch
import jinja2
def get_dist_info_for(key):
try:
with open(join(dirname(__file__), 'dist_info.json'), 'r') as fileh:
info = json.load(fileh)
value = str(info[key])
except (OSError, KeyError) as e:
print("BUILD FAILURE: Couldn't extract the key `" + key + "` " +
"from dist_info.json: " + str(e))
sys.exit(1)
return value
def get_hostpython():
return get_dist_info_for('hostpython')
def get_python_version():
return get_dist_info_for('python_version')
def get_bootstrap_name():
return get_dist_info_for('bootstrap')
if os.name == 'nt':
ANDROID = 'android.bat'
ANT = 'ant.bat'
else:
ANDROID = 'android'
ANT = 'ant'
curdir = dirname(__file__)
PYTHON = get_hostpython()
PYTHON_VERSION = get_python_version()
if PYTHON is not None and not exists(PYTHON):
PYTHON = None
BLACKLIST_PATTERNS = [
# code versionning
'^*.hg/*',
'^*.git/*',
'^*.bzr/*',
'^*.svn/*',
# temp files
'~',
'*.bak',
'*.swp',
]
# pyc/py
if PYTHON is not None:
BLACKLIST_PATTERNS.append('*.py')
if PYTHON_VERSION and int(PYTHON_VERSION[0]) == 2:
# we only blacklist `.pyc` for python2 because in python3 the compiled
# extension is `.pyc` (.pyo files not exists for python >= 3.6)
BLACKLIST_PATTERNS.append('*.pyc')
WHITELIST_PATTERNS = []
if get_bootstrap_name() in ('sdl2', 'webview', 'service_only'):
WHITELIST_PATTERNS.append('pyconfig.h')
python_files = []
environment = jinja2.Environment(loader=jinja2.FileSystemLoader(
join(curdir, 'templates')))
def try_unlink(fn):
if exists(fn):
os.unlink(fn)
def ensure_dir(path):
if not exists(path):
makedirs(path)
def render(template, dest, **kwargs):
'''Using jinja2, render `template` to the filename `dest`, supplying the
keyword arguments as template parameters.
'''
dest_dir = dirname(dest)
if dest_dir and not exists(dest_dir):
makedirs(dest_dir)
template = environment.get_template(template)
text = template.render(**kwargs)
f = open(dest, 'wb')
f.write(text.encode('utf-8'))
f.close()
def is_whitelist(name):
return match_filename(WHITELIST_PATTERNS, name)
def is_blacklist(name):
if is_whitelist(name):
return False
return match_filename(BLACKLIST_PATTERNS, name)
def match_filename(pattern_list, name):
for pattern in pattern_list:
if pattern.startswith('^'):
pattern = pattern[1:]
else:
pattern = '*/' + pattern
if fnmatch(name, pattern):
return True
def listfiles(d):
basedir = d
subdirlist = []
for item in os.listdir(d):
fn = join(d, item)
if isfile(fn):
yield fn
else:
subdirlist.append(join(basedir, item))
for subdir in subdirlist:
for fn in listfiles(subdir):
yield fn
def make_python_zip():
'''
Search for all the python related files, and construct the pythonXX.zip
According to
# http://randomsplat.com/id5-cross-compiling-python-for-embedded-linux.html
site-packages, config and lib-dynload will be not included.
'''
if not exists('private'):
print('No compiled python is present to zip, skipping.')
return
global python_files
d = realpath(join('private', 'lib', 'python2.7'))
def select(fn):
if is_blacklist(fn):
return False
fn = realpath(fn)
assert(fn.startswith(d))
fn = fn[len(d):]
if (fn.startswith('/site-packages/')
or fn.startswith('/config/')
or fn.startswith('/lib-dynload/')
or fn.startswith('/libpymodules.so')):
return False
return fn
# get a list of all python file
python_files = [x for x in listfiles(d) if select(x)]
# create the final zipfile
zfn = join('private', 'lib', 'python27.zip')
zf = ZipFile(zfn, 'w')
# put all the python files in it
for fn in python_files:
afn = fn[len(d):]
zf.write(fn, afn)
zf.close()
def make_tar(tfn, source_dirs, ignore_path=[], optimize_python=True):
'''
Make a zip file `fn` from the contents of source_dis.
'''
# selector function
def select(fn):
rfn = realpath(fn)
for p in ignore_path:
if p.endswith('/'):
p = p[:-1]
if rfn.startswith(p):
return False
if rfn in python_files:
return False
return not is_blacklist(fn)
# get the files and relpath file of all the directory we asked for
files = []
for sd in source_dirs:
sd = realpath(sd)
compile_dir(sd, optimize_python=optimize_python)
files += [(x, relpath(realpath(x), sd)) for x in listfiles(sd)
if select(x)]
# create tar.gz of thoses files
tf = tarfile.open(tfn, 'w:gz', format=tarfile.USTAR_FORMAT)
dirs = []
for fn, afn in files:
dn = dirname(afn)
if dn not in dirs:
# create every dirs first if not exist yet
d = ''
for component in split(dn):
d = join(d, component)
if d.startswith('/'):
d = d[1:]
if d == '' or d in dirs:
continue
dirs.append(d)
tinfo = tarfile.TarInfo(d)
tinfo.type = tarfile.DIRTYPE
tf.addfile(tinfo)
# put the file
tf.add(fn, afn)
tf.close()
def compile_dir(dfn, optimize_python=True):
'''
Compile *.py in directory `dfn` to *.pyo
'''
if PYTHON is None:
return
if int(PYTHON_VERSION[0]) >= 3:
args = [PYTHON, '-m', 'compileall', '-b', '-f', dfn]
else:
args = [PYTHON, '-m', 'compileall', '-f', dfn]
if optimize_python:
# -OO = strip docstrings
args.insert(1, '-OO')
return_code = subprocess.call(args)
if return_code != 0:
print('Error while running "{}"'.format(' '.join(args)))
print('This probably means one of your Python files has a syntax '
'error, see logs above')
exit(1)
def make_package(args):
# If no launcher is specified, require a main.py/main.pyo:
if (get_bootstrap_name() != "sdl" or args.launcher is None) and \
get_bootstrap_name() != "webview":
# (webview doesn't need an entrypoint, apparently)
if args.private is None or (
not exists(join(realpath(args.private), 'main.py')) and
not exists(join(realpath(args.private), 'main.pyo'))):
print('''BUILD FAILURE: No main.py(o) found in your app directory. This
file must exist to act as the entry point for you app. If your app is
started by a file with a different name, rename it to main.py or add a
main.py that loads it.''')
sys.exit(1)
assets_dir = "src/main/assets"
# Delete the old assets.
try_unlink(join(assets_dir, 'public.mp3'))
try_unlink(join(assets_dir, 'private.mp3'))
ensure_dir(assets_dir)
# In order to speedup import and initial depack,
# construct a python27.zip
make_python_zip()
# Add extra environment variable file into tar-able directory:
env_vars_tarpath = tempfile.mkdtemp(prefix="p4a-extra-env-")
with open(os.path.join(env_vars_tarpath, "p4a_env_vars.txt"), "w") as f:
f.write("P4A_IS_WINDOWED=" + str(args.window) + "\n")
if hasattr(args, "orientation"):
f.write("P4A_ORIENTATION=" + str(args.orientation) + "\n")
f.write("P4A_NUMERIC_VERSION=" + str(args.numeric_version) + "\n")
f.write("P4A_MINSDK=" + str(args.min_sdk_version) + "\n")
# Package up the private data (public not supported).
tar_dirs = [env_vars_tarpath]
if args.private:
tar_dirs.append(args.private)
for python_bundle_dir in ('private', 'crystax_python', '_python_bundle'):
if exists(python_bundle_dir):
tar_dirs.append(python_bundle_dir)
if get_bootstrap_name() == "webview":
tar_dirs.append('webview_includes')
if args.private or args.launcher:
make_tar(
join(assets_dir, 'private.mp3'), tar_dirs, args.ignore_path,
optimize_python=args.optimize_python)
# Remove extra env vars tar-able directory:
shutil.rmtree(env_vars_tarpath)
# Prepare some variables for templating process
res_dir = "src/main/res"
default_icon = 'templates/kivy-icon.png'
default_presplash = 'templates/kivy-presplash.jpg'
shutil.copy(
args.icon or default_icon,
join(res_dir, 'drawable/icon.png')
)
if get_bootstrap_name() != "service_only":
shutil.copy(
args.presplash or default_presplash,
join(res_dir, 'drawable/presplash.jpg')
)
# If extra Java jars were requested, copy them into the libs directory
jars = []
if args.add_jar:
for jarname in args.add_jar:
if not exists(jarname):
print('Requested jar does not exist: {}'.format(jarname))
sys.exit(-1)
shutil.copy(jarname, 'src/main/libs')
jars.append(basename(jarname))
# If extra aar were requested, copy them into the libs directory
aars = []
if args.add_aar:
ensure_dir("libs")
for aarname in args.add_aar:
if not exists(aarname):
print('Requested aar does not exists: {}'.format(aarname))
sys.exit(-1)
shutil.copy(aarname, 'libs')
aars.append(basename(aarname).rsplit('.', 1)[0])
versioned_name = (args.name.replace(' ', '').replace('\'', '') +
'-' + args.version)
version_code = 0
if not args.numeric_version:
# Set version code in format (arch-minsdk-app_version)
with open(join(dirname(__file__), 'dist_info.json'), 'r') as dist_info:
dist_data = json.load(dist_info)
arch = dist_data["archs"][0]
arch_dict = {"x86_64": "9", "arm64-v8a": "8", "armeabi-v7a": "7", "x86": "6"}
arch_code = arch_dict.get(arch, '1')
min_sdk = args.min_sdk_version
for i in args.version.split('.'):
version_code *= 100
version_code += int(i)
args.numeric_version = "{}{}{}".format(arch_code, min_sdk, version_code)
if args.intent_filters:
with open(args.intent_filters) as fd:
args.intent_filters = fd.read()
if not args.add_activity:
args.add_activity = []
if not args.activity_launch_mode:
args.activity_launch_mode = ''
if args.extra_source_dirs:
esd = []
for spec in args.extra_source_dirs:
if ':' in spec:
specdir, specincludes = spec.split(':')
else:
specdir = spec
specincludes = '**'
esd.append((realpath(specdir), specincludes))
args.extra_source_dirs = esd
else:
args.extra_source_dirs = []
service = False
if args.private:
service_main = join(realpath(args.private), 'service', 'main.py')
if exists(service_main) or exists(service_main + 'o'):
service = True
service_names = []
for sid, spec in enumerate(args.services):
spec = spec.split(':')
name = spec[0]
entrypoint = spec[1]
options = spec[2:]
foreground = 'foreground' in options
sticky = 'sticky' in options
service_names.append(name)
service_target_path =\
'src/main/java/{}/Service{}.java'.format(
args.package.replace(".", "/"),
name.capitalize()
)
render(
'Service.tmpl.java',
service_target_path,
name=name,
entrypoint=entrypoint,
args=args,
foreground=foreground,
sticky=sticky,
service_id=sid + 1,
)
# Find the SDK directory and target API
with open('project.properties', 'r') as fileh:
target = fileh.read().strip()
android_api = target.split('-')[1]
try:
int(android_api)
except (ValueError, TypeError):
raise ValueError(
"failed to extract the Android API level from " +
"build.properties. expected int, got: '" +
str(android_api) + "'"
)
with open('local.properties', 'r') as fileh:
sdk_dir = fileh.read().strip()
sdk_dir = sdk_dir[8:]
# Try to build with the newest available build tools
ignored = {".DS_Store", ".ds_store"}
build_tools_versions = [x for x in listdir(join(sdk_dir, 'build-tools')) if x not in ignored]
build_tools_versions = sorted(build_tools_versions,
key=LooseVersion)
build_tools_version = build_tools_versions[-1]
# Folder name for launcher (used by SDL2 bootstrap)
url_scheme = 'kivy'
# Render out android manifest:
manifest_path = "src/main/AndroidManifest.xml"
render_args = {
"args": args,
"service": service,
"service_names": service_names,
"android_api": android_api
}
if get_bootstrap_name() == "sdl2":
render_args["url_scheme"] = url_scheme
render(
'AndroidManifest.tmpl.xml',
manifest_path,
**render_args)
# Copy the AndroidManifest.xml to the dist root dir so that ant
# can also use it
if exists('AndroidManifest.xml'):
remove('AndroidManifest.xml')
shutil.copy(manifest_path, 'AndroidManifest.xml')
# gradle build templates
render(
'build.tmpl.gradle',
'build.gradle',
args=args,
aars=aars,
jars=jars,
android_api=android_api,
build_tools_version=build_tools_version
)
# ant build templates
render(
'build.tmpl.xml',
'build.xml',
args=args,
versioned_name=versioned_name)
# String resources:
render_args = {
"args": args,
"private_version": str(time.time())
}
if get_bootstrap_name() == "sdl2":
render_args["url_scheme"] = url_scheme
render(
'strings.tmpl.xml',
join(res_dir, 'values/strings.xml'),
**render_args)
if exists(join("templates", "custom_rules.tmpl.xml")):
render(
'custom_rules.tmpl.xml',
'custom_rules.xml',
args=args)
if get_bootstrap_name() == "webview":
render('WebViewLoader.tmpl.java',
'src/main/java/org/kivy/android/WebViewLoader.java',
args=args)
if args.sign:
render('build.properties', 'build.properties')
else:
if exists('build.properties'):
os.remove('build.properties')
# Apply java source patches if any are present:
if exists(join('src', 'patches')):
print("Applying Java source code patches...")
for patch_name in os.listdir(join('src', 'patches')):
patch_path = join('src', 'patches', patch_name)
print("Applying patch: " + str(patch_path))
try:
subprocess.check_output([
# -N: insist this is FORWARd patch, don't reverse apply
# -p1: strip first path component
# -t: batch mode, don't ask questions
"patch", "-N", "-p1", "-t", "-i", patch_path
])
except subprocess.CalledProcessError as e:
if e.returncode == 1:
# Return code 1 means it didn't apply, this will
# usually mean it is already applied.
print("Warning: failed to apply patch (" +
"exit code 1), " +
"assuming it is already applied: " +
str(patch_path)
)
else:
raise e
def parse_args(args=None):
global BLACKLIST_PATTERNS, WHITELIST_PATTERNS, PYTHON
# Get the default minsdk, equal to the NDK API that this dist is built against
try:
with open('dist_info.json', 'r') as fileh:
info = json.load(fileh)
default_min_api = int(info['ndk_api'])
ndk_api = default_min_api
except (OSError, KeyError, ValueError, TypeError):
print('WARNING: Failed to read ndk_api from dist info, defaulting to 12')
default_min_api = 12 # The old default before ndk_api was introduced
ndk_api = 12
import argparse
ap = argparse.ArgumentParser(description='''\
Package a Python application for Android (using
bootstrap ''' + get_bootstrap_name() + ''').
For this to work, Java and Ant need to be in your path, as does the
tools directory of the Android SDK.
''')
# --private is required unless for sdl2, where there's also --launcher
ap.add_argument('--private', dest='private',
help='the directory with the app source code files' +
' (containing your main.py entrypoint)',
required=(get_bootstrap_name() != "sdl2"))
ap.add_argument('--package', dest='package',
help=('The name of the java package the project will be'
' packaged under.'),
required=True)
ap.add_argument('--name', dest='name',
help=('The human-readable name of the project.'),
required=True)
ap.add_argument('--numeric-version', dest='numeric_version',
help=('The numeric version number of the project. If not '
'given, this is automatically computed from the '
'version.'))
ap.add_argument('--version', dest='version',
help=('The version number of the project. This should '
'consist of numbers and dots, and should have the '
'same number of groups of numbers as previous '
'versions.'),
required=True)
if get_bootstrap_name() == "sdl2":
ap.add_argument('--launcher', dest='launcher', action='store_true',
help=('Provide this argument to build a multi-app '
'launcher, rather than a single app.'))
ap.add_argument('--permission', dest='permissions', action='append', default=[],
help='The permissions to give this app.', nargs='+')
ap.add_argument('--meta-data', dest='meta_data', action='append', default=[],
help='Custom key=value to add in application metadata')
ap.add_argument('--uses-library', dest='android_used_libs', action='append', default=[],
help='Used shared libraries included using <uses-library> tag in AndroidManifest.xml')
ap.add_argument('--icon', dest='icon',
help=('A png file to use as the icon for '
'the application.'))
ap.add_argument('--service', dest='services', action='append', default=[],
help='Declare a new service entrypoint: '
'NAME:PATH_TO_PY[:foreground]')
if get_bootstrap_name() != "service_only":
ap.add_argument('--presplash', dest='presplash',
help=('A jpeg file to use as a screen while the '
'application is loading.'))
ap.add_argument('--presplash-color',
dest='presplash_color',
default='#000000',
help=('A string to set the loading screen '
'background color. '
'Supported formats are: '
'#RRGGBB #AARRGGBB or color names '
'like red, green, blue, etc.'))
ap.add_argument('--window', dest='window', action='store_true',
default=False,
help='Indicate if the application will be windowed')
ap.add_argument('--orientation', dest='orientation',
default='portrait',
help=('The orientation that the game will '
'display in. '
'Usually one of "landscape", "portrait", '
'"sensor", or "user" (the same as "sensor" '
'but obeying the '
'user\'s Android rotation setting). '
'The full list of options is given under '
'android_screenOrientation at '
'https://developer.android.com/guide/'
'topics/manifest/'
'activity-element.html'))
ap.add_argument('--wakelock', dest='wakelock', action='store_true',
help=('Indicate if the application needs the device '
'to stay on'))
ap.add_argument('--blacklist', dest='blacklist',
default=join(curdir, 'blacklist.txt'),
help=('Use a blacklist file to match unwanted file in '
'the final APK'))
ap.add_argument('--whitelist', dest='whitelist',
default=join(curdir, 'whitelist.txt'),
help=('Use a whitelist file to prevent blacklisting of '
'file in the final APK'))
ap.add_argument('--add-jar', dest='add_jar', action='append',
help=('Add a Java .jar to the libs, so you can access its '
'classes with pyjnius. You can specify this '
'argument more than once to include multiple jars'))
ap.add_argument('--add-aar', dest='add_aar', action='append',
help=('Add an aar dependency manually'))
ap.add_argument('--depend', dest='depends', action='append',
help=('Add a external dependency '
'(eg: com.android.support:appcompat-v7:19.0.1)'))
# The --sdk option has been removed, it is ignored in favour of
# --android-api handled by toolchain.py
ap.add_argument('--sdk', dest='sdk_version', default=-1,
type=int, help=('Deprecated argument, does nothing'))
ap.add_argument('--minsdk', dest='min_sdk_version',
default=default_min_api, type=int,
help=('Minimum Android SDK version that the app supports. '
'Defaults to {}.'.format(default_min_api)))
ap.add_argument('--allow-minsdk-ndkapi-mismatch', default=False,
action='store_true',
help=('Allow the --minsdk argument to be different from '
'the discovered ndk_api in the dist'))
ap.add_argument('--intent-filters', dest='intent_filters',
help=('Add intent-filters xml rules to the '
'AndroidManifest.xml file. The argument is a '
'filename containing xml. The filename should be '
'located relative to the python-for-android '
'directory'))
ap.add_argument('--with-billing', dest='billing_pubkey',
help='If set, the billing service will be added (not implemented)')
ap.add_argument('--add-source', dest='extra_source_dirs', action='append',
help='Include additional source dirs in Java build')
if get_bootstrap_name() == "webview":
ap.add_argument('--port',
help='The port on localhost that the WebView will access',
default='5000')
ap.add_argument('--try-system-python-compile', dest='try_system_python_compile',
action='store_true',
help='Use the system python during compileall if possible.')
ap.add_argument('--no-compile-pyo', dest='no_compile_pyo', action='store_true',
help='Do not optimise .py files to .pyo.')
ap.add_argument('--sign', action='store_true',
help=('Try to sign the APK with your credentials. You must set '
'the appropriate environment variables.'))
ap.add_argument('--add-activity', dest='add_activity', action='append',
help='Add this Java class as an Activity to the manifest.')
ap.add_argument('--activity-launch-mode',
dest='activity_launch_mode',
default='singleTask',
help='Set the launch mode of the main activity in the manifest.')
ap.add_argument('--allow-backup', dest='allow_backup', default='true',
help="if set to 'false', then android won't backup the application.")
ap.add_argument('--no-optimize-python', dest='optimize_python',
action='store_false', default=True,
help=('Whether to compile to optimised .pyo files, using -OO '
'(strips docstrings and asserts)'))
# Put together arguments, and add those from .p4a config file:
if args is None:
args = sys.argv[1:]
def _read_configuration():
if not exists(".p4a"):
return
print("Reading .p4a configuration")
with open(".p4a") as fd:
lines = fd.readlines()
lines = [shlex.split(line)
for line in lines if not line.startswith("#")]
for line in lines:
for arg in line:
args.append(arg)
_read_configuration()
args = ap.parse_args(args)
args.ignore_path = []
if args.name and args.name[0] == '"' and args.name[-1] == '"':
args.name = args.name[1:-1]
if ndk_api != args.min_sdk_version:
print(('WARNING: --minsdk argument does not match the api that is '
'compiled against. Only proceed if you know what you are '
'doing, otherwise use --minsdk={} or recompile against api '
'{}').format(ndk_api, args.min_sdk_version))
if not args.allow_minsdk_ndkapi_mismatch:
print('You must pass --allow-minsdk-ndkapi-mismatch to build '
'with --minsdk different to the target NDK api from the '
'build step')
sys.exit(1)
else:
print('Proceeding with --minsdk not matching build target api')
if args.billing_pubkey:
print('Billing not yet supported!')
sys.exit(1)
if args.sdk_version == -1:
print('WARNING: Received a --sdk argument, but this argument is '
'deprecated and does nothing.')
args.sdk_version = -1 # ensure it is not used
if args.permissions and isinstance(args.permissions[0], list):
args.permissions = [p for perm in args.permissions for p in perm]
if args.try_system_python_compile:
# Hardcoding python2.7 is okay for now, as python3 skips the
# compilation anyway
if not exists('crystax_python'):
python_executable = 'python2.7'
try:
subprocess.call([python_executable, '--version'])
except (OSError, subprocess.CalledProcessError):
pass
else:
PYTHON = python_executable
if args.no_compile_pyo:
PYTHON = None
BLACKLIST_PATTERNS.remove('*.py')
if args.blacklist:
with open(args.blacklist) as fd:
patterns = [x.strip() for x in fd.read().splitlines()
if x.strip() and not x.strip().startswith('#')]
BLACKLIST_PATTERNS += patterns
if args.whitelist:
with open(args.whitelist) as fd:
patterns = [x.strip() for x in fd.read().splitlines()
if x.strip() and not x.strip().startswith('#')]
WHITELIST_PATTERNS += patterns
if args.private is None and \
get_bootstrap_name() == 'sdl2' and args.launcher is None:
print('Need --private directory or ' +
'--launcher (SDL2 bootstrap only)' +
'to have something to launch inside the .apk!')
sys.exit(1)
make_package(args)
return args
if __name__ == "__main__":
parse_args()

View file

@ -0,0 +1,6 @@
#Mon Mar 09 17:19:02 CET 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip

View file

@ -0,0 +1,164 @@
#!/usr/bin/env bash
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn ( ) {
echo "$*"
}
die ( ) {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac
# For Cygwin, ensure paths are in UNIX format before anything is touched.
if $cygwin ; then
[ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
fi
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >&-
APP_HOME="`pwd -P`"
cd "$SAVED" >&-
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

View file

@ -0,0 +1,90 @@
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS=
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:init
@rem Get command-line arguments, handling Windowz variants
if not "%OS%" == "Windows_NT" goto win9xME_args
if "%@eval[2+2]" == "4" goto 4NT_args
:win9xME_args
@rem Slurp the command line arguments.
set CMD_LINE_ARGS=
set _SKIP=2
:win9xME_args_slurp
if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%*
goto execute
:4NT_args
@rem Get arguments from the 4NT Shell from JP Software
set CMD_LINE_ARGS=%$
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View file

@ -0,0 +1 @@
include $(call all-subdir-makefiles)

View file

@ -0,0 +1 @@
include $(call all-subdir-makefiles)

View file

@ -0,0 +1,27 @@
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := main
SDL_PATH := ../../SDL
LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include
# Add your application source files here...
LOCAL_SRC_FILES := $(SDL_PATH)/src/main/android/SDL_android_main.c \
start.c
LOCAL_CFLAGS += -I$(PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS)
LOCAL_SHARED_LIBRARIES := SDL2 python_shared
LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -llog $(EXTRA_LDLIBS)
LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS)
include $(BUILD_SHARED_LIBRARY)
ifdef CRYSTAX_PYTHON_VERSION
$(call import-module,python/$(CRYSTAX_PYTHON_VERSION))
endif

View file

@ -1,5 +1,4 @@
#define PY_SSIZE_T_CLEAN #define PY_SSIZE_T_CLEAN
#include "Python.h" #include "Python.h"
#ifndef Py_PYTHON_H #ifndef Py_PYTHON_H
@ -15,6 +14,16 @@
#include <sys/types.h> #include <sys/types.h>
#include <errno.h> #include <errno.h>
#include "bootstrap_name.h"
#ifndef BOOTSTRAP_USES_NO_SDL_HEADERS
#include "SDL.h"
#ifndef BOOTSTRAP_NAME_PYGAME
#include "SDL_opengles2.h"
#endif
#endif
#ifdef BOOTSTRAP_NAME_PYGAME
#include "jniwrapperstuff.h"
#endif
#include "android/log.h" #include "android/log.h"
#define ENTRYPOINT_MAXLEN 128 #define ENTRYPOINT_MAXLEN 128
@ -58,7 +67,7 @@ int dir_exists(char *filename) {
int file_exists(const char *filename) { int file_exists(const char *filename) {
FILE *file; FILE *file;
if (file = fopen(filename, "r")) { if ((file = fopen(filename, "r"))) {
fclose(file); fclose(file);
return 1; return 1;
} }
@ -75,25 +84,79 @@ int main(int argc, char *argv[]) {
int ret = 0; int ret = 0;
FILE *fd; FILE *fd;
/* AND: Several filepaths are hardcoded here, these must be made LOGP("Initializing Python for Android");
configurable */
/* AND: P4A uses env vars...not sure what's best */ // Set a couple of built-in environment vars:
LOGP("Initialize Python for Android"); setenv("P4A_BOOTSTRAP", bootstrap_name, 1); // env var to identify p4a to applications
env_argument = getenv("ANDROID_ARGUMENT"); env_argument = getenv("ANDROID_ARGUMENT");
setenv("ANDROID_APP_PATH", env_argument, 1); setenv("ANDROID_APP_PATH", env_argument, 1);
env_entrypoint = getenv("ANDROID_ENTRYPOINT"); env_entrypoint = getenv("ANDROID_ENTRYPOINT");
env_logname = getenv("PYTHON_NAME"); env_logname = getenv("PYTHON_NAME");
if (!getenv("ANDROID_UNPACK")) {
/* ANDROID_UNPACK currently isn't set in services */
setenv("ANDROID_UNPACK", env_argument, 1);
}
if (env_logname == NULL) { if (env_logname == NULL) {
env_logname = "python"; env_logname = "python";
setenv("PYTHON_NAME", "python", 1); setenv("PYTHON_NAME", "python", 1);
} }
// Set additional file-provided environment vars:
LOGP("Setting additional env vars from p4a_env_vars.txt");
char env_file_path[256];
snprintf(env_file_path, sizeof(env_file_path),
"%s/p4a_env_vars.txt", getenv("ANDROID_UNPACK"));
FILE *env_file_fd = fopen(env_file_path, "r");
if (env_file_fd) {
char* line = NULL;
size_t len = 0;
while (getline(&line, &len, env_file_fd) != -1) {
if (strlen(line) > 0) {
char *eqsubstr = strstr(line, "=");
if (eqsubstr) {
size_t eq_pos = eqsubstr - line;
// Extract name:
char env_name[256];
strncpy(env_name, line, sizeof(env_name));
env_name[eq_pos] = '\0';
// Extract value (with line break removed:
char env_value[256];
strncpy(env_value, (char*)(line + eq_pos + 1), sizeof(env_value));
if (strlen(env_value) > 0 &&
env_value[strlen(env_value)-1] == '\n') {
env_value[strlen(env_value)-1] = '\0';
if (strlen(env_value) > 0 &&
env_value[strlen(env_value)-1] == '\r') {
// Also remove windows line breaks (\r\n)
env_value[strlen(env_value)-1] = '\0';
}
}
// Set value:
setenv(env_name, env_value, 1);
}
}
}
fclose(env_file_fd);
} else {
LOGP("Warning: no p4a_env_vars.txt found / failed to open!");
}
LOGP("Changing directory to the one provided by ANDROID_ARGUMENT"); LOGP("Changing directory to the one provided by ANDROID_ARGUMENT");
LOGP(env_argument); LOGP(env_argument);
chdir(env_argument); chdir(env_argument);
#if PY_MAJOR_VERSION < 3
Py_NoSiteFlag=1;
#endif
#if PY_MAJOR_VERSION < 3
Py_SetProgramName("android_python");
#else
Py_SetProgramName(L"android_python"); Py_SetProgramName(L"android_python");
#endif
#if PY_MAJOR_VERSION >= 3 #if PY_MAJOR_VERSION >= 3
/* our logging module for android /* our logging module for android
@ -103,34 +166,55 @@ int main(int argc, char *argv[]) {
LOGP("Preparing to initialize python"); LOGP("Preparing to initialize python");
if (dir_exists("crystax_python/")) { // Set up the python path
LOGP("crystax_python exists");
char paths[256]; char paths[256];
char crystax_python_dir[256];
snprintf(crystax_python_dir, 256,
"%s/crystax_python", getenv("ANDROID_UNPACK"));
char python_bundle_dir[256];
snprintf(python_bundle_dir, 256,
"%s/_python_bundle", getenv("ANDROID_UNPACK"));
if (dir_exists(crystax_python_dir) || dir_exists(python_bundle_dir)) {
if (dir_exists(crystax_python_dir)) {
LOGP("crystax_python exists");
snprintf(paths, 256, snprintf(paths, 256,
"%s/crystax_python/stdlib.zip:%s/crystax_python/modules", "%s/stdlib.zip:%s/modules",
env_argument, env_argument); crystax_python_dir, crystax_python_dir);
/* snprintf(paths, 256, "%s/stdlib.zip:%s/modules", env_argument, }
* env_argument); */
if (dir_exists(python_bundle_dir)) {
LOGP("_python_bundle dir exists");
snprintf(paths, 256,
"%s/stdlib.zip:%s/modules",
python_bundle_dir, python_bundle_dir);
}
LOGP("calculated paths to be..."); LOGP("calculated paths to be...");
LOGP(paths); LOGP(paths);
#if PY_MAJOR_VERSION >= 3 #if PY_MAJOR_VERSION >= 3
wchar_t *wchar_paths = Py_DecodeLocale(paths, NULL); wchar_t *wchar_paths = Py_DecodeLocale(paths, NULL);
Py_SetPath(wchar_paths); Py_SetPath(wchar_paths);
#else
char *wchar_paths = paths;
LOGP("Can't Py_SetPath in python2, so crystax python2 doesn't work yet");
exit(1);
#endif #endif
LOGP("set wchar paths..."); LOGP("set wchar paths...");
} else { } else {
LOGP("crystax_python does not exist"); // We do not expect to see crystax_python any more, so no point
// reminding the user about it. If it does exist, we'll have
// logged it earlier.
LOGP("_python_bundle does not exist");
} }
Py_Initialize(); Py_Initialize();
#if PY_MAJOR_VERSION < 3 #if PY_MAJOR_VERSION < 3
// Can't Py_SetPath in python2 but we can set PySys_SetPath, which must
// be applied after Py_Initialize rather than before like Py_SetPath
#if PY_MICRO_VERSION >= 15
// Only for python native-build
PySys_SetPath(paths);
#endif
PySys_SetArgv(argc, argv); PySys_SetArgv(argc, argv);
#endif #endif
@ -153,8 +237,10 @@ int main(int argc, char *argv[]) {
*/ */
PyRun_SimpleString("import sys, posix\n"); PyRun_SimpleString("import sys, posix\n");
if (dir_exists("lib")) { if (dir_exists("lib")) {
/* If we built our own python, set up the paths correctly */ /* If we built our own python, set up the paths correctly.
LOGP("Setting up python from ANDROID_PRIVATE"); * This is only the case if we are using the python2legacy recipe
*/
LOGP("Setting up python from ANDROID_APP_PATH");
PyRun_SimpleString("private = posix.environ['ANDROID_APP_PATH']\n" PyRun_SimpleString("private = posix.environ['ANDROID_APP_PATH']\n"
"argument = posix.environ['ANDROID_ARGUMENT']\n" "argument = posix.environ['ANDROID_ARGUMENT']\n"
"sys.path[:] = [ \n" "sys.path[:] = [ \n"
@ -165,11 +251,24 @@ int main(int argc, char *argv[]) {
" argument ]\n"); " argument ]\n");
} }
if (dir_exists("crystax_python")) {
char add_site_packages_dir[256]; char add_site_packages_dir[256];
if (dir_exists(crystax_python_dir)) {
snprintf(add_site_packages_dir, 256, snprintf(add_site_packages_dir, 256,
"sys.path.append('%s/crystax_python/site-packages')", "sys.path.append('%s/site-packages')",
env_argument); crystax_python_dir);
PyRun_SimpleString("import sys\n"
"sys.argv = ['notaninterpreterreally']\n"
"from os.path import realpath, join, dirname");
PyRun_SimpleString(add_site_packages_dir);
/* "sys.path.append(join(dirname(realpath(__file__)), 'site-packages'))") */
PyRun_SimpleString("sys.path = ['.'] + sys.path");
}
if (dir_exists(python_bundle_dir)) {
snprintf(add_site_packages_dir, 256,
"sys.path.append('%s/site-packages')",
python_bundle_dir);
PyRun_SimpleString("import sys\n" PyRun_SimpleString("import sys\n"
"sys.argv = ['notaninterpreterreally']\n" "sys.argv = ['notaninterpreterreally']\n"
@ -210,6 +309,11 @@ int main(int argc, char *argv[]) {
/* Get the entrypoint, search the .pyo then .py /* Get the entrypoint, search the .pyo then .py
*/ */
char *dot = strrchr(env_entrypoint, '.'); char *dot = strrchr(env_entrypoint, '.');
#if PY_MAJOR_VERSION > 2
char *ext = ".pyc";
#else
char *ext = ".pyo";
#endif
if (dot <= 0) { if (dot <= 0) {
LOGP("Invalid entrypoint, abort."); LOGP("Invalid entrypoint, abort.");
return -1; return -1;
@ -218,14 +322,14 @@ int main(int argc, char *argv[]) {
LOGP("Entrypoint path is too long, try increasing ENTRYPOINT_MAXLEN."); LOGP("Entrypoint path is too long, try increasing ENTRYPOINT_MAXLEN.");
return -1; return -1;
} }
if (!strcmp(dot, ".pyo")) { if (!strcmp(dot, ext)) {
if (!file_exists(env_entrypoint)) { if (!file_exists(env_entrypoint)) {
/* fallback on .py */ /* fallback on .py */
strcpy(entrypoint, env_entrypoint); strcpy(entrypoint, env_entrypoint);
entrypoint[strlen(env_entrypoint) - 1] = '\0'; entrypoint[strlen(env_entrypoint) - 1] = '\0';
LOGP(entrypoint); LOGP(entrypoint);
if (!file_exists(entrypoint)) { if (!file_exists(entrypoint)) {
LOGP("Entrypoint not found (.pyo, fallback on .py), abort"); LOGP("Entrypoint not found (.pyc/.pyo, fallback on .py), abort");
return -1; return -1;
} }
} else { } else {
@ -235,7 +339,11 @@ int main(int argc, char *argv[]) {
/* if .py is passed, check the pyo version first */ /* if .py is passed, check the pyo version first */
strcpy(entrypoint, env_entrypoint); strcpy(entrypoint, env_entrypoint);
entrypoint[strlen(env_entrypoint) + 1] = '\0'; entrypoint[strlen(env_entrypoint) + 1] = '\0';
#if PY_MAJOR_VERSION > 2
entrypoint[strlen(env_entrypoint)] = 'c';
#else
entrypoint[strlen(env_entrypoint)] = 'o'; entrypoint[strlen(env_entrypoint)] = 'o';
#endif
if (!file_exists(entrypoint)) { if (!file_exists(entrypoint)) {
/* fallback on pure python version */ /* fallback on pure python version */
if (!file_exists(env_entrypoint)) { if (!file_exists(env_entrypoint)) {
@ -245,7 +353,7 @@ int main(int argc, char *argv[]) {
strcpy(entrypoint, env_entrypoint); strcpy(entrypoint, env_entrypoint);
} }
} else { } else {
LOGP("Entrypoint have an invalid extension (must be .py or .pyo), abort."); LOGP("Entrypoint have an invalid extension (must be .py or .pyc/.pyo), abort.");
return -1; return -1;
} }
// LOGP("Entrypoint is:"); // LOGP("Entrypoint is:");
@ -260,6 +368,7 @@ int main(int argc, char *argv[]) {
/* run python ! /* run python !
*/ */
ret = PyRun_SimpleFile(fd, entrypoint); ret = PyRun_SimpleFile(fd, entrypoint);
fclose(fd);
if (PyErr_Occurred() != NULL) { if (PyErr_Occurred() != NULL) {
ret = 1; ret = 1;
@ -270,19 +379,48 @@ int main(int argc, char *argv[]) {
PyErr_Clear(); PyErr_Clear();
} }
/* close everything
*/
Py_Finalize();
fclose(fd);
LOGP("Python for android ended."); LOGP("Python for android ended.");
/* Shut down: since regular shutdown causes issues sometimes
(seems to be an incomplete shutdown breaking next launch)
we'll use sys.exit(ret) to shutdown, since that one works.
Reference discussion:
https://github.com/kivy/kivy/pull/6107#issue-246120816
*/
char terminatecmd[256];
snprintf(
terminatecmd, sizeof(terminatecmd),
"import sys; sys.exit(%d)\n", ret
);
PyRun_SimpleString(terminatecmd);
/* This should never actually be reached, but we'll leave the clean-up
* here just to be safe.
*/
#if PY_MAJOR_VERSION < 3
Py_Finalize();
LOGP("Unexpectedly reached Py_FinalizeEx(), but was successful.");
#else
if (Py_FinalizeEx() != 0) // properly check success on Python 3
LOGP("Unexpectedly reached Py_FinalizeEx(), and got error!");
else
LOGP("Unexpectedly reached Py_FinalizeEx(), but was successful.");
#endif
return ret; return ret;
} }
JNIEXPORT void JNICALL Java_org_kivy_android_PythonService_nativeStart( JNIEXPORT void JNICALL Java_org_kivy_android_PythonService_nativeStart(
JNIEnv *env, jobject thiz, jstring j_android_private, JNIEnv *env,
jstring j_android_argument, jstring j_service_entrypoint, jobject thiz,
jstring j_python_name, jstring j_python_home, jstring j_python_path, jstring j_android_private,
jstring j_android_argument,
jstring j_service_entrypoint,
jstring j_python_name,
jstring j_python_home,
jstring j_python_path,
jstring j_arg) { jstring j_arg) {
jboolean iscopy; jboolean iscopy;
const char *android_private = const char *android_private =
@ -308,10 +446,7 @@ JNIEXPORT void JNICALL Java_org_kivy_android_PythonService_nativeStart(
setenv("PYTHONHOME", python_home, 1); setenv("PYTHONHOME", python_home, 1);
setenv("PYTHONPATH", python_path, 1); setenv("PYTHONPATH", python_path, 1);
setenv("PYTHON_SERVICE_ARGUMENT", arg, 1); setenv("PYTHON_SERVICE_ARGUMENT", arg, 1);
setenv("P4A_BOOTSTRAP", bootstrap_name, 1);
char ca_path[128];
snprintf(ca_path, 128, "%s/crystax_python/site-packages/certifi/cacert.pem", python_home);
setenv("SSL_CERT_FILE", ca_path, 1);
char *argv[] = {"."}; char *argv[] = {"."};
/* ANDROID_ARGUMENT points to service subdir, /* ANDROID_ARGUMENT points to service subdir,
@ -320,4 +455,47 @@ JNIEXPORT void JNICALL Java_org_kivy_android_PythonService_nativeStart(
main(1, argv); main(1, argv);
} }
#if defined(BOOTSTRAP_NAME_WEBVIEW) || defined(BOOTSTRAP_NAME_SERVICEONLY)
// Webview and service_only uses some more functions:
void Java_org_kivy_android_PythonActivity_nativeSetenv(
JNIEnv* env, jclass cls,
jstring name, jstring value)
//JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSetenv)(
// JNIEnv* env, jclass cls,
// jstring name, jstring value)
{
const char *utfname = (*env)->GetStringUTFChars(env, name, NULL);
const char *utfvalue = (*env)->GetStringUTFChars(env, value, NULL);
setenv(utfname, utfvalue, 1);
(*env)->ReleaseStringUTFChars(env, name, utfname);
(*env)->ReleaseStringUTFChars(env, value, utfvalue);
}
void Java_org_kivy_android_PythonActivity_nativeInit(JNIEnv* env, jclass cls, jobject obj)
{
/* This nativeInit follows SDL2 */
/* This interface could expand with ABI negotiation, calbacks, etc. */
/* SDL_Android_Init(env, cls); */
/* SDL_SetMainReady(); */
/* Run the application code! */
int status;
char *argv[2];
argv[0] = "Python_app";
argv[1] = NULL;
/* status = SDL_main(1, argv); */
main(1, argv);
/* Do not issue an exit or the whole application will terminate instead of just the SDL thread */
/* exit(status); */
}
#endif
#endif #endif

View file

@ -0,0 +1,164 @@
package org.kivy.android;
import android.os.Build;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import android.app.Service;
import android.os.IBinder;
import android.os.Bundle;
import android.content.Intent;
import android.content.Context;
import android.util.Log;
import android.app.Notification;
import android.app.PendingIntent;
import android.os.Process;
import java.io.File;
import org.kivy.android.PythonUtil;
import org.renpy.android.Hardware;
public class PythonService extends Service implements Runnable {
// Thread for Python code
private Thread pythonThread = null;
// Python environment variables
private String androidPrivate;
private String androidArgument;
private String pythonName;
private String pythonHome;
private String pythonPath;
private String serviceEntrypoint;
// Argument to pass to Python code,
private String pythonServiceArgument;
public static PythonService mService = null;
private Intent startIntent = null;
private boolean autoRestartService = false;
public void setAutoRestartService(boolean restart) {
autoRestartService = restart;
}
public boolean canDisplayNotification() {
return true;
}
public int startType() {
return START_NOT_STICKY;
}
@Override
public IBinder onBind(Intent arg0) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (pythonThread != null) {
Log.v("python service", "service exists, do not start again");
return START_NOT_STICKY;
}
startIntent = intent;
Bundle extras = intent.getExtras();
androidPrivate = extras.getString("androidPrivate");
androidArgument = extras.getString("androidArgument");
serviceEntrypoint = extras.getString("serviceEntrypoint");
pythonName = extras.getString("pythonName");
pythonHome = extras.getString("pythonHome");
pythonPath = extras.getString("pythonPath");
pythonServiceArgument = extras.getString("pythonServiceArgument");
pythonThread = new Thread(this);
pythonThread.start();
if (canDisplayNotification()) {
doStartForeground(extras);
}
return startType();
}
protected void doStartForeground(Bundle extras) {
String serviceTitle = extras.getString("serviceTitle");
String serviceDescription = extras.getString("serviceDescription");
Notification notification;
Context context = getApplicationContext();
Intent contextIntent = new Intent(context, PythonActivity.class);
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
notification = new Notification(
context.getApplicationInfo().icon, serviceTitle, System.currentTimeMillis());
try {
// prevent using NotificationCompat, this saves 100kb on apk
Method func = notification.getClass().getMethod(
"setLatestEventInfo", Context.class, CharSequence.class,
CharSequence.class, PendingIntent.class);
func.invoke(notification, context, serviceTitle, serviceDescription, pIntent);
} catch (NoSuchMethodException | IllegalAccessException |
IllegalArgumentException | InvocationTargetException e) {
}
} else {
Notification.Builder builder = new Notification.Builder(context);
builder.setContentTitle(serviceTitle);
builder.setContentText(serviceDescription);
builder.setContentIntent(pIntent);
builder.setSmallIcon(context.getApplicationInfo().icon);
notification = builder.build();
}
startForeground(1, notification);
}
@Override
public void onDestroy() {
super.onDestroy();
pythonThread = null;
if (autoRestartService && startIntent != null) {
Log.v("python service", "service restart requested");
startService(startIntent);
}
Process.killProcess(Process.myPid());
}
/**
* Stops the task gracefully when killed.
* Calling stopSelf() will trigger a onDestroy() call from the system.
*/
@Override
public void onTaskRemoved(Intent rootIntent) {
super.onTaskRemoved(rootIntent);
stopSelf();
}
@Override
public void run(){
String app_root = getFilesDir().getAbsolutePath() + "/app";
File app_root_file = new File(app_root);
PythonUtil.loadLibraries(app_root_file,
new File(getApplicationInfo().nativeLibraryDir));
this.mService = this;
nativeStart(
androidPrivate, androidArgument,
serviceEntrypoint, pythonName,
pythonHome, pythonPath,
pythonServiceArgument);
stopSelf();
}
// Native part
public static native void nativeStart(
String androidPrivate, String androidArgument,
String serviceEntrypoint, String pythonName,
String pythonHome, String pythonPath,
String pythonServiceArgument);
}

View file

@ -0,0 +1,77 @@
package org.kivy.android;
import java.io.File;
import android.util.Log;
import java.util.ArrayList;
import java.io.FilenameFilter;
import java.util.regex.Pattern;
public class PythonUtil {
private static final String TAG = "pythonutil";
protected static void addLibraryIfExists(ArrayList<String> libsList, String pattern, File libsDir) {
// pattern should be the name of the lib file, without the
// preceding "lib" or suffix ".so", for instance "ssl.*" will
// match files of the form "libssl.*.so".
File [] files = libsDir.listFiles();
pattern = "lib" + pattern + "\\.so";
Pattern p = Pattern.compile(pattern);
for (int i = 0; i < files.length; ++i) {
File file = files[i];
String name = file.getName();
Log.v(TAG, "Checking pattern " + pattern + " against " + name);
if (p.matcher(name).matches()) {
Log.v(TAG, "Pattern " + pattern + " matched file " + name);
libsList.add(name.substring(3, name.length() - 3));
}
}
}
protected static ArrayList<String> getLibraries(File libsDir) {
ArrayList<String> libsList = new ArrayList<String>();
addLibraryIfExists(libsList, "crystax", libsDir);
addLibraryIfExists(libsList, "sqlite3", libsDir);
addLibraryIfExists(libsList, "ffi", libsDir);
addLibraryIfExists(libsList, "ssl.*", libsDir);
addLibraryIfExists(libsList, "crypto.*", libsDir);
libsList.add("python2.7");
libsList.add("python3.5m");
libsList.add("python3.6m");
libsList.add("python3.7m");
libsList.add("main");
return libsList;
}
public static void loadLibraries(File filesDir, File libsDir) {
String filesDirPath = filesDir.getAbsolutePath();
boolean foundPython = false;
for (String lib : getLibraries(libsDir)) {
Log.v(TAG, "Loading library: " + lib);
try {
System.loadLibrary(lib);
if (lib.startsWith("python")) {
foundPython = true;
}
} catch(UnsatisfiedLinkError e) {
// If this is the last possible libpython
// load, and it has failed, give a more
// general error
Log.v(TAG, "Library loading error: " + e.getMessage());
if (lib.startsWith("python3.7") && !foundPython) {
throw new java.lang.RuntimeException("Could not load any libpythonXXX.so");
} else if (lib.startsWith("python")) {
continue;
} else {
Log.v(TAG, "An UnsatisfiedLinkError occurred loading " + lib);
throw e;
}
}
}
Log.v(TAG, "Loaded everything!");
}
}

View file

@ -0,0 +1,115 @@
// This string is autogenerated by ChangeAppSettings.sh, do not change
// spaces amount
package org.renpy.android;
import java.io.*;
import android.app.Activity;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.File;
import java.util.zip.GZIPInputStream;
import android.content.res.AssetManager;
import org.kamranzafar.jtar.*;
public class AssetExtract {
private AssetManager mAssetManager = null;
private Activity mActivity = null;
public AssetExtract(Activity act) {
mActivity = act;
mAssetManager = act.getAssets();
}
public boolean extractTar(String asset, String target) {
byte buf[] = new byte[1024 * 1024];
InputStream assetStream = null;
TarInputStream tis = null;
try {
assetStream = mAssetManager.open(asset, AssetManager.ACCESS_STREAMING);
tis = new TarInputStream(new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(assetStream, 8192)), 8192));
} catch (IOException e) {
Log.e("python", "opening up extract tar", e);
return false;
}
while (true) {
TarEntry entry = null;
try {
entry = tis.getNextEntry();
} catch ( java.io.IOException e ) {
Log.e("python", "extracting tar", e);
return false;
}
if ( entry == null ) {
break;
}
Log.v("python", "extracting " + entry.getName());
if (entry.isDirectory()) {
try {
new File(target +"/" + entry.getName()).mkdirs();
} catch ( SecurityException e ) { };
continue;
}
OutputStream out = null;
String path = target + "/" + entry.getName();
try {
out = new BufferedOutputStream(new FileOutputStream(path), 8192);
} catch ( FileNotFoundException e ) {
} catch ( SecurityException e ) { };
if ( out == null ) {
Log.e("python", "could not open " + path);
return false;
}
try {
while (true) {
int len = tis.read(buf);
if (len == -1) {
break;
}
out.write(buf, 0, len);
}
out.flush();
out.close();
} catch ( java.io.IOException e ) {
Log.e("python", "extracting zip", e);
return false;
}
}
try {
tis.close();
assetStream.close();
} catch (IOException e) {
// pass
}
return true;
}
}

View file

@ -0,0 +1,54 @@
/**
* This class takes care of managing resources for us. In our code, we
* can't use R, since the name of the package containing R will
* change. (This same code is used in both org.renpy.android and
* org.renpy.pygame.) So this is the next best thing.
*/
package org.renpy.android;
import android.app.Activity;
import android.content.res.Resources;
import android.view.View;
import android.util.Log;
public class ResourceManager {
private Activity act;
private Resources res;
public ResourceManager(Activity activity) {
act = activity;
res = act.getResources();
}
public int getIdentifier(String name, String kind) {
Log.v("SDL", "getting identifier");
Log.v("SDL", "kind is " + kind + " and name " + name);
Log.v("SDL", "result is " + res.getIdentifier(name, kind, act.getPackageName()));
return res.getIdentifier(name, kind, act.getPackageName());
}
public String getString(String name) {
try {
Log.v("SDL", "asked to get string " + name);
return res.getString(getIdentifier(name, "string"));
} catch (Exception e) {
Log.v("SDL", "got exception looking for string!");
return null;
}
}
public View inflateView(String name) {
int id = getIdentifier(name, "layout");
return act.getLayoutInflater().inflate(id, null);
}
public View getViewById(View v, String name) {
int id = getIdentifier(name, "id");
return v.findViewById(id);
}
}

View file

@ -0,0 +1,77 @@
package {{ args.package }};
import android.os.Build;
import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import android.content.Intent;
import android.content.Context;
import android.app.Notification;
import android.app.PendingIntent;
import android.os.Bundle;
import org.kivy.android.PythonService;
import org.kivy.android.PythonActivity;
public class Service{{ name|capitalize }} extends PythonService {
{% if sticky %}
@Override
public int startType() {
return START_STICKY;
}
{% endif %}
{% if not foreground %}
@Override
public boolean canDisplayNotification() {
return false;
}
{% endif %}
@Override
protected void doStartForeground(Bundle extras) {
Notification notification;
Context context = getApplicationContext();
Intent contextIntent = new Intent(context, PythonActivity.class);
PendingIntent pIntent = PendingIntent.getActivity(context, 0, contextIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
notification = new Notification(
context.getApplicationInfo().icon, "{{ args.name }}", System.currentTimeMillis());
try {
// prevent using NotificationCompat, this saves 100kb on apk
Method func = notification.getClass().getMethod(
"setLatestEventInfo", Context.class, CharSequence.class,
CharSequence.class, PendingIntent.class);
func.invoke(notification, context, "{{ args.name }}", "{{ name| capitalize }}", pIntent);
} catch (NoSuchMethodException | IllegalAccessException |
IllegalArgumentException | InvocationTargetException e) {
}
} else {
Notification.Builder builder = new Notification.Builder(context);
builder.setContentTitle("{{ args.name }}");
builder.setContentText("{{ name| capitalize }}");
builder.setContentIntent(pIntent);
builder.setSmallIcon(context.getApplicationInfo().icon);
notification = builder.build();
}
startForeground({{ service_id }}, notification);
}
static public void start(Context ctx, String pythonServiceArgument) {
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
String argument = ctx.getFilesDir().getAbsolutePath() + "/app";
intent.putExtra("androidPrivate", ctx.getFilesDir().getAbsolutePath());
intent.putExtra("androidArgument", argument);
intent.putExtra("serviceEntrypoint", "{{ entrypoint }}");
intent.putExtra("pythonName", "{{ name }}");
intent.putExtra("pythonHome", argument);
intent.putExtra("pythonPath", argument + ":" + argument + "/lib");
intent.putExtra("pythonServiceArgument", pythonServiceArgument);
ctx.startService(intent);
}
static public void stop(Context ctx) {
Intent intent = new Intent(ctx, Service{{ name|capitalize }}.class);
ctx.stopService(intent);
}
}

View file

@ -0,0 +1,21 @@
# This file is used to override default values used by the Ant build system.
#
# This file must be checked in Version Control Systems, as it is
# integral to the build system of your project.
# This file is only used by the Ant script.
# You can use this to override default values such as
# 'source.dir' for the location of your java source folder and
# 'out.dir' for the location of your output folder.
# You can also use it define how the release builds are signed by declaring
# the following properties:
# 'key.store' for the location of your keystore and
# 'key.alias' for the name of the key to use.
# The password will be asked during the build when you use the 'release' target.
key.store=${env.P4A_RELEASE_KEYSTORE}
key.alias=${env.P4A_RELEASE_KEYALIAS}
key.store.password=${env.P4A_RELEASE_KEYSTORE_PASSWD}
key.alias.password=${env.P4A_RELEASE_KEYALIAS_PASSWD}

View file

@ -0,0 +1,80 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
}
}
allprojects {
repositories {
google()
jcenter()
flatDir {
dirs 'libs'
}
}
}
apply plugin: 'com.android.application'
android {
compileSdkVersion {{ android_api }}
buildToolsVersion '{{ build_tools_version }}'
defaultConfig {
minSdkVersion {{ args.min_sdk_version }}
targetSdkVersion {{ android_api }}
versionCode {{ args.numeric_version }}
versionName '{{ args.version }}'
}
{% if args.sign -%}
signingConfigs {
release {
storeFile file(System.getenv("P4A_RELEASE_KEYSTORE"))
keyAlias System.getenv("P4A_RELEASE_KEYALIAS")
storePassword System.getenv("P4A_RELEASE_KEYSTORE_PASSWD")
keyPassword System.getenv("P4A_RELEASE_KEYALIAS_PASSWD")
}
}
{%- endif %}
buildTypes {
debug {
}
release {
{% if args.sign -%}
signingConfig signingConfigs.release
{%- endif %}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_7
targetCompatibility JavaVersion.VERSION_1_7
}
sourceSets {
main {
jniLibs.srcDir 'libs'
}
}
}
dependencies {
{%- for aar in aars %}
compile(name: '{{ aar }}', ext: 'aar')
{%- endfor -%}
{%- for jar in jars %}
compile files('src/main/libs/{{ jar }}')
{%- endfor -%}
{%- if args.depends -%}
{%- for depend in args.depends %}
compile '{{ depend }}'
{%- endfor %}
{%- endif %}
}

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This should be changed to the name of your project -->
<project name="{{ versioned_name }}" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties" />
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties" />
<!-- if sdk.dir was not set from one of the property file, then
get it from the ANDROID_HOME env var.
This must be done before we load project.properties since
the proguard config can use sdk.dir -->
<property environment="env" />
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
<isset property="env.ANDROID_HOME" />
</condition>
<property file="build.properties" />
<!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties" />
<!-- quick check on sdk.dir -->
<fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true" />
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml" />
</project>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="CustomRules">
<target name="-pre-build">
<copy todir="tmp-src">
{% if args.launcher %}
<fileset dir="src/main/java" includes="**" />
{% else %}
<fileset dir="src/main/java">
<exclude name="org/kivy/android/ProjectAdapter.java" />
<exclude name="org/kivy/android/ProjectChooser.java" />
</fileset>
{% endif %}
{% for dir, includes in args.extra_source_dirs %}
<fileset dir="{{ dir }}" includes="{{ includes }}" />
{% endfor %}
</copy>
</target>
<target name="-post-build">
<delete dir="tmp-src" />
</target>
</project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1 @@
# put files here that you need to un-blacklist

View file

@ -7,13 +7,13 @@ LOCAL_MODULE := main
# Add your application source files here... # Add your application source files here...
LOCAL_SRC_FILES := start.c pyjniusjni.c LOCAL_SRC_FILES := start.c pyjniusjni.c
LOCAL_CFLAGS += -I$(LOCAL_PATH)/../../../../other_builds/$(PYTHON2_NAME)/$(ARCH)/python2/python-install/include/python2.7 $(EXTRA_CFLAGS) LOCAL_CFLAGS += -I$(PYTHON_INCLUDE_ROOT) $(EXTRA_CFLAGS)
LOCAL_SHARED_LIBRARIES := python_shared LOCAL_SHARED_LIBRARIES := python_shared
LOCAL_LDLIBS := -llog $(EXTRA_LDLIBS) LOCAL_LDLIBS := -llog $(EXTRA_LDLIBS)
LOCAL_LDFLAGS += -L$(LOCAL_PATH)/../../../../other_builds/$(PYTHON2_NAME)/$(ARCH)/python2/python-install/lib $(APPLICATION_ADDITIONAL_LDFLAGS) LOCAL_LDFLAGS += -L$(PYTHON_LINK_ROOT) $(APPLICATION_ADDITIONAL_LDFLAGS)
include $(BUILD_SHARED_LIBRARY) include $(BUILD_SHARED_LIBRARY)

View file

@ -0,0 +1,6 @@
#define BOOTSTRAP_NAME_SERVICEONLY
#define BOOTSTRAP_USES_NO_SDL_HEADERS
const char bootstrap_name[] = "service_only";

View file

@ -25,6 +25,7 @@
<!-- OpenGL ES 2.0 --> <!-- OpenGL ES 2.0 -->
<uses-feature android:glEsVersion="0x00020000" /> <uses-feature android:glEsVersion="0x00020000" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
{% for perm in args.permissions %} {% for perm in args.permissions %}
{% if '.' in perm %} {% if '.' in perm %}
<uses-permission android:name="{{ perm }}" /> <uses-permission android:name="{{ perm }}" />
@ -54,7 +55,8 @@
android:icon="@drawable/icon" android:icon="@drawable/icon"
android:allowBackup="true" android:allowBackup="true"
android:theme="@style/LbryAppTheme" android:theme="@style/LbryAppTheme"
android:hardwareAccelerated="true"> android:hardwareAccelerated="true"
android:usesCleartextTraffic="true">
{% for m in args.meta_data %} {% for m in args.meta_data %}
<meta-data android:name="{{ m.split('=', 1)[0] }}" android:value="{{ m.split('=', 1)[-1] }}"/>{% endfor %} <meta-data android:name="{{ m.split('=', 1)[0] }}" android:value="{{ m.split('=', 1)[-1] }}"/>{% endfor %}

View file

@ -41,12 +41,6 @@ android {
} }
} }
applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "../" + outputFileName
}
}
dexOptions { dexOptions {
jumboMode true jumboMode true
} }
@ -98,10 +92,12 @@ subprojects {
} }
dependencies { dependencies {
compile project(':@react-native-community_async-storage')
compile project(':react-native-exception-handler') compile project(':react-native-exception-handler')
compile project(':react-native-fast-image') compile project(':react-native-fast-image')
compile project(':rn-fetch-blob') compile project(':react-native-gesture-handler')
compile project(':react-native-video') compile project(':react-native-video')
compile project(':rn-fetch-blob')
{%- for aar in aars %} {%- for aar in aars %}
compile(name: '{{ aar }}', ext: 'aar') compile(name: '{{ aar }}', ext: 'aar')
{%- endfor -%} {%- endfor -%}

View file

@ -1,9 +1,13 @@
rootProject.name = 'browser' rootProject.name = 'browser'
include ':@react-native-community_async-storage'
project(':@react-native-community_async-storage').projectDir = new File(rootProject.projectDir, './react/node_modules/@react-native-community/async-storage/android')
include ':react-native-exception-handler' include ':react-native-exception-handler'
project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-exception-handler/android') project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-exception-handler/android')
include ':react-native-fast-image' include ':react-native-fast-image'
project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-fast-image/android') project(':react-native-fast-image').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-fast-image/android')
include ':rn-fetch-blob' include ':react-native-gesture-handler'
project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, './react/node_modules/rn-fetch-blob/android') project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-gesture-handler/android')
include ':react-native-video' include ':react-native-video'
project(':react-native-video').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-video/android-exoplayer') project(':react-native-video').projectDir = new File(rootProject.projectDir, './react/node_modules/react-native-video/android-exoplayer')
include ':rn-fetch-blob'
project(':rn-fetch-blob').projectDir = new File(rootProject.projectDir, './react/node_modules/rn-fetch-blob/android')

View file

@ -2,21 +2,25 @@ from __future__ import print_function
from os.path import (join, realpath, dirname, expanduser, exists, from os.path import (join, realpath, dirname, expanduser, exists,
split, isdir) split, isdir)
from os import environ, listdir from os import environ
import copy
import os import os
import glob import glob
import sys import sys
import re import re
import sh import sh
import subprocess
from pythonforandroid.util import (ensure_dir, current_directory) from pythonforandroid.util import (
from pythonforandroid.logger import (info, warning, error, info_notify, current_directory, ensure_dir, get_virtualenv_executable,
Err_Fore, Err_Style, info_main, BuildInterruptingException
shprint) )
from pythonforandroid.archs import ArchARM, ArchARMv7_a, Archx86, Archx86_64, ArchAarch_64 from pythonforandroid.logger import (info, warning, info_notify, info_main, shprint)
from pythonforandroid.recipe import Recipe from pythonforandroid.archs import ArchARM, ArchARMv7_a, ArchAarch_64, Archx86, Archx86_64
from pythonforandroid.recipe import CythonRecipe, Recipe
DEFAULT_ANDROID_API = 15 from pythonforandroid.recommendations import (
check_ndk_version, check_target_api, check_ndk_api,
RECOMMENDED_NDK_API, RECOMMENDED_TARGET_API)
class Context(object): class Context(object):
@ -24,14 +28,19 @@ class Context(object):
will be instantiated and used to hold all the build state.''' will be instantiated and used to hold all the build state.'''
env = environ.copy() env = environ.copy()
root_dir = None # the filepath of toolchain.py # the filepath of toolchain.py
storage_dir = None # the root dir where builds and dists will be stored root_dir = None
# the root dir where builds and dists will be stored
storage_dir = None
build_dir = None # in which bootstraps are copied for building # in which bootstraps are copied for building
# and recipes are built # and recipes are built
dist_dir = None # the Android project folder where everything ends up build_dir = None
libs_dir = None # where Android libs are cached after build but # the Android project folder where everything ends up
# before being placed in dists dist_dir = None
# where Android libs are cached after build
# but before being placed in dists
libs_dir = None
aars_dir = None aars_dir = None
ccache = None # whether to use ccache ccache = None # whether to use ccache
@ -121,17 +130,17 @@ class Context(object):
self._android_api = value self._android_api = value
@property @property
def ndk_ver(self): def ndk_api(self):
'''The version of the NDK being used for compilation.''' '''The API number compile against'''
if self._ndk_ver is None: if self._ndk_api is None:
raise ValueError('Tried to access ndk_ver but it has not ' raise ValueError('Tried to access ndk_api but it has not '
'been set - this should not happen, something ' 'been set - this should not happen, something '
'went wrong!') 'went wrong!')
return self._ndk_ver return self._ndk_api
@ndk_ver.setter @ndk_api.setter
def ndk_ver(self, value): def ndk_api(self, value):
self._ndk_ver = value self._ndk_api = value
@property @property
def sdk_dir(self): def sdk_dir(self):
@ -159,9 +168,11 @@ class Context(object):
def ndk_dir(self, value): def ndk_dir(self, value):
self._ndk_dir = value self._ndk_dir = value
def prepare_build_environment(self, user_sdk_dir, user_ndk_dir, def prepare_build_environment(self,
user_android_api, user_android_min_api, user_sdk_dir,
user_ndk_ver): user_ndk_dir,
user_android_api,
user_ndk_api):
'''Checks that build dependencies exist and sets internal variables '''Checks that build dependencies exist and sets internal variables
for the Android SDK etc. for the Android SDK etc.
@ -180,12 +191,14 @@ class Context(object):
sdk_dir = None sdk_dir = None
if user_sdk_dir: if user_sdk_dir:
sdk_dir = user_sdk_dir sdk_dir = user_sdk_dir
if sdk_dir is None: # This is the old P4A-specific var # This is the old P4A-specific var
if sdk_dir is None:
sdk_dir = environ.get('ANDROIDSDK', None) sdk_dir = environ.get('ANDROIDSDK', None)
if sdk_dir is None: # This seems used more conventionally # This seems used more conventionally
if sdk_dir is None:
sdk_dir = environ.get('ANDROID_HOME', None) sdk_dir = environ.get('ANDROID_HOME', None)
if sdk_dir is None: # Checks in the buildozer SDK dir, useful # Checks in the buildozer SDK dir, useful for debug tests of p4a
# for debug tests of p4a if sdk_dir is None:
possible_dirs = glob.glob(expanduser(join( possible_dirs = glob.glob(expanduser(join(
'~', '.buildozer', 'android', 'platform', 'android-sdk-*'))) '~', '.buildozer', 'android', 'platform', 'android-sdk-*')))
possible_dirs = [d for d in possible_dirs if not possible_dirs = [d for d in possible_dirs if not
@ -199,57 +212,25 @@ class Context(object):
'maintain your own SDK download.') 'maintain your own SDK download.')
sdk_dir = possible_dirs[0] sdk_dir = possible_dirs[0]
if sdk_dir is None: if sdk_dir is None:
warning('Android SDK dir was not specified, exiting.') raise BuildInterruptingException('Android SDK dir was not specified, exiting.')
exit(1)
self.sdk_dir = realpath(sdk_dir) self.sdk_dir = realpath(sdk_dir)
# Check what Android API we're using # Check what Android API we're using
android_api = None android_api = None
if user_android_api: if user_android_api:
android_api = user_android_api android_api = user_android_api
if android_api is not None: info('Getting Android API version from user argument: {}'.format(android_api))
info('Getting Android API version from user argument') elif 'ANDROIDAPI' in environ:
if android_api is None: android_api = environ['ANDROIDAPI']
android_api = environ.get('ANDROIDAPI', None) info('Found Android API target in $ANDROIDAPI: {}'.format(android_api))
if android_api is not None: else:
info('Found Android API target in $ANDROIDAPI')
if android_api is None:
info('Android API target was not set manually, using ' info('Android API target was not set manually, using '
'the default of {}'.format(DEFAULT_ANDROID_API)) 'the default of {}'.format(RECOMMENDED_TARGET_API))
android_api = DEFAULT_ANDROID_API android_api = RECOMMENDED_TARGET_API
android_api = int(android_api) android_api = int(android_api)
self.android_api = android_api self.android_api = android_api
if self.android_api >= 21 and self.archs[0].arch == 'armeabi': check_target_api(android_api, self.archs[0].arch)
error('Asked to build for armeabi architecture with API '
'{}, but API 21 or greater does not support armeabi'.format(
self.android_api))
error('You probably want to build with --arch=armeabi-v7a instead')
exit(1)
# try to determinate min_api
android_min_api = None
if user_android_min_api:
android_min_api = user_android_min_api
if android_min_api is not None:
info('Getting Minimum Android API version from user argument')
if android_min_api is None:
android_min_api = environ.get("ANDROIDMINAPI", None)
if android_min_api is not None:
info('Found Android minimum api in $ANDROIDMINAPI')
if android_min_api is None:
info('Minimum Android API was not set, using current Android API '
'{}'.format(android_api))
android_min_api = android_api
android_min_api = int(android_min_api)
self.android_min_api = android_min_api
info("Requested API {} (minimum {})".format(
self.android_api, self.android_min_api))
if self.android_min_api > android_api:
error('Android minimum api cannot be higher than Android api')
exit(1)
if exists(join(sdk_dir, 'tools', 'bin', 'avdmanager')): if exists(join(sdk_dir, 'tools', 'bin', 'avdmanager')):
avdmanager = sh.Command(join(sdk_dir, 'tools', 'bin', 'avdmanager')) avdmanager = sh.Command(join(sdk_dir, 'tools', 'bin', 'avdmanager'))
@ -258,9 +239,9 @@ class Context(object):
android = sh.Command(join(sdk_dir, 'tools', 'android')) android = sh.Command(join(sdk_dir, 'tools', 'android'))
targets = android('list').stdout.decode('utf-8').split('\n') targets = android('list').stdout.decode('utf-8').split('\n')
else: else:
error('Could not find `android` or `sdkmanager` binaries in ' raise BuildInterruptingException(
'Android SDK. Exiting.') 'Could not find `android` or `sdkmanager` binaries in Android SDK',
exit(1) instructions='Make sure the path to the Android SDK is correct')
apis = [s for s in targets if re.match(r'^ *API level: ', s)] apis = [s for s in targets if re.match(r'^ *API level: ', s)]
apis = [re.findall(r'[0-9]+', s) for s in apis] apis = [re.findall(r'[0-9]+', s) for s in apis]
apis = [int(s[0]) for s in apis if s] apis = [int(s[0]) for s in apis if s]
@ -270,30 +251,28 @@ class Context(object):
info(('Requested API target {} is available, ' info(('Requested API target {} is available, '
'continuing.').format(android_api)) 'continuing.').format(android_api))
else: else:
warning(('Requested API target {} is not available, install ' raise BuildInterruptingException(
('Requested API target {} is not available, install '
'it with the SDK android tool.').format(android_api)) 'it with the SDK android tool.').format(android_api))
warning('Exiting.')
exit(1)
# Find the Android NDK # Find the Android NDK
# Could also use ANDROID_NDK, but doesn't look like many tools use this # Could also use ANDROID_NDK, but doesn't look like many tools use this
ndk_dir = None ndk_dir = None
if user_ndk_dir: if user_ndk_dir:
ndk_dir = user_ndk_dir ndk_dir = user_ndk_dir
if ndk_dir is not None:
info('Getting NDK dir from from user argument') info('Getting NDK dir from from user argument')
if ndk_dir is None: # The old P4A-specific dir if ndk_dir is None: # The old P4A-specific dir
ndk_dir = environ.get('ANDROIDNDK', None) ndk_dir = environ.get('ANDROIDNDK', None)
if ndk_dir is not None: if ndk_dir is not None:
info('Found NDK dir in $ANDROIDNDK') info('Found NDK dir in $ANDROIDNDK: {}'.format(ndk_dir))
if ndk_dir is None: # Apparently the most common convention if ndk_dir is None: # Apparently the most common convention
ndk_dir = environ.get('NDK_HOME', None) ndk_dir = environ.get('NDK_HOME', None)
if ndk_dir is not None: if ndk_dir is not None:
info('Found NDK dir in $NDK_HOME') info('Found NDK dir in $NDK_HOME: {}'.format(ndk_dir))
if ndk_dir is None: # Another convention (with maven?) if ndk_dir is None: # Another convention (with maven?)
ndk_dir = environ.get('ANDROID_NDK_HOME', None) ndk_dir = environ.get('ANDROID_NDK_HOME', None)
if ndk_dir is not None: if ndk_dir is not None:
info('Found NDK dir in $ANDROID_NDK_HOME') info('Found NDK dir in $ANDROID_NDK_HOME: {}'.format(ndk_dir))
if ndk_dir is None: # Checks in the buildozer NDK dir, useful if ndk_dir is None: # Checks in the buildozer NDK dir, useful
# # for debug tests of p4a # # for debug tests of p4a
possible_dirs = glob.glob(expanduser(join( possible_dirs = glob.glob(expanduser(join(
@ -307,62 +286,31 @@ class Context(object):
'maintain your own NDK download.') 'maintain your own NDK download.')
ndk_dir = possible_dirs[0] ndk_dir = possible_dirs[0]
if ndk_dir is None: if ndk_dir is None:
warning('Android NDK dir was not specified, exiting.') raise BuildInterruptingException('Android NDK dir was not specified')
exit(1)
self.ndk_dir = realpath(ndk_dir) self.ndk_dir = realpath(ndk_dir)
# Find the NDK version, and check it against what the NDK dir check_ndk_version(ndk_dir)
# seems to report
ndk_ver = None
if user_ndk_ver:
ndk_ver = user_ndk_ver
if ndk_dir is not None:
info('Got NDK version from from user argument')
if ndk_ver is None:
ndk_ver = environ.get('ANDROIDNDKVER', None)
if ndk_dir is not None:
info('Got NDK version from $ANDROIDNDKVER')
self.ndk = 'google' self.ndk = 'crystax' # force crystax detection
try: ndk_api = None
with open(join(ndk_dir, 'RELEASE.TXT')) as fileh: if user_ndk_api:
reported_ndk_ver = fileh.read().split(' ')[0].strip() ndk_api = user_ndk_api
except IOError: info('Getting NDK API version (i.e. minimum supported API) from user argument')
pass elif 'NDKAPI' in environ:
ndk_api = environ.get('NDKAPI', None)
info('Found Android API target in $NDKAPI')
else: else:
if reported_ndk_ver.startswith('crystax-ndk-'): ndk_api = min(self.android_api, RECOMMENDED_NDK_API)
reported_ndk_ver = reported_ndk_ver[12:] warning('NDK API target was not set manually, using '
self.ndk = 'crystax' 'the default of {} = min(android-api={}, default ndk-api={})'.format(
if ndk_ver is None: ndk_api, self.android_api, RECOMMENDED_NDK_API))
ndk_ver = reported_ndk_ver ndk_api = int(ndk_api)
info(('Got Android NDK version from the NDK dir: ' self.ndk_api = ndk_api
'it is {}').format(ndk_ver))
else:
if ndk_ver != reported_ndk_ver:
warning('NDK version was set as {}, but checking '
'the NDK dir claims it is {}.'.format(
ndk_ver, reported_ndk_ver))
warning('The build will try to continue, but it may '
'fail and you should check '
'that your setting is correct.')
warning('If the NDK dir result is correct, you don\'t '
'need to manually set the NDK ver.')
if ndk_ver is None:
warning('Android NDK version could not be found. This probably'
'won\'t cause any problems, but if necessary you can'
'set it with `--ndk-version=...`.')
self.ndk_ver = ndk_ver
info('Using {} NDK {}'.format(self.ndk.capitalize(), self.ndk_ver)) check_ndk_api(ndk_api, self.android_api)
virtualenv = None virtualenv = get_virtualenv_executable()
if virtualenv is None:
virtualenv = sh.which('virtualenv2')
if virtualenv is None:
virtualenv = sh.which('virtualenv-2.7')
if virtualenv is None:
virtualenv = sh.which('virtualenv')
if virtualenv is None: if virtualenv is None:
raise IOError('Couldn\'t find a virtualenv executable, ' raise IOError('Couldn\'t find a virtualenv executable, '
'you must install this to use p4a.') 'you must install this to use p4a.')
@ -374,14 +322,13 @@ class Context(object):
if not self.ccache: if not self.ccache:
info('ccache is missing, the build will not be optimized in the ' info('ccache is missing, the build will not be optimized in the '
'future.') 'future.')
for cython_fn in ("cython2", "cython-2.7", "cython"): for cython_fn in ("cython", "cython3", "cython2", "cython-2.7"):
cython = sh.which(cython_fn) cython = sh.which(cython_fn)
if cython: if cython:
self.cython = cython self.cython = cython
break break
else: else:
error('No cython binary found. Exiting.') raise BuildInterruptingException('No cython binary found.')
exit(1)
if not self.cython: if not self.cython:
ok = False ok = False
warning("Missing requirement: cython is not installed") warning("Missing requirement: cython is not installed")
@ -394,9 +341,8 @@ class Context(object):
self.ndk_platform = join( self.ndk_platform = join(
self.ndk_dir, self.ndk_dir,
'platforms', 'platforms',
'android-{}'.format(self.android_min_api), 'android-{}'.format(self.ndk_api),
platform_dir) platform_dir)
if not exists(self.ndk_platform): if not exists(self.ndk_platform):
warning('ndk_platform doesn\'t exist: {}'.format( warning('ndk_platform doesn\'t exist: {}'.format(
self.ndk_platform)) self.ndk_platform))
@ -408,7 +354,7 @@ class Context(object):
toolchain_versions = [] toolchain_versions = []
toolchain_path = join(self.ndk_dir, 'toolchains') toolchain_path = join(self.ndk_dir, 'toolchains')
if os.path.isdir(toolchain_path): if isdir(toolchain_path):
toolchain_contents = glob.glob('{}/{}-*'.format(toolchain_path, toolchain_contents = glob.glob('{}/{}-*'.format(toolchain_path,
toolchain_prefix)) toolchain_prefix))
toolchain_versions = [split(path)[-1][len(toolchain_prefix) + 1:] toolchain_versions = [split(path)[-1][len(toolchain_prefix) + 1:]
@ -456,9 +402,8 @@ class Context(object):
executable)) executable))
if not ok: if not ok:
error('{}python-for-android cannot continue; aborting{}'.format( raise BuildInterruptingException(
Err_Fore.RED, Err_Fore.RESET)) 'python-for-android cannot continue due to the missing executables above')
sys.exit(1)
def __init__(self): def __init__(self):
super(Context, self).__init__() super(Context, self).__init__()
@ -469,7 +414,7 @@ class Context(object):
self._sdk_dir = None self._sdk_dir = None
self._ndk_dir = None self._ndk_dir = None
self._android_api = None self._android_api = None
self._ndk_ver = None self._ndk_api = None
self.ndk = None self.ndk = None
self.toolchain_prefix = None self.toolchain_prefix = None
@ -483,6 +428,7 @@ class Context(object):
ArchARM(self), ArchARM(self),
ArchARMv7_a(self), ArchARMv7_a(self),
Archx86(self), Archx86(self),
Archx86_64(self),
ArchAarch_64(self), ArchAarch_64(self),
) )
@ -504,8 +450,7 @@ class Context(object):
new_archs.add(match) new_archs.add(match)
self.archs = list(new_archs) self.archs = list(new_archs)
if not self.archs: if not self.archs:
warning('Asked to compile for no Archs, so failing.') raise BuildInterruptingException('Asked to compile for no Archs, so failing.')
exit(1)
info('Will compile for the following archs: {}'.format( info('Will compile for the following archs: {}'.format(
', '.join([arch.arch for arch in self.archs]))) ', '.join([arch.arch for arch in self.archs])))
@ -523,14 +468,10 @@ class Context(object):
'''Returns the location of site-packages in the python-install build '''Returns the location of site-packages in the python-install build
dir. dir.
''' '''
if self.python_recipe.name == 'python2legacy':
# This needs to be replaced with something more general in
# order to support multiple python versions and/or multiple
# archs.
if self.python_recipe.from_crystax:
return self.get_python_install_dir()
return join(self.get_python_install_dir(), return join(self.get_python_install_dir(),
'lib', 'python3.7', 'site-packages') 'lib', 'python2.7', 'site-packages')
return self.get_python_install_dir()
def get_libs_dir(self, arch): def get_libs_dir(self, arch):
'''The libs dir for a given arch.''' '''The libs dir for a given arch.'''
@ -541,9 +482,33 @@ class Context(object):
return exists(join(self.get_libs_dir(arch), lib)) return exists(join(self.get_libs_dir(arch), lib))
def has_package(self, name, arch=None): def has_package(self, name, arch=None):
# If this is a file path, it'll need special handling:
if (name.find("/") >= 0 or name.find("\\") >= 0) and \
name.find("://") < 0: # (:// would indicate an url)
if not os.path.exists(name):
# Non-existing dir, cannot look this up.
return False
if os.path.exists(os.path.join(name, "setup.py")):
# Get name from setup.py:
name = subprocess.check_output([
sys.executable, "setup.py", "--name"],
cwd=name)
try:
name = name.decode('utf-8', 'replace')
except AttributeError:
pass
name = name.strip()
if len(name) == 0:
# Failed to look up any meaningful name.
return False
else:
# A folder with whatever, cannot look this up.
return False
# Try to look up recipe by name:
try: try:
recipe = Recipe.get_recipe(name, self) recipe = Recipe.get_recipe(name, self)
except IOError: except ValueError:
pass pass
else: else:
name = getattr(recipe, 'site_packages_name', None) or name name = getattr(recipe, 'site_packages_name', None) or name
@ -562,7 +527,6 @@ class Context(object):
def build_recipes(build_order, python_modules, ctx): def build_recipes(build_order, python_modules, ctx):
# Put recipes in correct build order # Put recipes in correct build order
bs = ctx.bootstrap
info_notify("Recipe build order is {}".format(build_order)) info_notify("Recipe build order is {}".format(build_order))
if python_modules: if python_modules:
python_modules = sorted(set(python_modules)) python_modules = sorted(set(python_modules))
@ -635,7 +599,13 @@ def run_pymodules_install(ctx, modules):
venv = sh.Command(ctx.virtualenv) venv = sh.Command(ctx.virtualenv)
with current_directory(join(ctx.build_dir)): with current_directory(join(ctx.build_dir)):
shprint(venv, '--python=python3.7', 'venv') shprint(venv,
'--python=python{}'.format(
ctx.python_recipe.major_minor_version_string.
partition(".")[0]
),
'venv'
)
info('Creating a requirements.txt file for the Python modules') info('Creating a requirements.txt file for the Python modules')
with open('requirements.txt', 'w') as fileh: with open('requirements.txt', 'w') as fileh:
@ -647,18 +617,63 @@ def run_pymodules_install(ctx, modules):
line = '{}\n'.format(module) line = '{}\n'.format(module)
fileh.write(line) fileh.write(line)
info('Installing Python modules with pip') # Prepare base environment and upgrade pip:
info('If this fails with a message about /bin/false, this ' base_env = copy.copy(os.environ)
'probably means the package cannot be installed with ' base_env["PYTHONPATH"] = ctx.get_site_packages_dir()
'pip as it needs a compilation recipe.') info('Upgrade pip to latest version')
shprint(sh.bash, '-c', (
"source venv/bin/activate && pip install -U pip"
), _env=copy.copy(base_env))
# This bash method is what old-p4a used # Install Cython in case modules need it to build:
# It works but should be replaced with something better info('Install Cython in case one of the modules needs it to build')
shprint(sh.bash, '-c', (
"venv/bin/pip install Cython"
), _env=copy.copy(base_env))
# Get environment variables for build (with CC/compiler set):
standard_recipe = CythonRecipe()
standard_recipe.ctx = ctx
# (note: following line enables explicit -lpython... linker options)
standard_recipe.call_hostpython_via_targetpython = False
recipe_env = standard_recipe.get_recipe_env(ctx.archs[0])
env = copy.copy(base_env)
env.update(recipe_env)
info('Installing Python modules with pip')
info('IF THIS FAILS, THE MODULES MAY NEED A RECIPE. '
'A reason for this is often modules compiling '
'native code that is unaware of Android cross-compilation '
'and does not work without additional '
'changes / workarounds.')
# Make sure our build package dir is available, and the virtualenv
# site packages come FIRST (so the proper pip version is used):
env["PYTHONPATH"] += ":" + ctx.get_site_packages_dir()
env["PYTHONPATH"] = os.path.abspath(join(
ctx.build_dir, "venv", "lib",
"python" + ctx.python_recipe.major_minor_version_string,
"site-packages")) + ":" + env["PYTHONPATH"]
'''
# Do actual install:
shprint(sh.bash, '-c', (
"venv/bin/pip " +
"install -v --target '{0}' --no-deps -r requirements.txt"
).format(ctx.get_site_packages_dir().replace("'", "'\"'\"'")),
_env=copy.copy(env))
'''
# use old install script
shprint(sh.bash, '-c', ( shprint(sh.bash, '-c', (
"source venv/bin/activate && env CC=/bin/false CXX=/bin/false " "source venv/bin/activate && env CC=/bin/false CXX=/bin/false "
"PYTHONPATH={0} pip install --target '{0}' --no-deps -r requirements.txt" "PYTHONPATH={0} pip install --target '{0}' --no-deps -r requirements.txt"
).format(ctx.get_site_packages_dir())) ).format(ctx.get_site_packages_dir()))
# Strip object files after potential Cython or native code builds:
standard_recipe.strip_object_files(ctx.archs[0], env,
build_dir=ctx.build_dir)
def biglink(ctx, arch): def biglink(ctx, arch):
# First, collate object files from each recipe # First, collate object files from each recipe

View file

@ -2,9 +2,9 @@ from os.path import exists, join
import glob import glob
import json import json
from pythonforandroid.logger import (info, info_notify, warning, from pythonforandroid.logger import (info, info_notify, warning, Err_Style, Err_Fore)
Err_Style, Err_Fore) from pythonforandroid.util import current_directory, BuildInterruptingException
from pythonforandroid.util import current_directory from shutil import rmtree
class Distribution(object): class Distribution(object):
@ -21,6 +21,7 @@ class Distribution(object):
needs_build = False # Whether the dist needs compiling needs_build = False # Whether the dist needs compiling
url = None url = None
dist_dir = None # Where the dist dir ultimately is. Should not be None. dist_dir = None # Where the dist dir ultimately is. Should not be None.
ndk_api = None
archs = [] archs = []
'''The arch targets that the dist is built for.''' '''The arch targets that the dist is built for.'''
@ -42,9 +43,11 @@ class Distribution(object):
@classmethod @classmethod
def get_distribution(cls, ctx, name=None, recipes=[], def get_distribution(cls, ctx, name=None, recipes=[],
ndk_api=None,
force_build=False, force_build=False,
extra_dist_dirs=[], extra_dist_dirs=[],
require_perfect_match=False): require_perfect_match=False,
allow_replace_dist=True):
'''Takes information about the distribution, and decides what kind of '''Takes information about the distribution, and decides what kind of
distribution it will be. distribution it will be.
@ -68,21 +71,31 @@ class Distribution(object):
require_perfect_match : bool require_perfect_match : bool
If True, will only match distributions with precisely the If True, will only match distributions with precisely the
correct set of recipes. correct set of recipes.
allow_replace_dist : bool
If True, will allow an existing dist with the specified
name but incompatible requirements to be overwritten by
a new one with the current requirements.
''' '''
existing_dists = Distribution.get_distributions(ctx) existing_dists = Distribution.get_distributions(ctx)
needs_build = True # whether the dist needs building, will be returned
possible_dists = existing_dists possible_dists = existing_dists
name_match_dist = None
# 0) Check if a dist with that name already exists # 0) Check if a dist with that name already exists
if name is not None and name: if name is not None and name:
possible_dists = [d for d in possible_dists if d.name == name] possible_dists = [d for d in possible_dists if d.name == name]
if possible_dists:
name_match_dist = possible_dists[0]
# 1) Check if any existing dists meet the requirements # 1) Check if any existing dists meet the requirements
_possible_dists = [] _possible_dists = []
for dist in possible_dists: for dist in possible_dists:
if (
ndk_api is not None and dist.ndk_api != ndk_api
) or dist.ndk_api is None:
continue
for recipe in recipes: for recipe in recipes:
if recipe not in dist.recipes: if recipe not in dist.recipes:
break break
@ -97,10 +110,12 @@ class Distribution(object):
else: else:
info('No existing dists meet the given requirements!') info('No existing dists meet the given requirements!')
# If any dist has perfect recipes, return it # If any dist has perfect recipes and ndk API, return it
for dist in possible_dists: for dist in possible_dists:
if force_build: if force_build:
continue continue
if ndk_api is not None and dist.ndk_api != ndk_api:
continue
if (set(dist.recipes) == set(recipes) or if (set(dist.recipes) == set(recipes) or
(set(recipes).issubset(set(dist.recipes)) and (set(recipes).issubset(set(dist.recipes)) and
not require_perfect_match)): not require_perfect_match)):
@ -110,33 +125,20 @@ class Distribution(object):
assert len(possible_dists) < 2 assert len(possible_dists) < 2
if not name and possible_dists: # If there was a name match but we didn't already choose it,
info('Asked for dist with name {} with recipes ({}), but a dist ' # then the existing dist is incompatible with the requested
'with this name already exists and has incompatible recipes ' # configuration and the build cannot continue
'({})'.format(name, ', '.join(recipes), if name_match_dist is not None and not allow_replace_dist:
', '.join(possible_dists[0].recipes))) raise BuildInterruptingException(
info('No compatible dist found, so exiting.') 'Asked for dist with name {name} with recipes ({req_recipes}) and '
exit(1) 'NDK API {req_ndk_api}, but a dist '
'with this name already exists and has either incompatible recipes '
# # 2) Check if any downloadable dists meet the requirements '({dist_recipes}) or NDK API {dist_ndk_api}'.format(
name=name,
# online_dists = [('testsdl2', ['hostpython2', 'sdl2_image', req_ndk_api=ndk_api,
# 'sdl2_mixer', 'sdl2_ttf', dist_ndk_api=name_match_dist.ndk_api,
# 'python2', 'sdl2', req_recipes=', '.join(recipes),
# 'pyjniussdl2', 'kivysdl2'], dist_recipes=', '.join(name_match_dist.recipes)))
# 'https://github.com/inclement/sdl2-example-dist/archive/master.zip'),
# ]
# _possible_dists = []
# for dist_name, dist_recipes, dist_url in online_dists:
# for recipe in recipes:
# if recipe not in dist_recipes:
# break
# else:
# dist = Distribution(ctx)
# dist.name = dist_name
# dist.url = dist_url
# _possible_dists.append(dist)
# # if _possible_dists
# If we got this far, we need to build a new dist # If we got this far, we need to build a new dist
dist = Distribution(ctx) dist = Distribution(ctx)
@ -152,16 +154,23 @@ class Distribution(object):
dist.name = name dist.name = name
dist.dist_dir = join(ctx.dist_dir, dist.name) dist.dist_dir = join(ctx.dist_dir, dist.name)
dist.recipes = recipes dist.recipes = recipes
dist.ndk_api = ctx.ndk_api
return dist return dist
def folder_exists(self):
return exists(self.dist_dir)
def delete(self):
rmtree(self.dist_dir)
@classmethod @classmethod
def get_distributions(cls, ctx, extra_dist_dirs=[]): def get_distributions(cls, ctx, extra_dist_dirs=[]):
'''Returns all the distributions found locally.''' '''Returns all the distributions found locally.'''
if extra_dist_dirs: if extra_dist_dirs:
warning('extra_dist_dirs argument to get_distributions ' raise BuildInterruptingException(
'extra_dist_dirs argument to get_distributions '
'is not yet implemented') 'is not yet implemented')
exit(1)
dist_dir = ctx.dist_dir dist_dir = ctx.dist_dir
folders = glob.glob(join(dist_dir, '*')) folders = glob.glob(join(dist_dir, '*'))
for dir in extra_dist_dirs: for dir in extra_dist_dirs:
@ -179,40 +188,47 @@ class Distribution(object):
dist.recipes = dist_info['recipes'] dist.recipes = dist_info['recipes']
if 'archs' in dist_info: if 'archs' in dist_info:
dist.archs = dist_info['archs'] dist.archs = dist_info['archs']
if 'ndk_api' in dist_info:
dist.ndk_api = dist_info['ndk_api']
else:
dist.ndk_api = None
warning(
"Distribution {distname}: ({distdir}) has been "
"built with an unknown api target, ignoring it, "
"you might want to delete it".format(
distname=dist.name,
distdir=dist.dist_dir
)
)
dists.append(dist) dists.append(dist)
return dists return dists
def save_info(self): def save_info(self, dirn):
''' '''
Save information about the distribution in its dist_dir. Save information about the distribution in its dist_dir.
''' '''
with current_directory(self.dist_dir): with current_directory(dirn):
info('Saving distribution info') info('Saving distribution info')
with open('dist_info.json', 'w') as fileh: with open('dist_info.json', 'w') as fileh:
json.dump({'dist_name': self.name, json.dump({'dist_name': self.ctx.dist_name,
'bootstrap': self.ctx.bootstrap.name,
'archs': [arch.arch for arch in self.ctx.archs], 'archs': [arch.arch for arch in self.ctx.archs],
'recipes': self.ctx.recipe_build_order}, 'ndk_api': self.ctx.ndk_api,
'recipes': self.ctx.recipe_build_order + self.ctx.python_modules,
'hostpython': self.ctx.hostpython,
'python_version': self.ctx.python_recipe.major_minor_version_string},
fileh) fileh)
def load_info(self):
'''Load information about the dist from the info file that p4a
automatically creates.'''
with current_directory(self.dist_dir):
filen = 'dist_info.json'
if not exists(filen):
return None
with open('dist_info.json', 'r') as fileh:
dist_info = json.load(fileh)
return dist_info
def pretty_log_dists(dists, log_func=info): def pretty_log_dists(dists, log_func=info):
infos = [] infos = []
for dist in dists: for dist in dists:
infos.append('{Fore.GREEN}{Style.BRIGHT}{name}{Style.RESET_ALL}: ' ndk_api = 'unknown' if dist.ndk_api is None else dist.ndk_api
infos.append('{Fore.GREEN}{Style.BRIGHT}{name}{Style.RESET_ALL}: min API {ndk_api}, '
'includes recipes ({Fore.GREEN}{recipes}' 'includes recipes ({Fore.GREEN}{recipes}'
'{Style.RESET_ALL}), built for archs ({Fore.BLUE}' '{Style.RESET_ALL}), built for archs ({Fore.BLUE}'
'{archs}{Style.RESET_ALL})'.format( '{archs}{Style.RESET_ALL})'.format(
ndk_api=ndk_api,
name=dist.name, recipes=', '.join(dist.recipes), name=dist.name, recipes=', '.join(dist.recipes),
archs=', '.join(dist.archs) if dist.archs else 'UNKNOWN', archs=', '.join(dist.archs) if dist.archs else 'UNKNOWN',
Fore=Err_Fore, Style=Err_Style)) Fore=Err_Fore, Style=Err_Style))

View file

@ -1,24 +1,37 @@
from copy import deepcopy from copy import deepcopy
from itertools import product from itertools import product
from sys import exit
from pythonforandroid.logger import (info, warning, error) from pythonforandroid.logger import info
from pythonforandroid.recipe import Recipe from pythonforandroid.recipe import Recipe
from pythonforandroid.bootstrap import Bootstrap from pythonforandroid.bootstrap import Bootstrap
from pythonforandroid.util import BuildInterruptingException
def fix_deplist(deps):
""" Turn a dependency list into lowercase, and make sure all entries
that are just a string become a tuple of strings
"""
deps = [
((dep.lower(),)
if not isinstance(dep, (list, tuple))
else tuple([dep_entry.lower()
for dep_entry in dep
]))
for dep in deps
]
return deps
class RecipeOrder(dict): class RecipeOrder(dict):
def __init__(self, ctx): def __init__(self, ctx):
self.ctx = ctx self.ctx = ctx
def conflicts(self, name): def conflicts(self):
for name in self.keys(): for name in self.keys():
try: try:
recipe = Recipe.get_recipe(name, self.ctx) recipe = Recipe.get_recipe(name, self.ctx)
conflicts = recipe.conflicts conflicts = [dep.lower() for dep in recipe.conflicts]
except IOError: except ValueError:
conflicts = [] conflicts = []
if any([c in self for c in conflicts]): if any([c in self for c in conflicts]):
@ -26,26 +39,59 @@ class RecipeOrder(dict):
return False return False
def recursively_collect_orders(name, ctx, orders=[]): def get_dependency_tuple_list_for_recipe(recipe, blacklist=None):
""" Get the dependencies of a recipe with filtered out blacklist, and
turned into tuples with fix_deplist()
"""
if blacklist is None:
blacklist = set()
assert(type(blacklist) == set)
if recipe.depends is None:
dependencies = []
else:
# Turn all dependencies into tuples so that product will work
dependencies = fix_deplist(recipe.depends)
# Filter out blacklisted items and turn lowercase:
dependencies = [
tuple(set(deptuple) - blacklist)
for deptuple in dependencies
if tuple(set(deptuple) - blacklist)
]
return dependencies
def recursively_collect_orders(
name, ctx, all_inputs, orders=None, blacklist=None
):
'''For each possible recipe ordering, try to add the new recipe name '''For each possible recipe ordering, try to add the new recipe name
to that order. Recursively do the same thing with all the to that order. Recursively do the same thing with all the
dependencies of each recipe. dependencies of each recipe.
''' '''
name = name.lower()
if orders is None:
orders = []
if blacklist is None:
blacklist = set()
try: try:
recipe = Recipe.get_recipe(name, ctx) recipe = Recipe.get_recipe(name, ctx)
if recipe.depends is None: dependencies = get_dependency_tuple_list_for_recipe(
dependencies = [] recipe, blacklist=blacklist
else: )
# make all dependencies into lists so that product will work
dependencies = [([dependency] if not isinstance( # handle opt_depends: these impose requirements on the build
dependency, (list, tuple)) # order only if already present in the list of recipes to build
else dependency) for dependency in recipe.depends] dependencies.extend(fix_deplist(
[[d] for d in recipe.get_opt_depends_in_list(all_inputs)
if d.lower() not in blacklist]
))
if recipe.conflicts is None: if recipe.conflicts is None:
conflicts = [] conflicts = []
else: else:
conflicts = recipe.conflicts conflicts = [dep.lower() for dep in recipe.conflicts]
except IOError: except ValueError:
# The recipe does not exist, so we assume it can be installed # The recipe does not exist, so we assume it can be installed
# via pip with no extra dependencies # via pip with no extra dependencies
dependencies = [] dependencies = []
@ -57,7 +103,7 @@ def recursively_collect_orders(name, ctx, orders=[]):
if name in order: if name in order:
new_orders.append(deepcopy(order)) new_orders.append(deepcopy(order))
continue continue
if order.conflicts(name): if order.conflicts():
continue continue
if any([conflict in order for conflict in conflicts]): if any([conflict in order for conflict in conflicts]):
continue continue
@ -69,7 +115,9 @@ def recursively_collect_orders(name, ctx, orders=[]):
dependency_new_orders = [new_order] dependency_new_orders = [new_order]
for dependency in dependency_set: for dependency in dependency_set:
dependency_new_orders = recursively_collect_orders( dependency_new_orders = recursively_collect_orders(
dependency, ctx, dependency_new_orders) dependency, ctx, all_inputs, dependency_new_orders,
blacklist=blacklist
)
new_orders.extend(dependency_new_orders) new_orders.extend(dependency_new_orders)
@ -95,22 +143,142 @@ def find_order(graph):
bset.discard(result) bset.discard(result)
def get_recipe_order_and_bootstrap(ctx, names, bs=None): def obvious_conflict_checker(ctx, name_tuples, blacklist=None):
recipes_to_load = set(names) """ This is a pre-flight check function that will completely ignore
if bs is not None and bs.recipe_depends: recipe order or choosing an actual value in any of the multiple
recipes_to_load = recipes_to_load.union(set(bs.recipe_depends)) choice tuples/dependencies, and just do a very basic obvious
conflict check.
"""
deps_were_added_by = dict()
deps = set()
if blacklist is None:
blacklist = set()
possible_orders = [] # Add dependencies for all recipes:
to_be_added = [(name_tuple, None) for name_tuple in name_tuples]
while len(to_be_added) > 0:
current_to_be_added = list(to_be_added)
to_be_added = []
for (added_tuple, adding_recipe) in current_to_be_added:
assert(type(added_tuple) == tuple)
if len(added_tuple) > 1:
# No obvious commitment in what to add, don't check it itself
# but throw it into deps for later comparing against
# (Remember this function only catches obvious issues)
deps.add(added_tuple)
continue
name = added_tuple[0]
recipe_conflicts = set()
recipe_dependencies = []
try:
# Get recipe to add and who's ultimately adding it:
recipe = Recipe.get_recipe(name, ctx)
recipe_conflicts = {c.lower() for c in recipe.conflicts}
recipe_dependencies = get_dependency_tuple_list_for_recipe(
recipe, blacklist=blacklist
)
except ValueError:
pass
adder_first_recipe_name = adding_recipe or name
# Collect the conflicts:
triggered_conflicts = []
for dep_tuple_list in deps:
# See if the new deps conflict with things added before:
if set(dep_tuple_list).intersection(
recipe_conflicts) == set(dep_tuple_list):
triggered_conflicts.append(dep_tuple_list)
continue
# See if what was added before conflicts with the new deps:
if len(dep_tuple_list) > 1:
# Not an obvious commitment to a specific recipe/dep
# to be added, so we won't check.
# (remember this function only catches obvious issues)
continue
try:
dep_recipe = Recipe.get_recipe(dep_tuple_list[0], ctx)
except ValueError:
continue
conflicts = [c.lower() for c in dep_recipe.conflicts]
if name in conflicts:
triggered_conflicts.append(dep_tuple_list)
# Throw error on conflict:
if triggered_conflicts:
# Get first conflict and see who added that one:
adder_second_recipe_name = "'||'".join(triggered_conflicts[0])
second_recipe_original_adder = deps_were_added_by.get(
(adder_second_recipe_name,), None
)
if second_recipe_original_adder:
adder_second_recipe_name = second_recipe_original_adder
# Prompt error:
raise BuildInterruptingException(
"Conflict detected: '{}'"
" inducing dependencies {}, and '{}'"
" inducing conflicting dependencies {}".format(
adder_first_recipe_name,
(recipe.name,),
adder_second_recipe_name,
triggered_conflicts[0]
))
# Actually add it to our list:
deps.add(added_tuple)
deps_were_added_by[added_tuple] = adding_recipe
# Schedule dependencies to be added
to_be_added += [
(dep, adder_first_recipe_name or name)
for dep in recipe_dependencies
if dep not in deps
]
# If we came here, then there were no obvious conflicts.
return None
def get_recipe_order_and_bootstrap(ctx, names, bs=None, blacklist=None):
# Get set of recipe/dependency names, clean up and add bootstrap deps:
names = set(names)
if bs is not None and bs.recipe_depends:
names = names.union(set(bs.recipe_depends))
names = fix_deplist([
([name] if not isinstance(name, (list, tuple)) else name)
for name in names
])
if blacklist is None:
blacklist = set()
blacklist = {bitem.lower() for bitem in blacklist}
# Remove all values that are in the blacklist:
names_before_blacklist = list(names)
names = []
for name in names_before_blacklist:
cleaned_up_tuple = tuple([
item for item in name if item not in blacklist
])
if cleaned_up_tuple:
names.append(cleaned_up_tuple)
# Do check for obvious conflicts (that would trigger in any order, and
# without comitting to any specific choice in a multi-choice tuple of
# dependencies):
obvious_conflict_checker(ctx, names, blacklist=blacklist)
# If we get here, no obvious conflicts!
# get all possible order graphs, as names may include tuples/lists # get all possible order graphs, as names may include tuples/lists
# of alternative dependencies # of alternative dependencies
names = [([name] if not isinstance(name, (list, tuple)) else name) possible_orders = []
for name in names]
for name_set in product(*names): for name_set in product(*names):
new_possible_orders = [RecipeOrder(ctx)] new_possible_orders = [RecipeOrder(ctx)]
for name in name_set: for name in name_set:
new_possible_orders = recursively_collect_orders( new_possible_orders = recursively_collect_orders(
name, ctx, orders=new_possible_orders) name, ctx, name_set, orders=new_possible_orders,
blacklist=blacklist
)
possible_orders.extend(new_possible_orders) possible_orders.extend(new_possible_orders)
# turn each order graph into a linear list if possible # turn each order graph into a linear list if possible
@ -122,23 +290,18 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None):
info('Circular dependency found in graph {}, skipping it.'.format( info('Circular dependency found in graph {}, skipping it.'.format(
possible_order)) possible_order))
continue continue
except:
warning('Failed to import recipe named {}; the recipe exists '
'but appears broken.'.format(name))
warning('Exception was:')
raise
orders.append(list(order)) orders.append(list(order))
# prefer python2 and SDL2 if available # prefer python3 and SDL2 if available
orders = sorted(orders, orders = sorted(orders,
key=lambda order: -('python2' in order) - ('sdl2' in order)) key=lambda order: -('python3' in order) - ('sdl2' in order))
if not orders: if not orders:
error('Didn\'t find any valid dependency graphs.') raise BuildInterruptingException(
error('This means that some of your requirements pull in ' 'Didn\'t find any valid dependency graphs. '
'conflicting dependencies.') 'This means that some of your '
error('Exiting.') 'requirements pull in conflicting dependencies.')
exit(1)
# It would be better to check against possible orders other # It would be better to check against possible orders other
# than the first one, but in practice clashes will be rare, # than the first one, but in practice clashes will be rare,
# and can be resolved by specifying more parameters # and can be resolved by specifying more parameters
@ -153,18 +316,26 @@ def get_recipe_order_and_bootstrap(ctx, names, bs=None):
if bs is None: if bs is None:
bs = Bootstrap.get_bootstrap_from_recipes(chosen_order, ctx) bs = Bootstrap.get_bootstrap_from_recipes(chosen_order, ctx)
if bs is None:
# Note: don't remove this without thought, causes infinite loop
raise BuildInterruptingException(
"Could not find any compatible bootstrap!"
)
recipes, python_modules, bs = get_recipe_order_and_bootstrap( recipes, python_modules, bs = get_recipe_order_and_bootstrap(
ctx, chosen_order, bs=bs) ctx, chosen_order, bs=bs, blacklist=blacklist
)
else: else:
# check if each requirement has a recipe # check if each requirement has a recipe
recipes = [] recipes = []
python_modules = [] python_modules = []
for name in chosen_order: for name in chosen_order:
try: try:
Recipe.get_recipe(name, ctx) recipe = Recipe.get_recipe(name, ctx)
except IOError: python_modules += recipe.python_depends
except ValueError:
python_modules.append(name) python_modules.append(name)
else: else:
recipes.append(name) recipes.append(name)
python_modules = list(set(python_modules))
return recipes, python_modules, bs return recipes, python_modules, bs

View file

@ -44,9 +44,9 @@ class LevelDifferentiatingFormatter(logging.Formatter):
logger = logging.getLogger('p4a') logger = logging.getLogger('p4a')
if not hasattr(logger, 'touched'): # Necessary as importlib reloads # Necessary as importlib reloads this,
# this, which would add a second # which would add a second handler and reset the level
# handler and reset the level if not hasattr(logger, 'touched'):
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
logger.touched = True logger.touched = True
ch = logging.StreamHandler(stderr) ch = logging.StreamHandler(stderr)
@ -148,8 +148,10 @@ def shprint(command, *args, **kwargs):
kwargs["_bg"] = True kwargs["_bg"] = True
is_critical = kwargs.pop('_critical', False) is_critical = kwargs.pop('_critical', False)
tail_n = kwargs.pop('_tail', None) tail_n = kwargs.pop('_tail', None)
full_debug = False
if "P4A_FULL_DEBUG" in os.environ: if "P4A_FULL_DEBUG" in os.environ:
tail_n = 0 tail_n = 0
full_debug = True
filter_in = kwargs.pop('_filter', None) filter_in = kwargs.pop('_filter', None)
filter_out = kwargs.pop('_filterout', None) filter_out = kwargs.pop('_filterout', None)
if len(logger.handlers) > 1: if len(logger.handlers) > 1:
@ -177,11 +179,16 @@ def shprint(command, *args, **kwargs):
if isinstance(line, bytes): if isinstance(line, bytes):
line = line.decode('utf-8', errors='replace') line = line.decode('utf-8', errors='replace')
if logger.level > logging.DEBUG: if logger.level > logging.DEBUG:
if full_debug:
stdout.write(line)
stdout.flush()
continue
msg = line.replace( msg = line.replace(
'\n', ' ').replace( '\n', ' ').replace(
'\t', ' ').replace( '\t', ' ').replace(
'\b', ' ').rstrip() '\b', ' ').rstrip()
if msg: if msg:
if "CI" not in os.environ:
stdout.write(u'{}\r{}{:<{width}}'.format( stdout.write(u'{}\r{}{:<{width}}'.format(
Err_Style.RESET_ALL, msg_hdr, Err_Style.RESET_ALL, msg_hdr,
shorten_string(msg, msg_width), width=msg_width)) shorten_string(msg, msg_width), width=msg_width))

437
p4a/pythonforandroid/python.py Executable file
View file

@ -0,0 +1,437 @@
'''
This module is kind of special because it contains the base classes used to
build our python3 and python2 recipes and his corresponding hostpython recipes.
'''
from os.path import dirname, exists, join
from multiprocessing import cpu_count
from shutil import copy2
from os import environ
import subprocess
import glob
import sh
from pythonforandroid.recipe import Recipe, TargetPythonRecipe
from pythonforandroid.logger import logger, info, shprint
from pythonforandroid.util import (
current_directory, ensure_dir, walk_valid_filens,
BuildInterruptingException, build_platform)
class GuestPythonRecipe(TargetPythonRecipe):
'''
Class for target python recipes. Sets ctx.python_recipe to point to itself,
so as to know later what kind of Python was built or used.
This base class is used for our main python recipes (python2 and python3)
which shares most of the build process.
.. versionadded:: 0.6.0
Refactored from the inclement's python3 recipe with a few changes:
- Splits the python's build process several methods: :meth:`build_arch`
and :meth:`get_recipe_env`.
- Adds the attribute :attr:`configure_args`, which has been moved from
the method :meth:`build_arch` into a static class variable.
- Adds some static class variables used to create the python bundle and
modifies the method :meth:`create_python_bundle`, to adapt to the new
situation. The added static class variables are:
:attr:`stdlib_dir_blacklist`, :attr:`stdlib_filen_blacklist`,
:attr:`site_packages_dir_blacklist`and
:attr:`site_packages_filen_blacklist`.
'''
MIN_NDK_API = 21
'''Sets the minimal ndk api number needed to use the recipe.
.. warning:: This recipe can be built only against API 21+, so it means
that any class which inherits from class:`GuestPythonRecipe` will have
this limitation.
'''
from_crystax = False
'''True if the python is used from CrystaX, False otherwise (i.e. if
it is built by p4a).'''
configure_args = ()
'''The configure arguments needed to build the python recipe. Those are
used in method :meth:`build_arch` (if not overwritten like python3crystax's
recipe does).
.. note:: This variable should be properly set in subclass.
'''
stdlib_dir_blacklist = {
'__pycache__',
'test',
'tests',
'lib2to3',
'ensurepip',
'idlelib',
'tkinter',
}
'''The directories that we want to omit for our python bundle'''
stdlib_filen_blacklist = [
'*.py',
'*.exe',
'*.whl',
]
'''The file extensions that we want to blacklist for our python bundle'''
site_packages_dir_blacklist = {
'__pycache__',
'tests'
}
'''The directories from site packages dir that we don't want to be included
in our python bundle.'''
site_packages_filen_blacklist = [
'*.py'
]
'''The file extensions from site packages dir that we don't want to be
included in our python bundle.'''
opt_depends = ['sqlite3', 'libffi', 'openssl']
'''The optional libraries which we would like to get our python linked'''
compiled_extension = '.pyc'
'''the default extension for compiled python files.
.. note:: the default extension for compiled python files has been .pyo for
python 2.x-3.4 but as of Python 3.5, the .pyo filename extension is no
longer used and has been removed in favour of extension .pyc
'''
def __init__(self, *args, **kwargs):
self._ctx = None
super(GuestPythonRecipe, self).__init__(*args, **kwargs)
def get_recipe_env(self, arch=None, with_flags_in_cc=True):
if self.from_crystax:
return super(GuestPythonRecipe, self).get_recipe_env(
arch=arch, with_flags_in_cc=with_flags_in_cc)
env = environ.copy()
android_host = env['HOSTARCH'] = arch.command_prefix
toolchain = '{toolchain_prefix}-{toolchain_version}'.format(
toolchain_prefix=self.ctx.toolchain_prefix,
toolchain_version=self.ctx.toolchain_version)
toolchain = join(self.ctx.ndk_dir, 'toolchains',
toolchain, 'prebuilt', build_platform)
env['CC'] = (
'{clang} -target {target} -gcc-toolchain {toolchain}').format(
clang=join(self.ctx.ndk_dir, 'toolchains', 'llvm', 'prebuilt',
build_platform, 'bin', 'clang'),
target=arch.target,
toolchain=toolchain)
env['AR'] = join(toolchain, 'bin', android_host) + '-ar'
env['LD'] = join(toolchain, 'bin', android_host) + '-ld'
env['RANLIB'] = join(toolchain, 'bin', android_host) + '-ranlib'
env['READELF'] = join(toolchain, 'bin', android_host) + '-readelf'
env['STRIP'] = join(toolchain, 'bin', android_host) + '-strip'
env['STRIP'] += ' --strip-debug --strip-unneeded'
env['PATH'] = (
'{hostpython_dir}:{old_path}').format(
hostpython_dir=self.get_recipe(
'host' + self.name, self.ctx).get_path_to_python(),
old_path=env['PATH'])
ndk_flags = (
'-fPIC --sysroot={ndk_sysroot} -D__ANDROID_API__={android_api} '
'-isystem {ndk_android_host} -I{ndk_include}').format(
ndk_sysroot=join(self.ctx.ndk_dir, 'sysroot'),
android_api=self.ctx.ndk_api,
ndk_android_host=join(
self.ctx.ndk_dir, 'sysroot', 'usr', 'include', android_host),
ndk_include=join(self.ctx.ndk_dir, 'sysroot', 'usr', 'include'))
sysroot = self.ctx.ndk_platform
env['CFLAGS'] = env.get('CFLAGS', '') + ' ' + ndk_flags
env['CPPFLAGS'] = env.get('CPPFLAGS', '') + ' ' + ndk_flags
env['LDFLAGS'] = env.get('LDFLAGS', '') + ' --sysroot={} -L{}'.format(
sysroot, join(sysroot, 'usr', 'lib'))
# Manually add the libs directory, and copy some object
# files to the current directory otherwise they aren't
# picked up. This seems necessary because the --sysroot
# setting in LDFLAGS is overridden by the other flags.
# TODO: Work out why this doesn't happen in the original
# bpo-30386 Makefile system.
logger.warning('Doing some hacky stuff to link properly')
lib_dir = join(sysroot, 'usr', 'lib')
if arch.arch == 'x86_64':
lib_dir = join(sysroot, 'usr', 'lib64')
env['LDFLAGS'] += ' -L{}'.format(lib_dir)
shprint(sh.cp, join(lib_dir, 'crtbegin_so.o'), './')
shprint(sh.cp, join(lib_dir, 'crtend_so.o'), './')
env['SYSROOT'] = sysroot
if sh.which('lld') is not None:
# Note: The -L. is to fix a bug in python 3.7.
# https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=234409
env["LDFLAGS"] += ' -L. -fuse-ld=lld'
else:
logger.warning('lld not found, linking without it. ' +
'Consider installing lld if linker errors occur.')
return env
def set_libs_flags(self, env, arch):
'''Takes care to properly link libraries with python depending on our
requirements and the attribute :attr:`opt_depends`.
'''
def add_flags(include_flags, link_dirs, link_libs):
env['CPPFLAGS'] = env.get('CPPFLAGS', '') + include_flags
env['LDFLAGS'] = env.get('LDFLAGS', '') + link_dirs
env['LIBS'] = env.get('LIBS', '') + link_libs
if 'sqlite3' in self.ctx.recipe_build_order:
info('Activating flags for sqlite3')
recipe = Recipe.get_recipe('sqlite3', self.ctx)
add_flags(' -I' + recipe.get_build_dir(arch.arch),
' -L' + recipe.get_lib_dir(arch), ' -lsqlite3')
if 'libffi' in self.ctx.recipe_build_order:
info('Activating flags for libffi')
recipe = Recipe.get_recipe('libffi', self.ctx)
# In order to force the correct linkage for our libffi library, we
# set the following variable to point where is our libffi.pc file,
# because the python build system uses pkg-config to configure it.
env['PKG_CONFIG_PATH'] = recipe.get_build_dir(arch.arch)
add_flags(' -I' + ' -I'.join(recipe.get_include_dirs(arch)),
' -L' + join(recipe.get_build_dir(arch.arch), '.libs'),
' -lffi')
if 'openssl' in self.ctx.recipe_build_order:
info('Activating flags for openssl')
recipe = Recipe.get_recipe('openssl', self.ctx)
add_flags(recipe.include_flags(arch),
recipe.link_dirs_flags(arch), recipe.link_libs_flags())
return env
def prebuild_arch(self, arch):
super(TargetPythonRecipe, self).prebuild_arch(arch)
if self.from_crystax and self.ctx.ndk != 'crystax':
raise BuildInterruptingException(
'The {} recipe can only be built when using the CrystaX NDK. '
'Exiting.'.format(self.name))
self.ctx.python_recipe = self
def build_arch(self, arch):
if self.ctx.ndk_api < self.MIN_NDK_API:
raise BuildInterruptingException(
'Target ndk-api is {}, but the python3 recipe supports only'
' {}+'.format(self.ctx.ndk_api, self.MIN_NDK_API))
recipe_build_dir = self.get_build_dir(arch.arch)
# Create a subdirectory to actually perform the build
build_dir = join(recipe_build_dir, 'android-build')
ensure_dir(build_dir)
# TODO: Get these dynamically, like bpo-30386 does
sys_prefix = '/usr/local'
sys_exec_prefix = '/usr/local'
with current_directory(build_dir):
env = self.get_recipe_env(arch)
env = self.set_libs_flags(env, arch)
android_build = sh.Command(
join(recipe_build_dir,
'config.guess'))().stdout.strip().decode('utf-8')
if not exists('config.status'):
shprint(
sh.Command(join(recipe_build_dir, 'configure')),
*(' '.join(self.configure_args).format(
android_host=env['HOSTARCH'],
android_build=android_build,
prefix=sys_prefix,
exec_prefix=sys_exec_prefix)).split(' '),
_env=env)
if not exists('python'):
py_version = self.major_minor_version_string
if self.major_minor_version_string[0] == '3':
py_version += 'm'
shprint(sh.make, 'all', '-j', str(cpu_count()),
'INSTSONAME=libpython{version}.so'.format(
version=py_version), _env=env)
# TODO: Look into passing the path to pyconfig.h in a
# better way, although this is probably acceptable
sh.cp('pyconfig.h', join(recipe_build_dir, 'Include'))
def include_root(self, arch_name):
return join(self.get_build_dir(arch_name), 'Include')
def link_root(self, arch_name):
return join(self.get_build_dir(arch_name), 'android-build')
def compile_python_files(self, dir):
'''
Compile the python files (recursively) for the python files inside
a given folder.
.. note:: python2 compiles the files into extension .pyo, but in
python3, and as of Python 3.5, the .pyo filename extension is no
longer used...uses .pyc (https://www.python.org/dev/peps/pep-0488)
'''
args = [self.ctx.hostpython]
if self.ctx.python_recipe.name == 'python3':
args += ['-OO', '-m', 'compileall', '-b', '-f', dir]
else:
args += ['-OO', '-m', 'compileall', '-f', dir]
subprocess.call(args)
def create_python_bundle(self, dirn, arch):
"""
Create a packaged python bundle in the target directory, by
copying all the modules and standard library to the right
place.
"""
# Todo: find a better way to find the build libs folder
modules_build_dir = join(
self.get_build_dir(arch.arch),
'android-build',
'build',
'lib.linux{}-{}-{}'.format(
'2' if self.version[0] == '2' else '',
arch.command_prefix.split('-')[0],
self.major_minor_version_string
))
# Compile to *.pyc/*.pyo the python modules
self.compile_python_files(modules_build_dir)
# Compile to *.pyc/*.pyo the standard python library
self.compile_python_files(join(self.get_build_dir(arch.arch), 'Lib'))
# Compile to *.pyc/*.pyo the other python packages (site-packages)
self.compile_python_files(self.ctx.get_python_install_dir())
# Bundle compiled python modules to a folder
modules_dir = join(dirn, 'modules')
c_ext = self.compiled_extension
ensure_dir(modules_dir)
module_filens = (glob.glob(join(modules_build_dir, '*.so')) +
glob.glob(join(modules_build_dir, '*' + c_ext)))
info("Copy {} files into the bundle".format(len(module_filens)))
for filen in module_filens:
info(" - copy {}".format(filen))
copy2(filen, modules_dir)
# zip up the standard library
stdlib_zip = join(dirn, 'stdlib.zip')
with current_directory(join(self.get_build_dir(arch.arch), 'Lib')):
stdlib_filens = list(walk_valid_filens(
'.', self.stdlib_dir_blacklist, self.stdlib_filen_blacklist))
info("Zip {} files into the bundle".format(len(stdlib_filens)))
shprint(sh.zip, stdlib_zip, *stdlib_filens)
# copy the site-packages into place
ensure_dir(join(dirn, 'site-packages'))
ensure_dir(self.ctx.get_python_install_dir())
# TODO: Improve the API around walking and copying the files
with current_directory(self.ctx.get_python_install_dir()):
filens = list(walk_valid_filens(
'.', self.site_packages_dir_blacklist,
self.site_packages_filen_blacklist))
info("Copy {} files into the site-packages".format(len(filens)))
for filen in filens:
info(" - copy {}".format(filen))
ensure_dir(join(dirn, 'site-packages', dirname(filen)))
copy2(filen, join(dirn, 'site-packages', filen))
# copy the python .so files into place
python_build_dir = join(self.get_build_dir(arch.arch),
'android-build')
python_lib_name = 'libpython' + self.major_minor_version_string
if self.major_minor_version_string[0] == '3':
python_lib_name += 'm'
shprint(sh.cp, join(python_build_dir, python_lib_name + '.so'),
join(self.ctx.dist_dir, self.ctx.dist_name, 'libs', arch.arch))
info('Renaming .so files to reflect cross-compile')
self.reduce_object_file_names(join(dirn, 'site-packages'))
return join(dirn, 'site-packages')
class HostPythonRecipe(Recipe):
'''
This is the base class for hostpython3 and hostpython2 recipes. This class
will take care to do all the work to build a hostpython recipe but, be
careful, it is intended to be subclassed because some of the vars needs to
be set:
- :attr:`name`
- :attr:`version`
.. versionadded:: 0.6.0
Refactored from the hostpython3's recipe by inclement
'''
name = ''
'''The hostpython's recipe name. This should be ``hostpython2`` or
``hostpython3``
.. warning:: This must be set in inherited class.'''
version = ''
'''The hostpython's recipe version.
.. warning:: This must be set in inherited class.'''
build_subdir = 'native-build'
'''Specify the sub build directory for the hostpython recipe. Defaults
to ``native-build``.'''
url = 'https://www.python.org/ftp/python/{version}/Python-{version}.tgz'
'''The default url to download our host python recipe. This url will
change depending on the python version set in attribute :attr:`version`.'''
def get_build_container_dir(self, arch=None):
choices = self.check_recipe_choices()
dir_name = '-'.join([self.name] + choices)
return join(self.ctx.build_dir, 'other_builds', dir_name, 'desktop')
def get_build_dir(self, arch=None):
'''
.. note:: Unlike other recipes, the hostpython build dir doesn't
depend on the target arch
'''
return join(self.get_build_container_dir(), self.name)
def get_path_to_python(self):
return join(self.get_build_dir(), self.build_subdir)
def build_arch(self, arch):
recipe_build_dir = self.get_build_dir(arch.arch)
# Create a subdirectory to actually perform the build
build_dir = join(recipe_build_dir, self.build_subdir)
ensure_dir(build_dir)
if not exists(join(build_dir, 'python')):
with current_directory(recipe_build_dir):
# Configure the build
with current_directory(build_dir):
if not exists('config.status'):
shprint(
sh.Command(join(recipe_build_dir, 'configure')))
# Create the Setup file. This copying from Setup.dist
# seems to be the normal and expected procedure.
shprint(sh.cp, join('Modules', 'Setup.dist'),
join(build_dir, 'Modules', 'Setup'))
shprint(sh.make, '-j', str(cpu_count()), '-C', build_dir)
else:
info('Skipping {name} ({version}) build, as it has already '
'been completed'.format(name=self.name, version=self.version))
self.ctx.hostpython = join(build_dir, 'python')

View file

@ -1,4 +1,4 @@
from os.path import basename, dirname, exists, isdir, isfile, join, realpath from os.path import basename, dirname, exists, isdir, isfile, join, realpath, split
import importlib import importlib
import glob import glob
from shutil import rmtree from shutil import rmtree
@ -12,16 +12,16 @@ import shutil
import fnmatch import fnmatch
from os import listdir, unlink, environ, mkdir, curdir, walk from os import listdir, unlink, environ, mkdir, curdir, walk
from sys import stdout from sys import stdout
import time
try: try:
from urlparse import urlparse from urlparse import urlparse
except ImportError: except ImportError:
from urllib.parse import urlparse from urllib.parse import urlparse
from pythonforandroid.logger import (logger, info, warning, error, debug, shprint, info_main) from pythonforandroid.logger import (logger, info, warning, debug, shprint, info_main)
from pythonforandroid.util import (urlretrieve, current_directory, ensure_dir) from pythonforandroid.util import (urlretrieve, current_directory, ensure_dir,
BuildInterruptingException)
# this import is necessary to keep imp.load_source from complaining :) # this import is necessary to keep imp.load_source from complaining :)
if PY2: if PY2:
import imp import imp
import_recipe = imp.load_source import_recipe = imp.load_source
@ -140,13 +140,26 @@ class Recipe(with_metaclass(RecipeMeta)):
else: else:
progression = '{0:.2f}%'.format( progression = '{0:.2f}%'.format(
index * blksize * 100. / float(size)) index * blksize * 100. / float(size))
if "CI" not in environ:
stdout.write('- Download {}\r'.format(progression)) stdout.write('- Download {}\r'.format(progression))
stdout.flush() stdout.flush()
if exists(target): if exists(target):
unlink(target) unlink(target)
# Download item with multiple attempts (for bad connections):
attempts = 0
while True:
try:
urlretrieve(url, target, report_hook) urlretrieve(url, target, report_hook)
except OSError as e:
attempts += 1
if attempts >= 5:
raise e
stdout.write('Download failed retrying in a second...')
time.sleep(1)
continue
break
return target return target
elif parsed_url.scheme in ('git', 'git+file', 'git+ssh', 'git+http', 'git+https'): elif parsed_url.scheme in ('git', 'git+file', 'git+ssh', 'git+http', 'git+https'):
if isdir(target): if isdir(target):
@ -167,28 +180,18 @@ class Recipe(with_metaclass(RecipeMeta)):
shprint(sh.git, 'submodule', 'update', '--recursive') shprint(sh.git, 'submodule', 'update', '--recursive')
return target return target
# def get_archive_rootdir(self, filename): def apply_patch(self, filename, arch, build_dir=None):
# if filename.endswith(".tgz") or filename.endswith(".tar.gz") or \
# filename.endswith(".tbz2") or filename.endswith(".tar.bz2"):
# archive = tarfile.open(filename)
# root = archive.next().path.split("/")
# return root[0]
# elif filename.endswith(".zip"):
# with zipfile.ZipFile(filename) as zf:
# return dirname(zf.namelist()[0])
# else:
# print("Error: cannot detect root directory")
# print("Unrecognized extension for {}".format(filename))
# raise Exception()
def apply_patch(self, filename, arch):
""" """
Apply a patch from the current recipe directory into the current Apply a patch from the current recipe directory into the current
build directory. build directory.
.. versionchanged:: 0.6.0
Add ability to apply patch from any dir via kwarg `build_dir`'''
""" """
info("Applying patch {}".format(filename)) info("Applying patch {}".format(filename))
build_dir = build_dir if build_dir else self.get_build_dir(arch)
filename = join(self.get_recipe_dir(), filename) filename = join(self.get_recipe_dir(), filename)
shprint(sh.patch, "-t", "-d", self.get_build_dir(arch), "-p1", shprint(sh.patch, "-t", "-d", build_dir, "-p1",
"-i", filename, _tail=10) "-i", filename, _tail=10)
def copy_file(self, filename, dest): def copy_file(self, filename, dest):
@ -206,42 +209,12 @@ class Recipe(with_metaclass(RecipeMeta)):
with open(dest, "ab") as fd: with open(dest, "ab") as fd:
fd.write(data) fd.write(data)
# def has_marker(self, marker):
# """
# Return True if the current build directory has the marker set
# """
# return exists(join(self.build_dir, ".{}".format(marker)))
# def set_marker(self, marker):
# """
# Set a marker info the current build directory
# """
# with open(join(self.build_dir, ".{}".format(marker)), "w") as fd:
# fd.write("ok")
# def delete_marker(self, marker):
# """
# Delete a specific marker
# """
# try:
# unlink(join(self.build_dir, ".{}".format(marker)))
# except:
# pass
@property @property
def name(self): def name(self):
'''The name of the recipe, the same as the folder containing it.''' '''The name of the recipe, the same as the folder containing it.'''
modname = self.__class__.__module__ modname = self.__class__.__module__
return modname.split(".", 2)[-1] return modname.split(".", 2)[-1]
# @property
# def archive_fn(self):
# bfn = basename(self.url.format(version=self.version))
# fn = "{}/{}-{}".format(
# self.ctx.cache_dir,
# self.name, bfn)
# return fn
@property @property
def filtered_archs(self): def filtered_archs(self):
'''Return archs of self.ctx that are valid build archs '''Return archs of self.ctx that are valid build archs
@ -269,6 +242,12 @@ class Recipe(with_metaclass(RecipeMeta)):
recipes.append(recipe) recipes.append(recipe)
return sorted(recipes) return sorted(recipes)
def get_opt_depends_in_list(self, recipes):
'''Given a list of recipe names, returns those that are also in
self.opt_depends.
'''
return [recipe for recipe in recipes if recipe in self.opt_depends]
def get_build_container_dir(self, arch): def get_build_container_dir(self, arch):
'''Given the arch name, returns the directory where it will be '''Given the arch name, returns the directory where it will be
built. built.
@ -277,7 +256,8 @@ class Recipe(with_metaclass(RecipeMeta)):
alternative or optional dependencies are being built. alternative or optional dependencies are being built.
''' '''
dir_name = self.get_dir_name() dir_name = self.get_dir_name()
return join(self.ctx.build_dir, 'other_builds', dir_name, arch) return join(self.ctx.build_dir, 'other_builds',
dir_name, '{}__ndk_target_{}'.format(arch, self.ctx.ndk_api))
def get_dir_name(self): def get_dir_name(self):
choices = self.check_recipe_choices() choices = self.check_recipe_choices()
@ -410,24 +390,20 @@ class Recipe(with_metaclass(RecipeMeta)):
try: try:
sh.unzip(extraction_filename) sh.unzip(extraction_filename)
except (sh.ErrorReturnCode_1, sh.ErrorReturnCode_2): except (sh.ErrorReturnCode_1, sh.ErrorReturnCode_2):
pass # return code 1 means unzipping had # return code 1 means unzipping had
# warnings but did complete, # warnings but did complete,
# apparently happens sometimes with # apparently happens sometimes with
# github zips # github zips
pass
import zipfile import zipfile
fileh = zipfile.ZipFile(extraction_filename, 'r') fileh = zipfile.ZipFile(extraction_filename, 'r')
root_directory = fileh.filelist[0].filename.split('/')[0] root_directory = fileh.filelist[0].filename.split('/')[0]
if root_directory != basename(directory_name): if root_directory != basename(directory_name):
shprint(sh.mv, root_directory, directory_name) shprint(sh.mv, root_directory, directory_name)
elif (extraction_filename.endswith('.tar.gz') or elif extraction_filename.endswith(
extraction_filename.endswith('.tgz') or ('.tar.gz', '.tgz', '.tar.bz2', '.tbz2', '.tar.xz', '.txz')):
extraction_filename.endswith('.tar.bz2') or
extraction_filename.endswith('.tbz2') or
extraction_filename.endswith('.tar.xz') or
extraction_filename.endswith('.txz')):
sh.tar('xf', extraction_filename) sh.tar('xf', extraction_filename)
root_directory = shprint( root_directory = sh.tar('tf', extraction_filename).stdout.decode(
sh.tar, 'tf', extraction_filename).stdout.decode(
'utf-8').split('\n')[0].split('/')[0] 'utf-8').split('\n')[0].split('/')[0]
if root_directory != directory_name: if root_directory != directory_name:
shprint(sh.mv, root_directory, directory_name) shprint(sh.mv, root_directory, directory_name)
@ -450,12 +426,12 @@ class Recipe(with_metaclass(RecipeMeta)):
else: else:
info('{} is already unpacked, skipping'.format(self.name)) info('{} is already unpacked, skipping'.format(self.name))
def get_recipe_env(self, arch=None, with_flags_in_cc=True): def get_recipe_env(self, arch=None, with_flags_in_cc=True, clang=False):
"""Return the env specialized for the recipe """Return the env specialized for the recipe
""" """
if arch is None: if arch is None:
arch = self.filtered_archs[0] arch = self.filtered_archs[0]
return arch.get_env(with_flags_in_cc=with_flags_in_cc) return arch.get_env(with_flags_in_cc=with_flags_in_cc, clang=clang)
def prebuild_arch(self, arch): def prebuild_arch(self, arch):
'''Run any pre-build tasks for the Recipe. By default, this checks if '''Run any pre-build tasks for the Recipe. By default, this checks if
@ -471,8 +447,11 @@ class Recipe(with_metaclass(RecipeMeta)):
build_dir = self.get_build_dir(arch.arch) build_dir = self.get_build_dir(arch.arch)
return exists(join(build_dir, '.patched')) return exists(join(build_dir, '.patched'))
def apply_patches(self, arch): def apply_patches(self, arch, build_dir=None):
'''Apply any patches for the Recipe.''' '''Apply any patches for the Recipe.
.. versionchanged:: 0.6.0
Add ability to apply patches from any dir via kwarg `build_dir`'''
if self.patches: if self.patches:
info_main('Applying patches for {}[{}]' info_main('Applying patches for {}[{}]'
.format(self.name, arch.arch)) .format(self.name, arch.arch))
@ -481,6 +460,7 @@ class Recipe(with_metaclass(RecipeMeta)):
info_main('{} already patched, skipping'.format(self.name)) info_main('{} already patched, skipping'.format(self.name))
return return
build_dir = build_dir if build_dir else self.get_build_dir(arch.arch)
for patch in self.patches: for patch in self.patches:
if isinstance(patch, (tuple, list)): if isinstance(patch, (tuple, list)):
patch, patch_check = patch patch, patch_check = patch
@ -489,9 +469,9 @@ class Recipe(with_metaclass(RecipeMeta)):
self.apply_patch( self.apply_patch(
patch.format(version=self.version, arch=arch.arch), patch.format(version=self.version, arch=arch.arch),
arch.arch) arch.arch, build_dir=build_dir)
shprint(sh.touch, join(self.get_build_dir(arch.arch), '.patched')) shprint(sh.touch, join(build_dir, '.patched'))
def should_build(self, arch): def should_build(self, arch):
'''Should perform any necessary test and return True only if it needs '''Should perform any necessary test and return True only if it needs
@ -547,8 +527,8 @@ class Recipe(with_metaclass(RecipeMeta)):
if exists(base_dir): if exists(base_dir):
dirs.append(base_dir) dirs.append(base_dir)
if not dirs: if not dirs:
warning(('Attempted to clean build for {} but found no existing ' warning('Attempted to clean build for {} but found no existing '
'build dirs').format(self.name)) 'build dirs'.format(self.name))
for directory in dirs: for directory in dirs:
if exists(directory): if exists(directory):
@ -595,6 +575,7 @@ class Recipe(with_metaclass(RecipeMeta)):
@classmethod @classmethod
def get_recipe(cls, name, ctx): def get_recipe(cls, name, ctx):
'''Returns the Recipe with the given name, if it exists.''' '''Returns the Recipe with the given name, if it exists.'''
name = name.lower()
if not hasattr(cls, "recipes"): if not hasattr(cls, "recipes"):
cls.recipes = {} cls.recipes = {}
if name in cls.recipes: if name in cls.recipes:
@ -602,20 +583,28 @@ class Recipe(with_metaclass(RecipeMeta)):
recipe_file = None recipe_file = None
for recipes_dir in cls.recipe_dirs(ctx): for recipes_dir in cls.recipe_dirs(ctx):
recipe_file = join(recipes_dir, name, '__init__.py') if not exists(recipes_dir):
continue
# Find matching folder (may differ in case):
for subfolder in listdir(recipes_dir):
if subfolder.lower() == name:
recipe_file = join(recipes_dir, subfolder, '__init__.py')
if exists(recipe_file): if exists(recipe_file):
name = subfolder # adapt to actual spelling
break break
recipe_file = None recipe_file = None
if recipe_file is not None:
break
if not recipe_file: if not recipe_file:
raise IOError('Recipe does not exist: {}'.format(name)) raise ValueError('Recipe does not exist: {}'.format(name))
mod = import_recipe('pythonforandroid.recipes.{}'.format(name), recipe_file) mod = import_recipe('pythonforandroid.recipes.{}'.format(name), recipe_file)
if len(logger.handlers) > 1: if len(logger.handlers) > 1:
logger.removeHandler(logger.handlers[1]) logger.removeHandler(logger.handlers[1])
recipe = mod.recipe recipe = mod.recipe
recipe.ctx = ctx recipe.ctx = ctx
cls.recipes[name] = recipe cls.recipes[name.lower()] = recipe
return recipe return recipe
@ -626,8 +615,8 @@ class IncludedFilesBehaviour(object):
def prepare_build_dir(self, arch): def prepare_build_dir(self, arch):
if self.src_filename is None: if self.src_filename is None:
print('IncludedFilesBehaviour failed: no src_filename specified') raise BuildInterruptingException(
exit(1) 'IncludedFilesBehaviour failed: no src_filename specified')
shprint(sh.rm, '-rf', self.get_build_dir(arch)) shprint(sh.rm, '-rf', self.get_build_dir(arch))
shprint(sh.cp, '-a', join(self.get_recipe_dir(), self.src_filename), shprint(sh.cp, '-a', join(self.get_recipe_dir(), self.src_filename),
self.get_build_dir(arch)) self.get_build_dir(arch))
@ -640,6 +629,9 @@ class BootstrapNDKRecipe(Recipe):
To build an NDK project which is not part of the bootstrap, see To build an NDK project which is not part of the bootstrap, see
:class:`~pythonforandroid.recipe.NDKRecipe`. :class:`~pythonforandroid.recipe.NDKRecipe`.
To link with python, call the method :meth:`get_recipe_env`
with the kwarg *with_python=True*.
''' '''
dir_name = None # The name of the recipe build folder in the jni dir dir_name = None # The name of the recipe build folder in the jni dir
@ -656,6 +648,20 @@ class BootstrapNDKRecipe(Recipe):
def get_jni_dir(self): def get_jni_dir(self):
return join(self.ctx.bootstrap.build_dir, 'jni') return join(self.ctx.bootstrap.build_dir, 'jni')
def get_recipe_env(self, arch=None, with_flags_in_cc=True, with_python=False):
env = super(BootstrapNDKRecipe, self).get_recipe_env(
arch, with_flags_in_cc)
if not with_python:
return env
env['PYTHON_INCLUDE_ROOT'] = self.ctx.python_recipe.include_root(arch.arch)
env['PYTHON_LINK_ROOT'] = self.ctx.python_recipe.link_root(arch.arch)
env['EXTRA_LDLIBS'] = ' -lpython{}'.format(
self.ctx.python_recipe.major_minor_version_string)
if 'python3' in self.ctx.python_recipe.name:
env['EXTRA_LDLIBS'] += 'm'
return env
class NDKRecipe(Recipe): class NDKRecipe(Recipe):
'''A recipe class for any NDK project not included in the bootstrap.''' '''A recipe class for any NDK project not included in the bootstrap.'''
@ -682,7 +688,13 @@ class NDKRecipe(Recipe):
env = self.get_recipe_env(arch) env = self.get_recipe_env(arch)
with current_directory(self.get_build_dir(arch.arch)): with current_directory(self.get_build_dir(arch.arch)):
shprint(sh.ndk_build, 'V=1', 'APP_ABI=' + arch.arch, *extra_args, _env=env) shprint(
sh.ndk_build,
'V=1',
'APP_PLATFORM=android-' + str(self.ctx.ndk_api),
'APP_ABI=' + arch.arch,
*extra_args, _env=env
)
class PythonRecipe(Recipe): class PythonRecipe(Recipe):
@ -711,6 +723,13 @@ class PythonRecipe(Recipe):
setup_extra_args = [] setup_extra_args = []
'''List of extra arugments to pass to setup.py''' '''List of extra arugments to pass to setup.py'''
def __init__(self, *args, **kwargs):
super(PythonRecipe, self).__init__(*args, **kwargs)
depends = self.depends
depends.append(('python2', 'python2legacy', 'python3', 'python3crystax'))
depends = list(set(depends))
self.depends = depends
def clean_build(self, arch=None): def clean_build(self, arch=None):
super(PythonRecipe, self).clean_build(arch=arch) super(PythonRecipe, self).clean_build(arch=arch)
name = self.folder_name name = self.folder_name
@ -726,14 +745,12 @@ class PythonRecipe(Recipe):
@property @property
def real_hostpython_location(self): def real_hostpython_location(self):
if 'hostpython2' in self.ctx.recipe_build_order: host_name = 'host{}'.format(self.ctx.python_recipe.name)
return join( host_build = Recipe.get_recipe(host_name, self.ctx).get_build_dir()
Recipe.get_recipe('hostpython2', self.ctx).get_build_dir(), if host_name in ['hostpython2', 'hostpython3']:
'hostpython') return join(host_build, 'native-build', 'python')
elif 'hostpython3crystax' in self.ctx.recipe_build_order: elif host_name in ['hostpython3crystax', 'hostpython2legacy']:
return join( return join(host_build, 'hostpython')
Recipe.get_recipe('hostpython3crystax', self.ctx).get_build_dir(),
'hostpython')
else: else:
python_recipe = self.ctx.python_recipe python_recipe = self.ctx.python_recipe
return 'python{}'.format(python_recipe.version) return 'python{}'.format(python_recipe.version)
@ -757,17 +774,28 @@ class PythonRecipe(Recipe):
env['PYTHONNOUSERSITE'] = '1' env['PYTHONNOUSERSITE'] = '1'
# Set the LANG, this isn't usually important but is a better default
# as it occasionally matters how Python e.g. reads files
env['LANG'] = "en_GB.UTF-8"
if not self.call_hostpython_via_targetpython: if not self.call_hostpython_via_targetpython:
# sets python headers/linkages...depending on python's recipe # sets python headers/linkages...depending on python's recipe
python_name = self.ctx.python_recipe.name
python_version = self.ctx.python_recipe.version python_version = self.ctx.python_recipe.version
python_short_version = '.'.join(python_version.split('.')[:2]) python_short_version = '.'.join(python_version.split('.')[:2])
if 'python2' in self.ctx.recipe_build_order: if not self.ctx.python_recipe.from_crystax:
env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() env['CFLAGS'] += ' -I{}'.format(
env['CFLAGS'] += ' -I' + env[ self.ctx.python_recipe.include_root(arch.arch))
'PYTHON_ROOT'] + '/include/python2.7' env['LDFLAGS'] += ' -L{} -lpython{}'.format(
env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + \ self.ctx.python_recipe.link_root(arch.arch),
' -lpython2.7' self.ctx.python_recipe.major_minor_version_string)
elif self.ctx.python_recipe.from_crystax: if python_name == 'python3':
env['LDFLAGS'] += 'm'
elif python_name == 'python2legacy':
env['PYTHON_ROOT'] = join(
self.ctx.python_recipe.get_build_dir(
arch.arch), 'python-install')
else:
ndk_dir_python = join(self.ctx.ndk_dir, 'sources', ndk_dir_python = join(self.ctx.ndk_dir, 'sources',
'python', python_version) 'python', python_version)
env['CFLAGS'] += ' -I{} '.format( env['CFLAGS'] += ' -I{} '.format(
@ -776,22 +804,15 @@ class PythonRecipe(Recipe):
env['LDFLAGS'] += ' -L{}'.format( env['LDFLAGS'] += ' -L{}'.format(
join(ndk_dir_python, 'libs', arch.arch)) join(ndk_dir_python, 'libs', arch.arch))
env['LDFLAGS'] += ' -lpython{}m'.format(python_short_version) env['LDFLAGS'] += ' -lpython{}m'.format(python_short_version)
elif 'python3' in self.ctx.recipe_build_order:
# This headers are unused cause python3 recipe was removed
# TODO: should be reviewed when python3 recipe added
env['PYTHON_ROOT'] = self.ctx.get_python_install_dir()
env['CFLAGS'] += ' -I' + env[
'PYTHON_ROOT'] + '/include/python{}m'.format(
python_short_version)
env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + \
' -lpython{}m'.format(
python_short_version)
hppath = [] hppath = []
hppath.append(join(dirname(self.hostpython_location), 'Lib')) hppath.append(join(dirname(self.hostpython_location), 'Lib'))
hppath.append(join(hppath[0], 'site-packages')) hppath.append(join(hppath[0], 'site-packages'))
builddir = join(dirname(self.hostpython_location), 'build') builddir = join(dirname(self.hostpython_location), 'build')
if exists(builddir):
hppath += [join(builddir, d) for d in listdir(builddir) hppath += [join(builddir, d) for d in listdir(builddir)
if isdir(join(builddir, d))] if isdir(join(builddir, d))]
if len(hppath) > 0:
if 'PYTHONPATH' in env: if 'PYTHONPATH' in env:
env['PYTHONPATH'] = ':'.join(hppath + [env['PYTHONPATH']]) env['PYTHONPATH'] = ':'.join(hppath + [env['PYTHONPATH']])
else: else:
@ -826,7 +847,7 @@ class PythonRecipe(Recipe):
with current_directory(self.get_build_dir(arch.arch)): with current_directory(self.get_build_dir(arch.arch)):
hostpython = sh.Command(self.hostpython_location) hostpython = sh.Command(self.hostpython_location)
if self.ctx.python_recipe.from_crystax: if self.ctx.python_recipe.name != 'python2legacy':
hpenv = env.copy() hpenv = env.copy()
shprint(hostpython, 'setup.py', 'install', '-O2', shprint(hostpython, 'setup.py', 'install', '-O2',
'--root={}'.format(self.ctx.get_python_install_dir()), '--root={}'.format(self.ctx.get_python_install_dir()),
@ -835,13 +856,11 @@ class PythonRecipe(Recipe):
elif self.call_hostpython_via_targetpython: elif self.call_hostpython_via_targetpython:
shprint(hostpython, 'setup.py', 'install', '-O2', _env=env, shprint(hostpython, 'setup.py', 'install', '-O2', _env=env,
*self.setup_extra_args) *self.setup_extra_args)
else: else: # python2legacy
hppath = join(dirname(self.hostpython_location), 'Lib', hppath = join(dirname(self.hostpython_location), 'Lib', 'site-packages')
'site-packages')
hpenv = env.copy() hpenv = env.copy()
if 'PYTHONPATH' in hpenv: if 'PYTHONPATH' in hpenv:
hpenv['PYTHONPATH'] = ':'.join([hppath] + hpenv['PYTHONPATH'] = ':'.join([hppath] + hpenv['PYTHONPATH'].split(':'))
hpenv['PYTHONPATH'].split(':'))
else: else:
hpenv['PYTHONPATH'] = hppath hpenv['PYTHONPATH'] = hppath
shprint(hostpython, 'setup.py', 'install', '-O2', shprint(hostpython, 'setup.py', 'install', '-O2',
@ -920,12 +939,14 @@ class CppCompiledComponentsPythonRecipe(CompiledComponentsPythonRecipe):
arch_noeabi=arch.arch.replace('eabi', '') arch_noeabi=arch.arch.replace('eabi', '')
) )
env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions' env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions'
env['CFLAGS'] += " -I{ctx.ndk_dir}/platforms/android-{ctx.android_api}/arch-{arch_noeabi}/usr/include" \ env['CFLAGS'] += (
" -I{ctx.ndk_dir}/sources/cxx-stl/gnu-libstdc++/{ctx.toolchain_version}/include" \ " -I{ctx.ndk_dir}/platforms/android-{ctx.android_api}/arch-{arch_noeabi}/usr/include" +
" -I{ctx.ndk_dir}/sources/cxx-stl/gnu-libstdc++/{ctx.toolchain_version}/libs/{arch.arch}/include".format(**keys) " -I{ctx.ndk_dir}/sources/cxx-stl/gnu-libstdc++/{ctx.toolchain_version}/include" +
" -I{ctx.ndk_dir}/sources/cxx-stl/gnu-libstdc++/{ctx.toolchain_version}/libs/{arch.arch}/include").format(**keys)
env['CXXFLAGS'] = env['CFLAGS'] + ' -frtti -fexceptions' env['CXXFLAGS'] = env['CFLAGS'] + ' -frtti -fexceptions'
env['LDFLAGS'] += " -L{ctx.ndk_dir}/sources/cxx-stl/gnu-libstdc++/{ctx.toolchain_version}/libs/{arch.arch}" \ env['LDFLAGS'] += (
" -lgnustl_shared".format(**keys) " -L{ctx.ndk_dir}/sources/cxx-stl/gnu-libstdc++/{ctx.toolchain_version}/libs/{arch.arch}" +
" -lgnustl_shared").format(**keys)
return env return env
@ -949,7 +970,7 @@ class CythonRecipe(PythonRecipe):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(CythonRecipe, self).__init__(*args, **kwargs) super(CythonRecipe, self).__init__(*args, **kwargs)
depends = self.depends depends = self.depends
depends.append(('python2', 'python3crystax')) depends.append(('python2', 'python2legacy', 'python3', 'python3crystax'))
depends = list(set(depends)) depends = list(set(depends))
self.depends = depends self.depends = depends
@ -966,20 +987,10 @@ class CythonRecipe(PythonRecipe):
env = self.get_recipe_env(arch) env = self.get_recipe_env(arch)
if self.ctx.python_recipe.from_crystax:
command = sh.Command('python{}'.format(self.ctx.python_recipe.version))
site_packages_dirs = command(
'-c', 'import site; print("\\n".join(site.getsitepackages()))')
site_packages_dirs = site_packages_dirs.stdout.decode('utf-8').split('\n')
if 'PYTHONPATH' in env:
env['PYTHONPATH'] = env['PYTHONPATH'] + ':{}'.format(':'.join(site_packages_dirs))
else:
env['PYTHONPATH'] = ':'.join(site_packages_dirs)
with current_directory(self.get_build_dir(arch.arch)): with current_directory(self.get_build_dir(arch.arch)):
hostpython = sh.Command(self.ctx.hostpython) hostpython = sh.Command(self.ctx.hostpython)
shprint(hostpython, '-c', 'import sys; print(sys.path)', _env=env) shprint(hostpython, '-c', 'import sys; print(sys.path)', _env=env)
print('cwd is', realpath(curdir)) debug('cwd is {}'.format(realpath(curdir)))
info('Trying first build of {} to get cython files: this is ' info('Trying first build of {} to get cython files: this is '
'expected to fail'.format(self.name)) 'expected to fail'.format(self.name))
@ -1000,14 +1011,19 @@ class CythonRecipe(PythonRecipe):
info('First build appeared to complete correctly, skipping manual' info('First build appeared to complete correctly, skipping manual'
'cythonising.') 'cythonising.')
if 'python2' in self.ctx.recipe_build_order: self.strip_object_files(arch, env)
def strip_object_files(self, arch, env, build_dir=None):
if build_dir is None:
build_dir = self.get_build_dir(arch.arch)
with current_directory(build_dir):
info('Stripping object files')
if self.ctx.python_recipe.name == 'python2legacy':
info('Stripping object files') info('Stripping object files')
build_lib = glob.glob('./build/lib*') build_lib = glob.glob('./build/lib*')
shprint(sh.find, build_lib[0], '-name', '*.o', '-exec', shprint(sh.find, build_lib[0], '-name', '*.o', '-exec',
env['STRIP'], '{}', ';', _env=env) env['STRIP'], '{}', ';', _env=env)
else:
if 'python3crystax' in self.ctx.recipe_build_order:
info('Stripping object files')
shprint(sh.find, '.', '-iname', '*.so', '-exec', shprint(sh.find, '.', '-iname', '*.so', '-exec',
'/usr/bin/echo', '{}', ';', _env=env) '/usr/bin/echo', '{}', ';', _env=env)
shprint(sh.find, '.', '-iname', '*.so', '-exec', shprint(sh.find, '.', '-iname', '*.so', '-exec',
@ -1050,11 +1066,11 @@ class CythonRecipe(PythonRecipe):
if self.ctx.python_recipe.from_crystax: if self.ctx.python_recipe.from_crystax:
env['LDFLAGS'] = (env['LDFLAGS'] + env['LDFLAGS'] = (env['LDFLAGS'] +
' -L{}'.format(join(self.ctx.bootstrap.build_dir, 'libs', arch.arch))) ' -L{}'.format(join(self.ctx.bootstrap.build_dir, 'libs', arch.arch)))
# ' -L/home/asandy/.local/share/python-for-android/build/bootstrap_builds/sdl2/libs/armeabi '
if self.ctx.python_recipe.from_crystax: if self.ctx.python_recipe.name == 'python2legacy':
env['LDSHARED'] = env['CC'] + ' -shared'
else:
env['LDSHARED'] = join(self.ctx.root_dir, 'tools', 'liblink.sh') env['LDSHARED'] = join(self.ctx.root_dir, 'tools', 'liblink.sh')
else:
env['LDSHARED'] = env['CC'] + ' -shared'
# shprint(sh.whereis, env['LDSHARED'], _env=env) # shprint(sh.whereis, env['LDSHARED'], _env=env)
env['LIBLINK'] = 'NOTNONE' env['LIBLINK'] = 'NOTNONE'
env['NDKPLATFORM'] = self.ctx.ndk_platform env['NDKPLATFORM'] = self.ctx.ndk_platform
@ -1068,6 +1084,24 @@ class CythonRecipe(PythonRecipe):
env['LIBLINK_PATH'] = liblink_path env['LIBLINK_PATH'] = liblink_path
ensure_dir(liblink_path) ensure_dir(liblink_path)
# Add crystax-specific site packages:
if self.ctx.python_recipe.from_crystax:
command = sh.Command('python{}'.format(self.ctx.python_recipe.version))
site_packages_dirs = command(
'-c', 'import site; print("\\n".join(site.getsitepackages()))')
site_packages_dirs = site_packages_dirs.stdout.decode('utf-8').split('\n')
if 'PYTHONPATH' in env:
env['PYTHONPATH'] = env['PYTHONPATH'] +\
':{}'.format(':'.join(site_packages_dirs))
else:
env['PYTHONPATH'] = ':'.join(site_packages_dirs)
while env['PYTHONPATH'].find("::") > 0:
env['PYTHONPATH'] = env['PYTHONPATH'].replace("::", ":")
if env['PYTHONPATH'].endswith(":"):
env['PYTHONPATH'] = env['PYTHONPATH'][:-1]
if env['PYTHONPATH'].startswith(":"):
env['PYTHONPATH'] = env['PYTHONPATH'][1:]
return env return env
@ -1086,19 +1120,44 @@ class TargetPythonRecipe(Recipe):
def prebuild_arch(self, arch): def prebuild_arch(self, arch):
super(TargetPythonRecipe, self).prebuild_arch(arch) super(TargetPythonRecipe, self).prebuild_arch(arch)
if self.from_crystax and self.ctx.ndk != 'crystax': if self.from_crystax and self.ctx.ndk != 'crystax':
error('The {} recipe can only be built when ' raise BuildInterruptingException(
'The {} recipe can only be built when '
'using the CrystaX NDK. Exiting.'.format(self.name)) 'using the CrystaX NDK. Exiting.'.format(self.name))
exit(1)
self.ctx.python_recipe = self self.ctx.python_recipe = self
# @property def include_root(self, arch):
# def ctx(self): '''The root directory from which to include headers.'''
# return self._ctx raise NotImplementedError('Not implemented in TargetPythonRecipe')
# @ctx.setter def link_root(self):
# def ctx(self, ctx): raise NotImplementedError('Not implemented in TargetPythonRecipe')
# self._ctx = ctx
# ctx.python_recipe = self @property
def major_minor_version_string(self):
from distutils.version import LooseVersion
return '.'.join([str(v) for v in LooseVersion(self.version).version[:2]])
def create_python_bundle(self, dirn, arch):
"""
Create a packaged python bundle in the target directory, by
copying all the modules and standard library to the right
place.
"""
raise NotImplementedError('{} does not implement create_python_bundle'.format(self))
def reduce_object_file_names(self, dirn):
"""Recursively renames all files named XXX.cpython-...-linux-gnu.so"
to "XXX.so", i.e. removing the erroneous architecture name
coming from the local system.
"""
py_so_files = shprint(sh.find, dirn, '-iname', '*.so')
filens = py_so_files.stdout.decode('utf-8').split('\n')[:-1]
for filen in filens:
file_dirname, file_basename = split(filen)
parts = file_basename.split('.')
if len(parts) <= 2:
continue
shprint(sh.mv, filen, join(file_dirname, parts[0] + '.so'))
def md5sum(filen): def md5sum(filen):

View file

@ -0,0 +1,59 @@
from pythonforandroid.recipe import CompiledComponentsPythonRecipe
from os.path import join
class PillowRecipe(CompiledComponentsPythonRecipe):
version = '5.2.0'
url = 'https://github.com/python-pillow/Pillow/archive/{version}.tar.gz'
site_packages_name = 'Pillow'
depends = ['png', 'jpeg', 'freetype', 'setuptools']
patches = [join('patches', 'fix-docstring.patch'),
join('patches', 'fix-setup.patch')]
call_hostpython_via_targetpython = False
def get_recipe_env(self, arch=None, with_flags_in_cc=True):
env = super(PillowRecipe, self).get_recipe_env(arch, with_flags_in_cc)
env['ANDROID_ROOT'] = join(self.ctx.ndk_platform, 'usr')
ndk_lib_dir = join(self.ctx.ndk_platform, 'usr', 'lib')
ndk_include_dir = join(self.ctx.ndk_dir, 'sysroot', 'usr', 'include')
png = self.get_recipe('png', self.ctx)
png_lib_dir = png.get_lib_dir(arch)
png_jni_dir = png.get_jni_dir(arch)
jpeg = self.get_recipe('jpeg', self.ctx)
jpeg_inc_dir = jpeg_lib_dir = jpeg.get_build_dir(arch.arch)
freetype = self.get_recipe('freetype', self.ctx)
free_lib_dir = join(freetype.get_build_dir(arch.arch), 'objs', '.libs')
free_inc_dir = join(freetype.get_build_dir(arch.arch), 'include')
# harfbuzz is a direct dependency of freetype and we need the proper
# flags to successfully build the Pillow recipe, so we add them here.
harfbuzz = self.get_recipe('harfbuzz', self.ctx)
harf_lib_dir = join(harfbuzz.get_build_dir(arch.arch), 'src', '.libs')
harf_inc_dir = harfbuzz.get_build_dir(arch.arch)
env['JPEG_ROOT'] = '{}|{}'.format(jpeg_lib_dir, jpeg_inc_dir)
env['FREETYPE_ROOT'] = '{}|{}'.format(free_lib_dir, free_inc_dir)
env['ZLIB_ROOT'] = '{}|{}'.format(ndk_lib_dir, ndk_include_dir)
cflags = ' -I{}'.format(png_jni_dir)
cflags += ' -I{} -I{}'.format(harf_inc_dir, join(harf_inc_dir, 'src'))
cflags += ' -I{}'.format(free_inc_dir)
cflags += ' -I{}'.format(jpeg_inc_dir)
cflags += ' -I{}'.format(ndk_include_dir)
env['LIBS'] = ' -lpng -lfreetype -lharfbuzz -ljpeg -lturbojpeg'
env['LDFLAGS'] += ' -L{} -L{} -L{} -L{}'.format(
png_lib_dir, harf_lib_dir, jpeg_lib_dir, ndk_lib_dir)
if cflags not in env['CFLAGS']:
env['CFLAGS'] += cflags
return env
recipe = PillowRecipe()

View file

@ -0,0 +1,13 @@
diff --git a/src/PIL/__init__.py b/src/PIL/__init__.py
index a07280e..6b9fe99 100644
--- a/src/PIL/__init__.py
+++ b/src/PIL/__init__.py
@@ -24,7 +24,7 @@ PILLOW_VERSION = __version__ = _version.__version__
del _version
-__doc__ = __doc__.format(__version__) # include version in docstring
+__doc__ = ''
_plugins = ['BlpImagePlugin',

View file

@ -0,0 +1,148 @@
diff --git a/setup.py b/setup.py
index 761d552..4ddc598 100755
--- a/setup.py
+++ b/setup.py
@@ -136,12 +136,12 @@ except (ImportError, OSError):
NAME = 'Pillow'
PILLOW_VERSION = get_version()
-JPEG_ROOT = None
+JPEG_ROOT = tuple(os.environ['JPEG_ROOT'].split('|')) if 'JPEG_ROOT' in os.environ else None
JPEG2K_ROOT = None
-ZLIB_ROOT = None
+ZLIB_ROOT = tuple(os.environ['ZLIB_ROOT'].split('|')) if 'ZLIB_ROOT' in os.environ else None
IMAGEQUANT_ROOT = None
TIFF_ROOT = None
-FREETYPE_ROOT = None
+FREETYPE_ROOT = tuple(os.environ['FREETYPE_ROOT'].split('|')) if 'FREETYPE_ROOT' in os.environ else None
LCMS_ROOT = None
@@ -194,7 +194,7 @@ class pil_build_ext(build_ext):
]
def initialize_options(self):
- self.disable_platform_guessing = None
+ self.disable_platform_guessing = True
build_ext.initialize_options(self)
for x in self.feature:
setattr(self, 'disable_%s' % x, None)
@@ -466,61 +466,6 @@ class pil_build_ext(build_ext):
feature.jpeg = "libjpeg" # alternative name
feature.openjpeg_version = None
- if feature.want('jpeg2000'):
- _dbg('Looking for jpeg2000')
- best_version = None
- best_path = None
-
- # Find the best version
- for directory in self.compiler.include_dirs:
- _dbg('Checking for openjpeg-#.# in %s', directory)
- try:
- listdir = os.listdir(directory)
- except Exception:
- # WindowsError, FileNotFoundError
- continue
- for name in listdir:
- if name.startswith('openjpeg-') and \
- os.path.isfile(os.path.join(directory, name,
- 'openjpeg.h')):
- _dbg('Found openjpeg.h in %s/%s', (directory, name))
- version = tuple(int(x) for x in name[9:].split('.'))
- if best_version is None or version > best_version:
- best_version = version
- best_path = os.path.join(directory, name)
- _dbg('Best openjpeg version %s so far in %s',
- (best_version, best_path))
-
- if best_version and _find_library_file(self, 'openjp2'):
- # Add the directory to the include path so we can include
- # <openjpeg.h> rather than having to cope with the versioned
- # include path
- # FIXME (melvyn-sopacua):
- # At this point it's possible that best_path is already in
- # self.compiler.include_dirs. Should investigate how that is
- # possible.
- _add_directory(self.compiler.include_dirs, best_path, 0)
- feature.jpeg2000 = 'openjp2'
- feature.openjpeg_version = '.'.join(str(x) for x in best_version)
-
- if feature.want('imagequant'):
- _dbg('Looking for imagequant')
- if _find_include_file(self, 'libimagequant.h'):
- if _find_library_file(self, "imagequant"):
- feature.imagequant = "imagequant"
- elif _find_library_file(self, "libimagequant"):
- feature.imagequant = "libimagequant"
-
- if feature.want('tiff'):
- _dbg('Looking for tiff')
- if _find_include_file(self, 'tiff.h'):
- if _find_library_file(self, "tiff"):
- feature.tiff = "tiff"
- if sys.platform == "win32" and _find_library_file(self, "libtiff"):
- feature.tiff = "libtiff"
- if (sys.platform == "darwin" and
- _find_library_file(self, "libtiff")):
- feature.tiff = "libtiff"
if feature.want('freetype'):
_dbg('Looking for freetype')
@@ -546,36 +491,6 @@ class pil_build_ext(build_ext):
if subdir:
_add_directory(self.compiler.include_dirs, subdir, 0)
- if feature.want('lcms'):
- _dbg('Looking for lcms')
- if _find_include_file(self, "lcms2.h"):
- if _find_library_file(self, "lcms2"):
- feature.lcms = "lcms2"
- elif _find_library_file(self, "lcms2_static"):
- # alternate Windows name.
- feature.lcms = "lcms2_static"
-
- if feature.want('webp'):
- _dbg('Looking for webp')
- if (_find_include_file(self, "webp/encode.h") and
- _find_include_file(self, "webp/decode.h")):
- # In Google's precompiled zip it is call "libwebp":
- if _find_library_file(self, "webp"):
- feature.webp = "webp"
- elif _find_library_file(self, "libwebp"):
- feature.webp = "libwebp"
-
- if feature.want('webpmux'):
- _dbg('Looking for webpmux')
- if (_find_include_file(self, "webp/mux.h") and
- _find_include_file(self, "webp/demux.h")):
- if (_find_library_file(self, "webpmux") and
- _find_library_file(self, "webpdemux")):
- feature.webpmux = "webpmux"
- if (_find_library_file(self, "libwebpmux") and
- _find_library_file(self, "libwebpdemux")):
- feature.webpmux = "libwebpmux"
-
for f in feature:
if not getattr(feature, f) and feature.require(f):
if f in ('jpeg', 'zlib'):
@@ -612,8 +527,6 @@ class pil_build_ext(build_ext):
defs.append(("HAVE_LIBTIFF", None))
if sys.platform == "win32":
libs.extend(["kernel32", "user32", "gdi32"])
- if struct.unpack("h", "\0\1".encode('ascii'))[0] == 1:
- defs.append(("WORDS_BIGENDIAN", None))
if sys.platform == "win32" and not (PLATFORM_PYPY or PLATFORM_MINGW):
defs.append(("PILLOW_VERSION", '"\\"%s\\""' % PILLOW_VERSION))
@@ -658,10 +571,6 @@ class pil_build_ext(build_ext):
define_macros=defs))
tk_libs = ['psapi'] if sys.platform == 'win32' else []
- exts.append(Extension("PIL._imagingtk",
- ["src/_imagingtk.c", "src/Tk/tkImaging.c"],
- include_dirs=['src/Tk'],
- libraries=tk_libs))
exts.append(Extension("PIL._imagingmath", ["src/_imagingmath.c"]))
exts.append(Extension("PIL._imagingmorph", ["src/_imagingmorph.c"]))

View file

@ -1,3 +1,4 @@
from __future__ import unicode_literals
from pythonforandroid.recipe import CythonRecipe, IncludedFilesBehaviour from pythonforandroid.recipe import CythonRecipe, IncludedFilesBehaviour
from pythonforandroid.util import current_directory from pythonforandroid.util import current_directory
from pythonforandroid.patching import will_build from pythonforandroid.patching import will_build
@ -13,7 +14,8 @@ class AndroidRecipe(IncludedFilesBehaviour, CythonRecipe):
src_filename = 'src' src_filename = 'src'
depends = [('pygame', 'sdl2', 'genericndkbuild'), ('python2', 'python3crystax')] depends = [('pygame', 'sdl2', 'genericndkbuild'),
'pyjnius']
config_env = {} config_env = {}
@ -24,26 +26,35 @@ class AndroidRecipe(IncludedFilesBehaviour, CythonRecipe):
def prebuild_arch(self, arch): def prebuild_arch(self, arch):
super(AndroidRecipe, self).prebuild_arch(arch) super(AndroidRecipe, self).prebuild_arch(arch)
ctx_bootstrap = self.ctx.bootstrap.name
# define macros for Cython, C, Python
tpxi = 'DEF {} = {}\n' tpxi = 'DEF {} = {}\n'
th = '#define {} {}\n' th = '#define {} {}\n'
tpy = '{} = {}\n' tpy = '{} = {}\n'
bootstrap = bootstrap_name = self.ctx.bootstrap.name # make sure bootstrap name is in unicode
is_sdl2 = bootstrap_name in ('sdl2', 'sdl2python3') if isinstance(ctx_bootstrap, bytes):
ctx_bootstrap = ctx_bootstrap.decode('utf-8')
bootstrap = bootstrap_name = ctx_bootstrap
is_sdl2 = bootstrap_name in ('sdl2', 'sdl2python3', 'sdl2_gradle')
is_pygame = bootstrap_name in ('pygame',) is_pygame = bootstrap_name in ('pygame',)
is_webview = bootstrap_name in ('webview',) is_webview = bootstrap_name in ('webview',)
if is_sdl2 or is_webview: if is_sdl2 or is_webview:
if is_sdl2: if is_sdl2:
bootstrap = 'sdl2' bootstrap = 'sdl2'
java_ns = 'org.kivy.android' java_ns = u'org.kivy.android'
jni_ns = 'org/kivy/android' jni_ns = u'org/kivy/android'
elif is_pygame: elif is_pygame:
java_ns = 'org.renpy.android' java_ns = u'org.renpy.android'
jni_ns = 'org/renpy/android' jni_ns = u'org/renpy/android'
else: else:
logger.error('unsupported bootstrap for android recipe: {}'.format(bootstrap_name)) logger.error((
'unsupported bootstrap for android recipe: {}'
''.format(bootstrap_name)
))
exit(1) exit(1)
config = { config = {
@ -55,20 +66,28 @@ class AndroidRecipe(IncludedFilesBehaviour, CythonRecipe):
'JNI_NAMESPACE': jni_ns, 'JNI_NAMESPACE': jni_ns,
} }
with current_directory(self.get_build_dir(arch.arch)): # create config files for Cython, C and Python
with open(join('android', 'config.pxi'), 'w') as fpxi: with (
with open(join('android', 'config.h'), 'w') as fh: current_directory(self.get_build_dir(arch.arch))), (
with open(join('android', 'config.py'), 'w') as fpy: open(join('android', 'config.pxi'), 'w')) as fpxi, (
open(join('android', 'config.h'), 'w')) as fh, (
open(join('android', 'config.py'), 'w')) as fpy:
for key, value in config.items(): for key, value in config.items():
fpxi.write(tpxi.format(key, repr(value))) fpxi.write(tpxi.format(key, repr(value)))
fpy.write(tpy.format(key, repr(value))) fpy.write(tpy.format(key, repr(value)))
fh.write(th.format(key, value if isinstance(value, int)
else '"{}"'.format(value))) fh.write(th.format(
key,
value if isinstance(value, int) else '"{}"'.format(value)
))
self.config_env[key] = str(value) self.config_env[key] = str(value)
if is_sdl2: if is_sdl2:
fh.write('JNIEnv *SDL_AndroidGetJNIEnv(void);\n') fh.write('JNIEnv *SDL_AndroidGetJNIEnv(void);\n')
fh.write('#define SDL_ANDROID_GetJNIEnv SDL_AndroidGetJNIEnv\n') fh.write(
'#define SDL_ANDROID_GetJNIEnv SDL_AndroidGetJNIEnv\n'
)
elif is_pygame: elif is_pygame:
fh.write('JNIEnv *SDL_ANDROID_GetJNIEnv(void);\n') fh.write('JNIEnv *SDL_ANDROID_GetJNIEnv(void);\n')

View file

@ -5,4 +5,4 @@ Android module
''' '''
# legacy import # legacy import
from android._android import * from android._android import * # noqa: F401, F403

View file

@ -175,13 +175,13 @@ api_version = autoclass('android.os.Build$VERSION').SDK_INT
version_codes = autoclass('android.os.Build$VERSION_CODES') version_codes = autoclass('android.os.Build$VERSION_CODES')
python_act = autoclass(JAVA_NAMESPACE + '.PythonActivity') python_act = autoclass(JAVA_NAMESPACE + u'.PythonActivity')
Rect = autoclass('android.graphics.Rect') Rect = autoclass(u'android.graphics.Rect')
mActivity = python_act.mActivity mActivity = python_act.mActivity
if mActivity: if mActivity:
# PyGame backend already has the listener so adding # PyGame backend already has the listener so adding
# one here leads to a crash/too much cpu usage. # one here leads to a crash/too much cpu usage.
# SDL2 now does noe need the listener so there is # SDL2 now does not need the listener so there is
# no point adding a processor intensive layout listenere here. # no point adding a processor intensive layout listenere here.
height = 0 height = 0
def get_keyboard_height(): def get_keyboard_height():
@ -332,7 +332,7 @@ class AndroidBrowser(object):
return open_url(url) return open_url(url)
import webbrowser import webbrowser
webbrowser.register('android', AndroidBrowser, None, -1) webbrowser.register('android', AndroidBrowser)
cdef extern void android_start_service(char *, char *, char *) cdef extern void android_start_service(char *, char *, char *)
def start_service(title=None, description=None, arg=None): def start_service(title=None, description=None, arg=None):

View file

@ -1,11 +1,13 @@
from jnius import PythonJavaClass, java_method, autoclass, cast from jnius import PythonJavaClass, autoclass, java_method
from android.config import JAVA_NAMESPACE, JNI_NAMESPACE from android.config import JAVA_NAMESPACE, JNI_NAMESPACE
_activity = autoclass(JAVA_NAMESPACE + '.PythonActivity').mActivity _activity = autoclass(JAVA_NAMESPACE + '.PythonActivity').mActivity
_callbacks = { _callbacks = {
'on_new_intent': [], 'on_new_intent': [],
'on_activity_result': [] } 'on_activity_result': [],
}
class NewIntentListener(PythonJavaClass): class NewIntentListener(PythonJavaClass):
__javainterfaces__ = [JNI_NAMESPACE + '/PythonActivity$NewIntentListener'] __javainterfaces__ = [JNI_NAMESPACE + '/PythonActivity$NewIntentListener']
@ -46,6 +48,7 @@ def bind(**kwargs):
_activity.registerActivityResultListener(listener) _activity.registerActivityResultListener(listener)
_callbacks[event].append(listener) _callbacks[event].append(listener)
def unbind(**kwargs): def unbind(**kwargs):
for event, callback in kwargs.items(): for event, callback in kwargs.items():
if event not in _callbacks: if event not in _callbacks:
@ -58,4 +61,3 @@ def unbind(**kwargs):
_activity.unregisterNewIntentListener(listener) _activity.unregisterNewIntentListener(listener)
elif event == 'on_activity_result': elif event == 'on_activity_result':
_activity.unregisterActivityResultListener(listener) _activity.unregisterActivityResultListener(listener)

View file

@ -3,5 +3,3 @@ Android Billing API
=================== ===================
''' '''
from android._android_billing import *

View file

@ -61,8 +61,8 @@ class BroadcastReceiver(object):
Handler = autoclass('android.os.Handler') Handler = autoclass('android.os.Handler')
self.handlerthread.start() self.handlerthread.start()
self.handler = Handler(self.handlerthread.getLooper()) self.handler = Handler(self.handlerthread.getLooper())
self.context.registerReceiver(self.receiver, self.receiver_filter, None, self.context.registerReceiver(
self.handler) self.receiver, self.receiver_filter, None, self.handler)
def stop(self): def stop(self):
self.context.unregisterReceiver(self.receiver) self.context.unregisterReceiver(self.receiver)
@ -76,4 +76,3 @@ class BroadcastReceiver(object):
return PythonService.mService return PythonService.mService
PythonActivity = autoclass(JAVA_NAMESPACE + '.PythonActivity') PythonActivity = autoclass(JAVA_NAMESPACE + '.PythonActivity')
return PythonActivity.mActivity return PythonActivity.mActivity

View file

@ -0,0 +1,7 @@
from jnius import autoclass
def hide_loading_screen():
python_activity = autoclass('org.kivy.android.PythonActivity')
python_activity.removeLoadingScreen()

View file

@ -8,36 +8,45 @@ import os
condition = threading.Condition() condition = threading.Condition()
def periodic(): def periodic():
for i in range(0, num_channels): for i in range(0, num_channels):
if i in channels: if i in channels:
channels[i].periodic() channels[i].periodic()
num_channels = 8 num_channels = 8
reserved_channels = 0 reserved_channels = 0
def init(frequency=22050, size=-16, channels=2, buffer=4096): def init(frequency=22050, size=-16, channels=2, buffer=4096):
return None return None
def pre_init(frequency=22050, size=-16, channels=2, buffersize=4096): def pre_init(frequency=22050, size=-16, channels=2, buffersize=4096):
return None return None
def quit(): def quit():
stop() stop()
return None return None
def stop(): def stop():
for i in range(0, num_channels): for i in range(0, num_channels):
sound.stop(i) sound.stop(i)
def pause(): def pause():
for i in range(0, num_channels): for i in range(0, num_channels):
sound.pause(i) sound.pause(i)
def unpause(): def unpause():
for i in range(0, num_channels): for i in range(0, num_channels):
sound.unpause(i) sound.unpause(i)
def get_busy(): def get_busy():
for i in range(0, num_channels): for i in range(0, num_channels):
if sound.busy(i): if sound.busy(i):
@ -45,6 +54,7 @@ def get_busy():
return False return False
def fadeout(time): def fadeout(time):
# Fadeout doesn't work - it just immediately stops playback. # Fadeout doesn't work - it just immediately stops playback.
stop() stop()
@ -53,17 +63,21 @@ def fadeout(time):
# A map from channel number to Channel object. # A map from channel number to Channel object.
channels = {} channels = {}
def set_num_channels(count): def set_num_channels(count):
global num_channels global num_channels
num_channels = count num_channels = count
def get_num_channels(count): def get_num_channels(count):
return num_channels return num_channels
def set_reserved(count): def set_reserved(count):
global reserved_channels global reserved_channels
reserved_channels = count reserved_channels = count
def find_channel(force=False): def find_channel(force=False):
busy = [] busy = []
@ -83,6 +97,7 @@ def find_channel(force=False):
return busy[0] return busy[0]
class ChannelImpl(object): class ChannelImpl(object):
def __init__(self, id): def __init__(self, id):
@ -101,7 +116,6 @@ class ChannelImpl(object):
if self.loop is not None and sound.queue_depth(self.id) < 2: if self.loop is not None and sound.queue_depth(self.id) < 2:
self.queue(self.loop, loops=1) self.queue(self.loop, loops=1)
def play(self, s, loops=0, maxtime=0, fade_ms=0): def play(self, s, loops=0, maxtime=0, fade_ms=0):
if loops: if loops:
self.loop = s self.loop = s
@ -183,6 +197,7 @@ def Channel(n):
sound_serial = 0 sound_serial = 0
sounds = {} sounds = {}
class Sound(object): class Sound(object):
def __init__(self, what): def __init__(self, what):
@ -196,10 +211,10 @@ class Sound(object):
self.serial = str(sound_serial) self.serial = str(sound_serial)
sound_serial += 1 sound_serial += 1
if isinstance(what, file): if isinstance(what, file): # noqa F821
self.file = what self.file = what
else: else:
self.file = file(os.path.abspath(what), "rb") self.file = file(os.path.abspath(what), "rb") # noqa F821
sounds[self.serial] = self sounds[self.serial] = self
@ -214,7 +229,6 @@ class Sound(object):
channel.play(self, loops=loops) channel.play(self, loops=loops)
return channel return channel
def stop(self): def stop(self):
for i in range(0, num_channels): for i in range(0, num_channels):
if Channel(i).get_sound() is self: if Channel(i).get_sound() is self:
@ -244,9 +258,11 @@ class Sound(object):
def get_length(self): def get_length(self):
return 1.0 return 1.0
music_channel = Channel(256) music_channel = Channel(256)
music_sound = None music_sound = None
class music(object): class music(object):
@staticmethod @staticmethod
@ -306,6 +322,3 @@ class music(object):
@staticmethod @staticmethod
def queue(filename): def queue(filename):
return music_channel.queue(Sound(filename)) return music_channel.queue(Sound(filename))

View file

@ -0,0 +1,438 @@
try:
from jnius import autoclass
except ImportError:
# To allow importing by build/manifest-creating code without
# pyjnius being present:
def autoclass(item):
raise RuntimeError("pyjnius not available")
class Permission:
ACCEPT_HANDOVER = "android.permission.ACCEPT_HANDOVER"
ACCESS_COARSE_LOCATION = "android.permission.ACCESS_COARSE_LOCATION"
ACCESS_LOCATION_EXTRA_COMMANDS = (
"android.permission.ACCESS_LOCATION_EXTRA_COMMANDS"
)
ACCESS_NETWORK_STATE = "android.permission.ACCESS_NETWORK_STATE"
ACCESS_NOTIFICATION_POLICY = (
"android.permission.ACCESS_NOTIFICATION_POLICY"
)
ACCESS_WIFI_STATE = "android.permission.ACCESS_WIFI_STATE"
ADD_VOICEMAIL = "com.android.voicemail.permission.ADD_VOICEMAIL"
ANSWER_PHONE_CALLS = "android.permission.ANSWER_PHONE_CALLS"
BATTERY_STATS = "android.permission.BATTERY_STATS"
BIND_ACCESSIBILITY_SERVICE = (
"android.permission.BIND_ACCESSIBILITY_SERVICE"
)
BIND_AUTOFILL_SERVICE = "android.permission.BIND_AUTOFILL_SERVICE"
BIND_CARRIER_MESSAGING_SERVICE = ( # note: deprecated in api 23+
"android.permission.BIND_CARRIER_MESSAGING_SERVICE"
)
BIND_CARRIER_SERVICES = ( # replaces BIND_CARRIER_MESSAGING_SERVICE
"android.permission.BIND_CARRIER_SERVICES"
)
BIND_CHOOSER_TARGET_SERVICE = (
"android.permission.BIND_CHOOSER_TARGET_SERVICE"
)
BIND_CONDITION_PROVIDER_SERVICE = (
"android.permission.BIND_CONDITION_PROVIDER_SERVICE"
)
BIND_DEVICE_ADMIN = "android.permission.BIND_DEVICE_ADMIN"
BIND_DREAM_SERVICE = "android.permission.BIND_DREAM_SERVICE"
BIND_INCALL_SERVICE = "android.permission.BIND_INCALL_SERVICE"
BIND_INPUT_METHOD = (
"android.permission.BIND_INPUT_METHOD"
)
BIND_MIDI_DEVICE_SERVICE = (
"android.permission.BIND_MIDI_DEVICE_SERVICE"
)
BIND_NFC_SERVICE = (
"android.permission.BIND_NFC_SERVICE"
)
BIND_NOTIFICATION_LISTENER_SERVICE = (
"android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
)
BIND_PRINT_SERVICE = (
"android.permission.BIND_PRINT_SERVICE"
)
BIND_QUICK_SETTINGS_TILE = (
"android.permission.BIND_QUICK_SETTINGS_TILE"
)
BIND_REMOTEVIEWS = (
"android.permission.BIND_REMOTEVIEWS"
)
BIND_SCREENING_SERVICE = (
"android.permission.BIND_SCREENING_SERVICE"
)
BIND_TELECOM_CONNECTION_SERVICE = (
"android.permission.BIND_TELECOM_CONNECTION_SERVICE"
)
BIND_TEXT_SERVICE = (
"android.permission.BIND_TEXT_SERVICE"
)
BIND_TV_INPUT = (
"android.permission.BIND_TV_INPUT"
)
BIND_VISUAL_VOICEMAIL_SERVICE = (
"android.permission.BIND_VISUAL_VOICEMAIL_SERVICE"
)
BIND_VOICE_INTERACTION = (
"android.permission.BIND_VOICE_INTERACTION"
)
BIND_VPN_SERVICE = (
"android.permission.BIND_VPN_SERVICE"
)
BIND_VR_LISTENER_SERVICE = (
"android.permission.BIND_VR_LISTENER_SERVICE"
)
BIND_WALLPAPER = (
"android.permission.BIND_WALLPAPER"
)
BLUETOOTH = (
"android.permission.BLUETOOTH"
)
BLUETOOTH_ADMIN = (
"android.permission.BLUETOOTH_ADMIN"
)
BODY_SENSORS = (
"android.permission.BODY_SENSORS"
)
BROADCAST_PACKAGE_REMOVED = (
"android.permission.BROADCAST_PACKAGE_REMOVED"
)
BROADCAST_STICKY = (
"android.permission.BROADCAST_STICKY"
)
CALL_PHONE = (
"android.permission.CALL_PHONE"
)
CALL_PRIVILEGED = (
"android.permission.CALL_PRIVILEGED"
)
CAMERA = (
"android.permission.CAMERA"
)
CAPTURE_AUDIO_OUTPUT = (
"android.permission.CAPTURE_AUDIO_OUTPUT"
)
CAPTURE_SECURE_VIDEO_OUTPUT = (
"android.permission.CAPTURE_SECURE_VIDEO_OUTPUT"
)
CAPTURE_VIDEO_OUTPUT = (
"android.permission.CAPTURE_VIDEO_OUTPUT"
)
CHANGE_COMPONENT_ENABLED_STATE = (
"android.permission.CHANGE_COMPONENT_ENABLED_STATE"
)
CHANGE_CONFIGURATION = (
"android.permission.CHANGE_CONFIGURATION"
)
CHANGE_NETWORK_STATE = (
"android.permission.CHANGE_NETWORK_STATE"
)
CHANGE_WIFI_MULTICAST_STATE = (
"android.permission.CHANGE_WIFI_MULTICAST_STATE"
)
CHANGE_WIFI_STATE = (
"android.permission.CHANGE_WIFI_STATE"
)
CLEAR_APP_CACHE = (
"android.permission.CLEAR_APP_CACHE"
)
CONTROL_LOCATION_UPDATES = (
"android.permission.CONTROL_LOCATION_UPDATES"
)
DELETE_CACHE_FILES = (
"android.permission.DELETE_CACHE_FILES"
)
DELETE_PACKAGES = (
"android.permission.DELETE_PACKAGES"
)
DIAGNOSTIC = (
"android.permission.DIAGNOSTIC"
)
DISABLE_KEYGUARD = (
"android.permission.DISABLE_KEYGUARD"
)
DUMP = (
"android.permission.DUMP"
)
EXPAND_STATUS_BAR = (
"android.permission.EXPAND_STATUS_BAR"
)
FACTORY_TEST = (
"android.permission.FACTORY_TEST"
)
FOREGROUND_SERVICE = (
"android.permission.FOREGROUND_SERVICE"
)
GET_ACCOUNTS = (
"android.permission.GET_ACCOUNTS"
)
GET_ACCOUNTS_PRIVILEGED = (
"android.permission.GET_ACCOUNTS_PRIVILEGED"
)
GET_PACKAGE_SIZE = (
"android.permission.GET_PACKAGE_SIZE"
)
GET_TASKS = (
"android.permission.GET_TASKS"
)
GLOBAL_SEARCH = (
"android.permission.GLOBAL_SEARCH"
)
INSTALL_LOCATION_PROVIDER = (
"android.permission.INSTALL_LOCATION_PROVIDER"
)
INSTALL_PACKAGES = (
"android.permission.INSTALL_PACKAGES"
)
INSTALL_SHORTCUT = (
"com.android.launcher.permission.INSTALL_SHORTCUT"
)
INSTANT_APP_FOREGROUND_SERVICE = (
"android.permission.INSTANT_APP_FOREGROUND_SERVICE"
)
INTERNET = (
"android.permission.INTERNET"
)
KILL_BACKGROUND_PROCESSES = (
"android.permission.KILL_BACKGROUND_PROCESSES"
)
LOCATION_HARDWARE = (
"android.permission.LOCATION_HARDWARE"
)
MANAGE_DOCUMENTS = (
"android.permission.MANAGE_DOCUMENTS"
)
MANAGE_OWN_CALLS = (
"android.permission.MANAGE_OWN_CALLS"
)
MASTER_CLEAR = (
"android.permission.MASTER_CLEAR"
)
MEDIA_CONTENT_CONTROL = (
"android.permission.MEDIA_CONTENT_CONTROL"
)
MODIFY_AUDIO_SETTINGS = (
"android.permission.MODIFY_AUDIO_SETTINGS"
)
MODIFY_PHONE_STATE = (
"android.permission.MODIFY_PHONE_STATE"
)
MOUNT_FORMAT_FILESYSTEMS = (
"android.permission.MOUNT_FORMAT_FILESYSTEMS"
)
MOUNT_UNMOUNT_FILESYSTEMS = (
"android.permission.MOUNT_UNMOUNT_FILESYSTEMS"
)
NFC = (
"android.permission.NFC"
)
NFC_TRANSACTION_EVENT = (
"android.permission.NFC_TRANSACTION_EVENT"
)
PACKAGE_USAGE_STATS = (
"android.permission.PACKAGE_USAGE_STATS"
)
PERSISTENT_ACTIVITY = (
"android.permission.PERSISTENT_ACTIVITY"
)
PROCESS_OUTGOING_CALLS = (
"android.permission.PROCESS_OUTGOING_CALLS"
)
READ_CALENDAR = (
"android.permission.READ_CALENDAR"
)
READ_CALL_LOG = (
"android.permission.READ_CALL_LOG"
)
READ_CONTACTS = (
"android.permission.READ_CONTACTS"
)
READ_EXTERNAL_STORAGE = (
"android.permission.READ_EXTERNAL_STORAGE"
)
READ_FRAME_BUFFER = (
"android.permission.READ_FRAME_BUFFER"
)
READ_INPUT_STATE = (
"android.permission.READ_INPUT_STATE"
)
READ_LOGS = (
"android.permission.READ_LOGS"
)
READ_PHONE_NUMBERS = (
"android.permission.READ_PHONE_NUMBERS"
)
READ_PHONE_STATE = (
"android.permission.READ_PHONE_STATE"
)
READ_SMS = (
"android.permission.READ_SMS"
)
READ_SYNC_SETTINGS = (
"android.permission.READ_SYNC_SETTINGS"
)
READ_SYNC_STATS = (
"android.permission.READ_SYNC_STATS"
)
READ_VOICEMAIL = (
"com.android.voicemail.permission.READ_VOICEMAIL"
)
REBOOT = (
"android.permission.REBOOT"
)
RECEIVE_BOOT_COMPLETED = (
"android.permission.RECEIVE_BOOT_COMPLETED"
)
RECEIVE_MMS = (
"android.permission.RECEIVE_MMS"
)
RECEIVE_SMS = (
"android.permission.RECEIVE_SMS"
)
RECEIVE_WAP_PUSH = (
"android.permission.RECEIVE_WAP_PUSH"
)
RECORD_AUDIO = (
"android.permission.RECORD_AUDIO"
)
REORDER_TASKS = (
"android.permission.REORDER_TASKS"
)
REQUEST_COMPANION_RUN_IN_BACKGROUND = (
"android.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND"
)
REQUEST_COMPANION_USE_DATA_IN_BACKGROUND = (
"android.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND"
)
REQUEST_DELETE_PACKAGES = (
"android.permission.REQUEST_DELETE_PACKAGES"
)
REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = (
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"
)
REQUEST_INSTALL_PACKAGES = (
"android.permission.REQUEST_INSTALL_PACKAGES"
)
RESTART_PACKAGES = (
"android.permission.RESTART_PACKAGES"
)
SEND_RESPOND_VIA_MESSAGE = (
"android.permission.SEND_RESPOND_VIA_MESSAGE"
)
SEND_SMS = (
"android.permission.SEND_SMS"
)
SET_ALARM = (
"com.android.alarm.permission.SET_ALARM"
)
SET_ALWAYS_FINISH = (
"android.permission.SET_ALWAYS_FINISH"
)
SET_ANIMATION_SCALE = (
"android.permission.SET_ANIMATION_SCALE"
)
SET_DEBUG_APP = (
"android.permission.SET_DEBUG_APP"
)
SET_PREFERRED_APPLICATIONS = (
"android.permission.SET_PREFERRED_APPLICATIONS"
)
SET_PROCESS_LIMIT = (
"android.permission.SET_PROCESS_LIMIT"
)
SET_TIME = (
"android.permission.SET_TIME"
)
SET_TIME_ZONE = (
"android.permission.SET_TIME_ZONE"
)
SET_WALLPAPER = (
"android.permission.SET_WALLPAPER"
)
SET_WALLPAPER_HINTS = (
"android.permission.SET_WALLPAPER_HINTS"
)
SIGNAL_PERSISTENT_PROCESSES = (
"android.permission.SIGNAL_PERSISTENT_PROCESSES"
)
STATUS_BAR = (
"android.permission.STATUS_BAR"
)
SYSTEM_ALERT_WINDOW = (
"android.permission.SYSTEM_ALERT_WINDOW"
)
TRANSMIT_IR = (
"android.permission.TRANSMIT_IR"
)
UNINSTALL_SHORTCUT = (
"com.android.launcher.permission.UNINSTALL_SHORTCUT"
)
UPDATE_DEVICE_STATS = (
"android.permission.UPDATE_DEVICE_STATS"
)
USE_BIOMETRIC = (
"android.permission.USE_BIOMETRIC"
)
USE_FINGERPRINT = (
"android.permission.USE_FINGERPRINT"
)
USE_SIP = (
"android.permission.USE_SIP"
)
VIBRATE = (
"android.permission.VIBRATE"
)
WAKE_LOCK = (
"android.permission.WAKE_LOCK"
)
WRITE_APN_SETTINGS = (
"android.permission.WRITE_APN_SETTINGS"
)
WRITE_CALENDAR = (
"android.permission.WRITE_CALENDAR"
)
WRITE_CALL_LOG = (
"android.permission.WRITE_CALL_LOG"
)
WRITE_CONTACTS = (
"android.permission.WRITE_CONTACTS"
)
WRITE_EXTERNAL_STORAGE = (
"android.permission.WRITE_EXTERNAL_STORAGE"
)
WRITE_GSERVICES = (
"android.permission.WRITE_GSERVICES"
)
WRITE_SECURE_SETTINGS = (
"android.permission.WRITE_SECURE_SETTINGS"
)
WRITE_SETTINGS = (
"android.permission.WRITE_SETTINGS"
)
WRITE_SYNC_SETTINGS = (
"android.permission.WRITE_SYNC_SETTINGS"
)
WRITE_VOICEMAIL = (
"com.android.voicemail.permission.WRITE_VOICEMAIL"
)
def request_permissions(permissions):
python_activity = autoclass('org.kivy.android.PythonActivity')
python_activity.requestPermissions(permissions)
def request_permission(permission):
request_permissions([permission])
def check_permission(permission):
python_activity = autoclass('org.kivy.android.PythonActivity')
result = bool(python_activity.checkCurrentPermission(
permission + ""
))
return result

View file

@ -33,12 +33,13 @@ class Runnable(PythonJavaClass):
def run(self): def run(self):
try: try:
self.func(*self.args, **self.kwargs) self.func(*self.args, **self.kwargs)
except: except: # noqa E722
import traceback import traceback
traceback.print_exc() traceback.print_exc()
Runnable.__runnables__.remove(self) Runnable.__runnables__.remove(self)
def run_on_ui_thread(f): def run_on_ui_thread(f):
'''Decorator to create automatically a :class:`Runnable` object with the '''Decorator to create automatically a :class:`Runnable` object with the
function. The function will be delayed and call into the Activity thread. function. The function will be delayed and call into the Activity thread.

View file

@ -6,7 +6,7 @@ lib_dict = {
'pygame': ['sdl'], 'pygame': ['sdl'],
'sdl2': ['SDL2', 'SDL2_image', 'SDL2_mixer', 'SDL2_ttf'] 'sdl2': ['SDL2', 'SDL2_image', 'SDL2_mixer', 'SDL2_ttf']
} }
sdl_libs = lib_dict[os.environ['BOOTSTRAP']] if os.environ['BOOTSTRAP'] == 'sdl2' else [] sdl_libs = lib_dict.get(os.environ['BOOTSTRAP'], [])
renpy_sound = Extension('android._android_sound', renpy_sound = Extension('android._android_sound',
['android/_android_sound.c', 'android/_android_sound_jni.c', ], ['android/_android_sound.c', 'android/_android_sound_jni.c', ],

View file

@ -1,11 +1,12 @@
from pythonforandroid.toolchain import PythonRecipe, shprint, shutil, current_directory from pythonforandroid.recipe import PythonRecipe
from os.path import join, exists from pythonforandroid.toolchain import current_directory, shprint
import sh import sh
class ApswRecipe(PythonRecipe): class ApswRecipe(PythonRecipe):
version = '3.15.0-r1' version = '3.15.0-r1'
url = 'https://github.com/rogerbinns/apsw/archive/{version}.tar.gz' url = 'https://github.com/rogerbinns/apsw/archive/{version}.tar.gz'
depends = ['sqlite3', 'hostpython2', 'python2', 'setuptools'] depends = ['sqlite3', ('python2', 'python3'), 'setuptools']
call_hostpython_via_targetpython = False call_hostpython_via_targetpython = False
site_packages_name = 'apsw' site_packages_name = 'apsw'
@ -17,21 +18,17 @@ class ApswRecipe(PythonRecipe):
shprint(hostpython, shprint(hostpython,
'setup.py', 'setup.py',
'build_ext', 'build_ext',
'--enable=fts4' '--enable=fts4', _env=env)
, _env=env)
# Install python bindings # Install python bindings
super(ApswRecipe, self).build_arch(arch) super(ApswRecipe, self).build_arch(arch)
def get_recipe_env(self, arch): def get_recipe_env(self, arch):
env = super(ApswRecipe, self).get_recipe_env(arch) env = super(ApswRecipe, self).get_recipe_env(arch)
env['PYTHON_ROOT'] = self.ctx.get_python_install_dir() sqlite_recipe = self.get_recipe('sqlite3', self.ctx)
env['CFLAGS'] += ' -I' + env['PYTHON_ROOT'] + '/include/python2.7' + \ env['CFLAGS'] += ' -I' + sqlite_recipe.get_build_dir(arch.arch)
' -I' + self.get_recipe('sqlite3', self.ctx).get_build_dir(arch.arch) env['LDFLAGS'] += ' -L' + sqlite_recipe.get_lib_dir(arch)
# Set linker to use the correct gcc env['LIBS'] = env.get('LIBS', '') + ' -lsqlite3'
env['LDSHARED'] = env['CC'] + ' -pthread -shared -Wl,-O1 -Wl,-Bsymbolic-functions'
env['LDFLAGS'] += ' -L' + env['PYTHON_ROOT'] + '/lib' + \
' -lpython2.7' + \
' -lsqlite3'
return env return env
recipe = ApswRecipe() recipe = ApswRecipe()

View file

@ -1,9 +1,11 @@
from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe from pythonforandroid.recipe import CppCompiledComponentsPythonRecipe
class AtomRecipe(CppCompiledComponentsPythonRecipe): class AtomRecipe(CppCompiledComponentsPythonRecipe):
site_packages_name = 'atom' site_packages_name = 'atom'
version = '0.3.10' version = '0.3.10'
url = 'https://github.com/nucleic/atom/archive/master.zip' url = 'https://github.com/nucleic/atom/archive/master.zip'
depends = ['python2','setuptools'] depends = ['setuptools']
recipe = AtomRecipe() recipe = AtomRecipe()

View file

@ -1,36 +1,32 @@
from pythonforandroid.toolchain import CythonRecipe, shprint, current_directory, info from pythonforandroid.recipe import CythonRecipe
import sh from os.path import join
import glob
from os.path import join, exists
class AudiostreamRecipe(CythonRecipe): class AudiostreamRecipe(CythonRecipe):
version = 'master' version = 'master'
url = 'https://github.com/kivy/audiostream/archive/{version}.zip' url = 'https://github.com/kivy/audiostream/archive/{version}.zip'
name = 'audiostream' name = 'audiostream'
depends = ['python2', ('sdl', 'sdl2'), 'pyjnius'] depends = [('python2', 'python3'), ('sdl', 'sdl2'), 'pyjnius']
def get_recipe_env(self, arch): def get_recipe_env(self, arch):
env = super(AudiostreamRecipe, self).get_recipe_env(arch)
if 'sdl' in self.ctx.recipe_build_order: if 'sdl' in self.ctx.recipe_build_order:
sdl_include = 'sdl' sdl_include = 'sdl'
sdl_mixer_include = 'sdl_mixer' sdl_mixer_include = 'sdl_mixer'
elif 'sdl2' in self.ctx.recipe_build_order: elif 'sdl2' in self.ctx.recipe_build_order:
sdl_include = 'SDL' sdl_include = 'SDL2'
sdl_mixer_include = 'SDL2_mixer' sdl_mixer_include = 'SDL2_mixer'
env['USE_SDL2'] = 'True'
env['SDL2_INCLUDE_DIR'] = join(self.ctx.bootstrap.build_dir, 'jni', 'SDL', 'include')
#note: audiostream library is not yet able to judge whether it is being used with sdl or with sdl2.
#this causes linking to fail against SDL2 (compiling against SDL2 works)
#need to find a way to fix this in audiostream's setup.py
raise RuntimeError('Audiostream library is not yet able to configure itself to link against SDL2. Patch on audiostream library needed - any help much appreciated!')
env = super(AudiostreamRecipe, self).get_recipe_env(arch)
env['CFLAGS'] += ' -I{jni_path}/{sdl_include}/include -I{jni_path}/{sdl_mixer_include}'.format( env['CFLAGS'] += ' -I{jni_path}/{sdl_include}/include -I{jni_path}/{sdl_mixer_include}'.format(
jni_path=join(self.ctx.bootstrap.build_dir, 'jni'), jni_path=join(self.ctx.bootstrap.build_dir, 'jni'),
sdl_include=sdl_include, sdl_include=sdl_include,
sdl_mixer_include=sdl_mixer_include) sdl_mixer_include=sdl_mixer_include)
env['NDKPLATFORM'] = self.ctx.ndk_platform
env['LIBLINK'] = 'NOTNONE' # Hacky fix. Needed by audiostream setup.py
return env return env
recipe = AudiostreamRecipe() recipe = AudiostreamRecipe()

View file

@ -3,10 +3,10 @@ from pythonforandroid.recipe import PythonRecipe
class BabelRecipe(PythonRecipe): class BabelRecipe(PythonRecipe):
name = 'babel' name = 'babel'
version = '2.1.1' version = '2.2.0'
url = 'https://pypi.python.org/packages/source/B/Babel/Babel-{version}.tar.gz' url = 'https://pypi.python.org/packages/source/B/Babel/Babel-{version}.tar.gz'
depends = [('python2', 'python3crystax'), 'setuptools', 'pytz'] depends = ['setuptools', 'pytz']
call_hostpython_via_targetpython = False call_hostpython_via_targetpython = False
install_in_hostpython = True install_in_hostpython = True

View file

@ -1,17 +1,45 @@
from pythonforandroid.toolchain import Recipe, shprint, shutil, current_directory from pythonforandroid.toolchain import Recipe, shprint, shutil, current_directory
from os.path import join, exists from os.path import join, exists
from os import environ
import sh import sh
""" """
This recipe creates a custom toolchain and bootstraps Boost from source to build Boost.Build This recipe creates a custom toolchain and bootstraps Boost from source to build Boost.Build
including python bindings including python bindings
""" """
class BoostRecipe(Recipe): class BoostRecipe(Recipe):
version = '1.60.0' # Todo: make recipe compatible with all p4a architectures
# Don't forget to change the URL when changing the version '''
url = 'http://downloads.sourceforge.net/project/boost/boost/{version}/boost_1_60_0.tar.bz2' .. note:: This recipe can be built only against API 21+ and arch armeabi-v7a
depends = ['python2']
patches = ['disable-so-version.patch', 'use-android-libs.patch'] .. versionchanged:: 0.6.0
Rewrote recipe to support clang's build. The following changes has
been made:
- Bumped version number to 1.68.0
- Better version handling for url
- Added python 3 compatibility
- Default compiler for ndk's toolchain set to clang
- Python version will be detected via user-config.jam
- Changed stl's lib from ``gnustl_shared`` to ``c++_shared``
'''
version = '1.68.0'
url = 'http://downloads.sourceforge.net/project/boost/' \
'boost/{version}/boost_{version_underscore}.tar.bz2'
depends = [('python2', 'python3')]
patches = ['disable-so-version.patch',
'use-android-libs.patch',
'fix-android-issues.patch']
@property
def versioned_url(self):
if self.url is None:
return None
return self.url.format(
version=self.version,
version_underscore=self.version.replace('.', '_'))
def should_build(self, arch): def should_build(self, arch):
return not exists(join(self.get_build_dir(arch.arch), 'b2')) return not exists(join(self.get_build_dir(arch.arch), 'b2'))
@ -26,7 +54,9 @@ class BoostRecipe(Recipe):
shprint(bash, join(self.ctx.ndk_dir, 'build/tools/make-standalone-toolchain.sh'), shprint(bash, join(self.ctx.ndk_dir, 'build/tools/make-standalone-toolchain.sh'),
'--arch=' + env['ARCH'], '--arch=' + env['ARCH'],
'--platform=android-' + str(self.ctx.android_api), '--platform=android-' + str(self.ctx.android_api),
'--toolchain=' + env['CROSSHOST'] + '-' + env['TOOLCHAIN_VERSION'], '--toolchain=' + env['CROSSHOST'] + '-' + self.ctx.toolchain_version + ':-llvm',
'--use-llvm',
'--stl=libc++',
'--install-dir=' + env['CROSSHOME'] '--install-dir=' + env['CROSSHOME']
) )
# Set custom configuration # Set custom configuration
@ -36,31 +66,38 @@ class BoostRecipe(Recipe):
def build_arch(self, arch): def build_arch(self, arch):
super(BoostRecipe, self).build_arch(arch) super(BoostRecipe, self).build_arch(arch)
env = self.get_recipe_env(arch) env = self.get_recipe_env(arch)
env['PYTHON_HOST'] = self.ctx.hostpython
with current_directory(self.get_build_dir(arch.arch)): with current_directory(self.get_build_dir(arch.arch)):
# Compile Boost.Build engine with this custom toolchain # Compile Boost.Build engine with this custom toolchain
bash = sh.Command('bash') bash = sh.Command('bash')
shprint(bash, 'bootstrap.sh', shprint(bash, 'bootstrap.sh') # Do not pass env
'--with-python=' + join(env['PYTHON_ROOT'], 'bin/python.host'),
'--with-python-version=2.7',
'--with-python-root=' + env['PYTHON_ROOT']
) # Do not pass env
# Install app stl # Install app stl
shutil.copyfile(join(env['CROSSHOME'], env['CROSSHOST'], 'lib/libgnustl_shared.so'), shutil.copyfile(
join(self.ctx.get_libs_dir(arch.arch), 'libgnustl_shared.so')) join(self.ctx.ndk_dir, 'sources/cxx-stl/llvm-libc++/libs/'
'armeabi-v7a/libc++_shared.so'),
join(self.ctx.get_libs_dir(arch.arch), 'libc++_shared.so'))
def select_build_arch(self, arch): def select_build_arch(self, arch):
return arch.arch.replace('eabi-v7a', '').replace('eabi', '') return arch.arch.replace('eabi-v7a', '').replace('eabi', '')
def get_recipe_env(self, arch): def get_recipe_env(self, arch):
env = super(BoostRecipe, self).get_recipe_env(arch) # We don't use the normal env because we
# are building with a standalone toolchain
env = environ.copy()
env['BOOST_BUILD_PATH'] = self.get_build_dir(arch.arch) # find user-config.jam env['BOOST_BUILD_PATH'] = self.get_build_dir(arch.arch) # find user-config.jam
env['BOOST_ROOT'] = env['BOOST_BUILD_PATH'] # find boost source env['BOOST_ROOT'] = env['BOOST_BUILD_PATH'] # find boost source
env['PYTHON_ROOT'] = self.ctx.get_python_install_dir()
env['PYTHON_ROOT'] = self.ctx.python_recipe.link_root(arch.arch)
env['PYTHON_INCLUDE'] = self.ctx.python_recipe.include_root(arch.arch)
env['PYTHON_MAJOR_MINOR'] = self.ctx.python_recipe.version[:3]
env['PYTHON_LINK_VERSION'] = self.ctx.python_recipe.major_minor_version_string
if 'python3' in self.ctx.python_recipe.name:
env['PYTHON_LINK_VERSION'] += 'm'
env['ARCH'] = self.select_build_arch(arch) env['ARCH'] = self.select_build_arch(arch)
env['ANDROIDAPI'] = str(self.ctx.android_api)
env['CROSSHOST'] = env['ARCH'] + '-linux-androideabi' env['CROSSHOST'] = env['ARCH'] + '-linux-androideabi'
env['CROSSHOME'] = join(env['BOOST_ROOT'], 'standalone-' + env['ARCH'] + '-toolchain') env['CROSSHOME'] = join(env['BOOST_ROOT'], 'standalone-' + env['ARCH'] + '-toolchain')
env['TOOLCHAIN_PREFIX'] = join(env['CROSSHOME'], 'bin', env['CROSSHOST'])
return env return env

View file

@ -0,0 +1,68 @@
diff -u -r boost_1_68_0.orig/boost/config/user.hpp boost_1_68_0/boost/config/user.hpp
--- boost_1_68_0.orig/boost/config/user.hpp 2018-08-01 22:50:46.000000000 +0200
+++ boost_1_68_0/boost/config/user.hpp 2018-08-27 15:43:38.000000000 +0200
@@ -13,6 +13,12 @@
// configuration policy:
//
+// Android defines
+// There is problem with std::atomic on android (and some other platforms).
+// See this link for more info:
+// https://code.google.com/p/android/issues/detail?id=42735#makechanges
+#define BOOST_ASIO_DISABLE_STD_ATOMIC 1
+
// define this to locate a compiler config file:
// #define BOOST_COMPILER_CONFIG <myheader>
diff -u -r boost_1_68_0.orig/boost/asio/detail/config.hpp boost_1_68_0/boost/asio/detail/config.hpp
--- boost_1_68_0.orig/boost/asio/detail/config.hpp 2018-08-01 22:50:46.000000000 +0200
+++ boost_1_68_0/boost/asio/detail/config.hpp 2018-09-19 12:39:56.000000000 +0200
@@ -804,7 +804,11 @@
# if defined(__clang__)
# if (__cplusplus >= 201402)
# if __has_include(<experimental/string_view>)
-# define BOOST_ASIO_HAS_STD_EXPERIMENTAL_STRING_VIEW 1
+# if __clang_major__ >= 7
+# undef BOOST_ASIO_HAS_STD_EXPERIMENTAL_STRING_VIEW
+# else
+# define BOOST_ASIO_HAS_STD_EXPERIMENTAL_STRING_VIEW 1
+# endif // __clang_major__ >= 7
# endif // __has_include(<experimental/string_view>)
# endif // (__cplusplus >= 201402)
# endif // defined(__clang__)
diff -u -r boost_1_68_0.orig/boost/system/error_code.hpp boost_1_68_0/boost/system/error_code.hpp
--- boost_1_68_0.orig/boost/system/error_code.hpp 2018-08-01 22:50:53.000000000 +0200
+++ boost_1_68_0/boost/system/error_code.hpp 2018-08-27 15:44:29.000000000 +0200
@@ -17,6 +17,7 @@
#include <boost/assert.hpp>
#include <boost/noncopyable.hpp>
#include <boost/utility/enable_if.hpp>
+#include <stdio.h>
#include <ostream>
#include <string>
#include <stdexcept>
diff -u -r boost_1_68_0.orig/libs/filesystem/src/operations.cpp boost_1_68_0/libs/filesystem/src/operations.cpp
--- boost_1_68_0.orig/libs/filesystem/src/operations.cpp 2018-08-01 22:50:47.000000000 +0200
+++ boost_1_68_0/libs/filesystem/src/operations.cpp 2018-08-27 15:47:15.000000000 +0200
@@ -232,6 +232,21 @@
# if defined(BOOST_POSIX_API)
+# if defined(__ANDROID__)
+# define truncate libboost_truncate_wrapper
+// truncate() is present in Android libc only starting from ABI 21, so here's a simple wrapper
+static int libboost_truncate_wrapper(const char *path, off_t length)
+{
+ int fd = open(path, O_WRONLY);
+ if (fd == -1) {
+ return -1;
+ }
+ int status = ftruncate(fd, length);
+ close(fd);
+ return status;
+}
+# endif
+
typedef int err_t;
// POSIX uses a 0 return to indicate success

View file

@ -1,28 +1,61 @@
import os ; import os ;
local ANDROIDNDK = [ os.environ ANDROIDNDK ] ;
local ANDROIDAPI = [ os.environ ANDROIDAPI ] ;
local TOOLCHAIN_VERSION = [ os.environ TOOLCHAIN_VERSION ] ;
local TOOLCHAIN_PREFIX = [ os.environ TOOLCHAIN_PREFIX ] ;
local ARCH = [ os.environ ARCH ] ; local ARCH = [ os.environ ARCH ] ;
local CROSSHOME = [ os.environ CROSSHOME ] ;
local PYTHON_HOST = [ os.environ PYTHON_HOST ] ;
local PYTHON_ROOT = [ os.environ PYTHON_ROOT ] ; local PYTHON_ROOT = [ os.environ PYTHON_ROOT ] ;
local PYTHON_INCLUDE = [ os.environ PYTHON_INCLUDE ] ;
local PYTHON_LINK_VERSION = [ os.environ PYTHON_LINK_VERSION ] ;
local PYTHON_MAJOR_MINOR = [ os.environ PYTHON_MAJOR_MINOR ] ;
using gcc : $(ARCH) : $(TOOLCHAIN_PREFIX)-g++ : using clang : $(ARCH) : $(CROSSHOME)/bin/arm-linux-androideabi-clang++ :
<archiver>$(CROSSHOME)/bin/arm-linux-androideabi-ar
<root>$(CROSSHOME)/sysroot
<architecture>$(ARCH) <architecture>$(ARCH)
<archiver>$(TOOLCHAIN_PREFIX)-ar <compileflags>-fexceptions
<compileflags>-DBOOST_SP_USE_PTHREADS <compileflags>-frtti
<compileflags>-DBOOST_AC_USE_PTHREADS <compileflags>-fpic
<cxxflags>-DBOOST_SP_USE_PTHREADS <compileflags>-ffunction-sections
<cxxflags>-DBOOST_AC_USE_PTHREADS <compileflags>-funwind-tables
<cxxflags>-frtti <compileflags>-march=armv7-a
<cxxflags>-fexceptions <compileflags>-msoft-float
<compileflags>-I$(ANDROIDNDK)/platforms/android-$(ANDROIDAPI)/arch-$(ARCH)/usr/include <compileflags>-mfpu=neon
<compileflags>-I$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/include <compileflags>-mthumb
<compileflags>-I$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/libs/$(ARCH)/include <linkflags>-march=armv7-a
<compileflags>-I$(PYTHON_ROOT)/include/python2.7 <linkflags>-Wl,--fix-cortex-a8
<linkflags>--sysroot=$(ANDROIDNDK)/platforms/android-$(ANDROIDAPI)/arch-$(ARCH) <compileflags>-Os
<linkflags>-L$(ANDROIDNDK)/sources/cxx-stl/gnu-libstdc++/$(TOOLCHAIN_VERSION)/libs/$(ARCH) <compileflags>-fomit-frame-pointer
<linkflags>-L$(PYTHON_ROOT)/lib <compileflag>-fno-strict-aliasing
<linkflags>-lgnustl_shared <compileflags>-DANDROID
<linkflags>-lpython2.7 <compileflags>-D__ANDROID__
<compileflags>-DANDROID_TOOLCHAIN=clang
<compileflags>-DANDROID_ABI=armv7-a
<compileflags>-DANDROID_STL=c++_shared
<compileflags>-DBOOST_ALL_NO_LIB
#<compileflags>-DNDEBUG
<compileflags>-O2
<compileflags>-g
<compileflags>-fvisibility=hidden
<compileflags>-fvisibility-inlines-hidden
<compileflags>-fdata-sections
<cxxflags>-D__arm__
<cxxflags>-D_REENTRANT
<cxxflags>-D_GLIBCXX__PTHREADS
<compileflags>-Wno-long-long
<compileflags>-Wno-missing-field-initializers
<compileflags>-Wno-unused-variable
<linkflags>-Wl,-z,relro
<linkflags>-Wl,-z,now
<linkflags>-lc++_shared
<linkflags>-L$(PYTHON_ROOT)
<linkflags>-lpython$(PYTHON_LINK_VERSION)
<linkflags>-Wl,-O1
<linkflags>-Wl,-Bsymbolic-functions
;
using python : $(PYTHON_MAJOR_MINOR)
: $(PYTHON_host)
: $(PYTHON_ROOT) $(PYTHON_INCLUDE)
: $(PYTHON_ROOT)/libpython$(PYTHON_LINK_VERSION).so
: #<define>BOOST_ALL_DYN_LINK
; ;

View file

@ -1,5 +1,6 @@
from pythonforandroid.toolchain import Recipe from pythonforandroid.toolchain import Recipe
class BrokenRecipe(Recipe): class BrokenRecipe(Recipe):
def __init__(self): def __init__(self):
print('This is a broken recipe, not a real one!') print('This is a broken recipe, not a real one!')

View file

@ -1,5 +1,4 @@
from pythonforandroid.recipe import CompiledComponentsPythonRecipe
from pythonforandroid.toolchain import CompiledComponentsPythonRecipe
from pythonforandroid.patching import is_darwin from pythonforandroid.patching import is_darwin
@ -8,7 +7,7 @@ class CdecimalRecipe(CompiledComponentsPythonRecipe):
version = '2.3' version = '2.3'
url = 'http://www.bytereef.org/software/mpdecimal/releases/cdecimal-{version}.tar.gz' url = 'http://www.bytereef.org/software/mpdecimal/releases/cdecimal-{version}.tar.gz'
depends = ['python2'] depends = []
patches = ['locale.patch', patches = ['locale.patch',
'cross-compile.patch'] 'cross-compile.patch']

View file

@ -1,29 +1,52 @@
import os
from pythonforandroid.recipe import CompiledComponentsPythonRecipe from pythonforandroid.recipe import CompiledComponentsPythonRecipe
class CffiRecipe(CompiledComponentsPythonRecipe): class CffiRecipe(CompiledComponentsPythonRecipe):
"""
Extra system dependencies: autoconf, automake and libtool.
"""
name = 'cffi' name = 'cffi'
version = '1.4.2' version = '1.11.5'
url = 'https://pypi.python.org/packages/source/c/cffi/cffi-{version}.tar.gz' url = 'https://pypi.python.org/packages/source/c/cffi/cffi-{version}.tar.gz'
depends = [('python2', 'python3crystax'), 'setuptools', 'pycparser', 'libffi'] depends = ['setuptools', 'pycparser', 'libffi']
patches = ['disable-pkg-config.patch'] patches = ['disable-pkg-config.patch']
# call_hostpython_via_targetpython = False # call_hostpython_via_targetpython = False
install_in_hostpython = True install_in_hostpython = True
def get_hostrecipe_env(self, arch=None):
# fixes missing ffi.h on some host systems (e.g. gentoo)
env = super(CffiRecipe, self).get_hostrecipe_env(arch)
libffi = self.get_recipe('libffi', self.ctx)
includes = libffi.get_include_dirs(arch)
env['FFI_INC'] = ",".join(includes)
return env
def get_recipe_env(self, arch=None): def get_recipe_env(self, arch=None):
env = super(CffiRecipe, self).get_recipe_env(arch) env = super(CffiRecipe, self).get_recipe_env(arch)
libffi = self.get_recipe('libffi', self.ctx) libffi = self.get_recipe('libffi', self.ctx)
includes = libffi.get_include_dirs(arch) includes = libffi.get_include_dirs(arch)
env['CFLAGS'] = ' -I'.join([env.get('CFLAGS', '')] + includes) env['CFLAGS'] = ' -I'.join([env.get('CFLAGS', '')] + includes)
env['CFLAGS'] += ' -I{}'.format(self.ctx.python_recipe.include_root(arch.arch))
env['LDFLAGS'] = (env.get('CFLAGS', '') + ' -L' + env['LDFLAGS'] = (env.get('CFLAGS', '') + ' -L' +
self.ctx.get_libs_dir(arch.arch)) self.ctx.get_libs_dir(arch.arch))
env['LDFLAGS'] += ' -L{}'.format(os.path.join(self.ctx.bootstrap.build_dir, 'libs', arch.arch))
# required for libc and libdl
ndk_dir = self.ctx.ndk_platform
ndk_lib_dir = os.path.join(ndk_dir, 'usr', 'lib')
env['LDFLAGS'] += ' -L{}'.format(ndk_lib_dir)
env['LDFLAGS'] += " --sysroot={}".format(self.ctx.ndk_platform)
env['PYTHONPATH'] = ':'.join([ env['PYTHONPATH'] = ':'.join([
self.ctx.get_site_packages_dir(), self.ctx.get_site_packages_dir(),
env['BUILDLIB_PATH'], env['BUILDLIB_PATH'],
]) ])
env['LDFLAGS'] += ' -L{}'.format(self.ctx.python_recipe.link_root(arch.arch))
env['LDFLAGS'] += ' -lpython{}'.format(self.ctx.python_recipe.major_minor_version_string)
if 'python3' in self.ctx.python_recipe.name:
env['LDFLAGS'] += 'm'
return env return env

Some files were not shown because too many files have changed in this diff Show more