diff --git a/.gitignore b/.gitignore index c8a8ce14e3..5a0b00a76b 100755 --- a/.gitignore +++ b/.gitignore @@ -35,4 +35,6 @@ _vieux/ _mydocs .DS_Store Assets/DownloadBadges*.psd -node_modules \ No newline at end of file +node_modules +Tools/github_oauth_token.txt +_releases \ No newline at end of file diff --git a/ReactNativeClient/android/app/build.gradle b/ReactNativeClient/android/app/build.gradle index dc649e9be8..861c5adc32 100644 --- a/ReactNativeClient/android/app/build.gradle +++ b/ReactNativeClient/android/app/build.gradle @@ -75,7 +75,7 @@ apply from: "../../node_modules/react-native/react.gradle" * Upload all the APKs to the Play Store and people will download * the correct one based on the CPU architecture of their device. */ -def enableSeparateBuildPerCPUArchitecture = true +def enableSeparateBuildPerCPUArchitecture = false /** * Run Proguard to shrink the Java bytecode in release builds. @@ -137,11 +137,11 @@ android { } dependencies { - compile project(':react-native-securerandom') - compile project(':react-native-push-notification') - compile project(':react-native-fs') - compile project(':react-native-image-picker') - compile project(':react-native-vector-icons') + compile project(':react-native-securerandom') + compile project(':react-native-push-notification') + compile project(':react-native-fs') + compile project(':react-native-image-picker') + compile project(':react-native-vector-icons') compile project(':react-native-fs') compile fileTree(dir: "libs", include: ["*.jar"]) compile "com.android.support:appcompat-v7:23.0.1" diff --git a/ReactNativeClient/lib/fs-driver-rn.js b/ReactNativeClient/lib/fs-driver-rn.js index cd9f42edd4..ced34773d5 100644 --- a/ReactNativeClient/lib/fs-driver-rn.js +++ b/ReactNativeClient/lib/fs-driver-rn.js @@ -36,7 +36,7 @@ class FsDriverRN { return { birthtime: r.ctime, // Confusingly, "ctime" normally means "change time" but here it's used as "creation time" mtime: r.mtime, - isDirectory: () => return r.isDirectory(), + isDirectory: () => r.isDirectory(), path: path, }; } diff --git a/Tools/package-lock.json b/Tools/package-lock.json index 70f802de93..39597a3b05 100644 --- a/Tools/package-lock.json +++ b/Tools/package-lock.json @@ -73,6 +73,11 @@ "is-stream": "1.1.0" } }, + "pct-encode": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pct-encode/-/pct-encode-1.0.2.tgz", + "integrity": "sha1-uZt7BE1r18OeSDmnqAEirXUVyqU=" + }, "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", @@ -82,6 +87,14 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=" + }, + "uri-template": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-template/-/uri-template-1.0.1.tgz", + "integrity": "sha1-FKklo35Nk/diVDKqEWsF5Qyuga0=", + "requires": { + "pct-encode": "1.0.2" + } } } } diff --git a/Tools/package.json b/Tools/package.json index c22dbaa359..f47ba734bf 100644 --- a/Tools/package.json +++ b/Tools/package.json @@ -13,6 +13,7 @@ "gettext-parser": "^1.3.0", "marked": "^0.3.7", "mustache": "^2.3.0", - "node-fetch": "^1.7.3" + "node-fetch": "^1.7.3", + "uri-template": "^1.0.1" } } diff --git a/Tools/release-android.js b/Tools/release-android.js new file mode 100644 index 0000000000..b032218e79 --- /dev/null +++ b/Tools/release-android.js @@ -0,0 +1,132 @@ +const fs = require('fs-extra'); +const { execCommand } = require('./tool-utils.js'); +const path = require('path'); +const fetch = require('node-fetch'); +const uriTemplate = require('uri-template'); + +const rnDir = __dirname + '/../ReactNativeClient'; +const rootDir = path.dirname(__dirname); +const releaseDir = rootDir + '/_releases'; + +function increaseGradleVersionCode(content) { + const newContent = content.replace(/versionCode\s+(\d+)/, function(a, versionCode, c) { + const n = Number(versionCode); + if (isNaN(n) || !n) throw new Error('Invalid version code: ' + versionCode); + return 'versionCode ' + (n + 1); + }); + + if (newContent === content) throw new Error('Could not update version code'); + + return newContent; +} + +function increaseGradleVersionName(content) { + const newContent = content.replace(/(versionName\s+"\d+?\.\d+?\.)(\d+)"/, function(match, prefix, buildNum) { + const n = Number(buildNum); + if (isNaN(n) || !n) throw new Error('Invalid version code: ' + versionCode); + return prefix + (n + 1) + '"'; + }); + + if (newContent === content) throw new Error('Could not update version name'); + + return newContent; +} + +function updateGradleConfig() { + let content = fs.readFileSync(rnDir + '/android/app/build.gradle', 'utf8'); + content = increaseGradleVersionCode(content); + content = increaseGradleVersionName(content); + fs.writeFileSync(rnDir + '/android/app/build.gradle', content); + return content; +} + +function gradleVersionName(content) { + const matches = content.match(/versionName\s+"(\d+?\.\d+?\.\d+)"/); + if (!matches || matches.length < 1) throw new Error('Cannot get gradle version name'); + return matches[1]; +} + +async function githubOauthToken() { + const r = await fs.readFile(rootDir + '/Tools/github_oauth_token.txt'); + return r.toString(); +} + +async function main() { + const oauthToken = await githubOauthToken(); + + const newContent = updateGradleConfig(); + const version = gradleVersionName(newContent); + const tagName = 'android-v' + version; + const apkFilename = 'joplin-v' + version + '.apk'; + const apkFilePath = releaseDir + '/' + apkFilename; + const downloadUrl = 'https://github.com/laurent22/joplin/releases/download/' + tagName + '/' + apkFilename; + + process.chdir(rootDir); + + console.info('Running from: ' + process.cwd()); + + console.info('Building APK file...'); + const output = await execCommand('/mnt/c/Windows/System32/cmd.exe /c "cd ReactNativeClient\\android && gradlew.bat assembleRelease -PbuildDir=build --console plain"'); + console.info(output); + + await fs.mkdirp(releaseDir); + + console.info('Copying APK to ' + apkFilePath); + await fs.copy('ReactNativeClient/android/app/build/outputs/apk/app-release.apk', apkFilePath); + + console.info('Updating Readme URL...'); + + let readmeContent = await fs.readFile('README.md', 'utf8'); + readmeContent = readmeContent.replace(/(https:\/\/github.com\/laurent22\/joplin\/releases\/download\/.*?\.apk)/, downloadUrl); + await fs.writeFile('README.md', readmeContent); + + await execCommand('git add -A'); + await execCommand('git commit -m "Android release v' + version + '"'); + await execCommand('git tag ' + tagName); + await execCommand('git push'); + await execCommand('git push --tags'); + + console.info('Creating GitHub release ' + tagName + '...'); + + const response = await fetch('https://api.github.com/repos/laurent22/joplin/releases', { + method: 'POST', + body: JSON.stringify({ + tag_name: tagName, + name: tagName, + draft: true, + }), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'token ' + oauthToken, + }, + }); + + const responseText = await response.text(); + const responseJson = JSON.parse(responseText); + if (!responseJson.upload_url) throw new Error('No upload URL for release: ' + responseText); + + const uploadUrlTemplate = uriTemplate.parse(responseJson.upload_url); + const uploadUrl = uploadUrlTemplate.expand({ name: apkFilename }); + + const binaryBody = await fs.readFile(apkFilePath); + + console.info('Uploading ' + apkFilename + ' to ' + uploadUrl); + + const uploadResponse = await fetch(uploadUrl, { + method: 'POST', + body: binaryBody, + headers: { + 'Content-Type': 'application/vnd.android.package-archive', + 'Authorization': 'token ' + oauthToken, + 'Content-Length': binaryBody.length, + }, + }); + + const uploadResponseText = await uploadResponse.text(); + console.info(uploadResponseText); +} + +main().catch((error) => { + console.error('Fatal error'); + console.error(error); +}); \ No newline at end of file