##// END OF EJS Templates
merge with stable
Martin von Zweigbergk -
r43741:93f74a7d merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,15 b''
1 FROM centos:centos8
2
3 RUN groupadd -g %GID% build && \
4 useradd -u %UID% -g %GID% -s /bin/bash -d /build -m build
5
6 RUN yum install -y \
7 gcc \
8 gettext \
9 make \
10 python3-devel \
11 python3-docutils \
12 rpm-build
13
14 # For creating repo meta data
15 RUN yum install -y createrepo
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,187 +1,189 b''
1 1 35fb62a3a673d5322f6274a44ba6456e5e4b3b37 0 iD8DBQBEYmO2ywK+sNU5EO8RAnaYAKCO7x15xUn5mnhqWNXqk/ehlhRt2QCfRDfY0LrUq2q4oK/KypuJYPHgq1A=
2 2 2be3001847cb18a23c403439d9e7d0ace30804e9 0 iD8DBQBExUbjywK+sNU5EO8RAhzxAKCtyHAQUzcTSZTqlfJ0by6vhREwWQCghaQFHfkfN0l9/40EowNhuMOKnJk=
3 3 36a957364b1b89c150f2d0e60a99befe0ee08bd3 0 iD8DBQBFfL2QywK+sNU5EO8RAjYFAKCoGlaWRTeMsjdmxAjUYx6diZxOBwCfY6IpBYsKvPTwB3oktnPt5Rmrlys=
4 4 27230c29bfec36d5540fbe1c976810aefecfd1d2 0 iD8DBQBFheweywK+sNU5EO8RAt7VAKCrqJQWT2/uo2RWf0ZI4bLp6v82jACgjrMdsaTbxRsypcmEsdPhlG6/8F4=
5 5 fb4b6d5fe100b0886f8bc3d6731ec0e5ed5c4694 0 iD8DBQBGgHicywK+sNU5EO8RAgNxAJ0VG8ixAaeudx4sZbhngI1syu49HQCeNUJQfWBgA8bkJ2pvsFpNxwYaX3I=
6 6 23889160905a1b09fffe1c07378e9fc1827606eb 0 iD8DBQBHGTzoywK+sNU5EO8RAr/UAJ0Y8s4jQtzgS+G9vM8z6CWBThZ8fwCcCT5XDj2XwxKkz/0s6UELwjsO3LU=
7 7 bae2e9c838e90a393bae3973a7850280413e091a 0 iD8DBQBH6DO5ywK+sNU5EO8RAsfrAJ0e4r9c9GF/MJsM7Xjd3NesLRC3+ACffj6+6HXdZf8cswAoFPO+DY00oD0=
8 8 d5cbbe2c49cee22a9fbeb9ea41daa0ac4e26b846 0 iD8DBQBINdwsywK+sNU5EO8RAjIUAKCPmlFJSpsPAAUKF+iNHAwVnwmzeQCdEXrL27CWclXuUKdbQC8De7LICtE=
9 9 d2375bbee6d47e62ba8e415c86e83a465dc4dce9 0 iD8DBQBIo1wpywK+sNU5EO8RAmRNAJ94x3OFt6blbqu/yBoypm/AJ44fuACfUaldXcV5z9tht97hSp22DVTEPGc=
10 10 2a67430f92f15ea5159c26b09ec4839a0c549a26 0 iEYEABECAAYFAkk1hykACgkQywK+sNU5EO85QACeNJNUanjc2tl4wUoPHNuv+lSj0ZMAoIm93wSTc/feyYnO2YCaQ1iyd9Nu
11 11 3773e510d433969e277b1863c317b674cbee2065 0 iEYEABECAAYFAklNbbAACgkQywK+sNU5EO8o+gCfeb2/lfIJZMvyDA1m+G1CsBAxfFsAoIa6iAMG8SBY7hW1Q85Yf/LXEvaE
12 12 11a4eb81fb4f4742451591489e2797dc47903277 0 iEYEABECAAYFAklcAnsACgkQywK+sNU5EO+uXwCbBVHNNsLy1g7BlAyQJwadYVyHOXoAoKvtAVO71+bv7EbVoukwTzT+P4Sx
13 13 11efa41037e280d08cfb07c09ad485df30fb0ea8 0 iEYEABECAAYFAkmvJRQACgkQywK+sNU5EO9XZwCeLMgDgPSMWMm6vgjL4lDs2pEc5+0AnRxfiFbpbBfuEFTqKz9nbzeyoBlx
14 14 02981000012e3adf40c4849bd7b3d5618f9ce82d 0 iEYEABECAAYFAknEH3wACgkQywK+sNU5EO+uXwCeI+LbLMmhjU1lKSfU3UWJHjjUC7oAoIZLvYDGOL/tNZFUuatc3RnZ2eje
15 15 196d40e7c885fa6e95f89134809b3ec7bdbca34b 0 iEYEABECAAYFAkpL2X4ACgkQywK+sNU5EO9FOwCfXJycjyKJXsvQqKkHrglwOQhEKS4An36GfKzptfN8b1qNc3+ya/5c2WOM
16 16 3ef6c14a1e8e83a31226f5881b7fe6095bbfa6f6 0 iEYEABECAAYFAkpopLIACgkQywK+sNU5EO8QSgCfZ0ztsd071rOa2lhmp9Fyue/WoI0AoLTei80/xrhRlB8L/rZEf2KBl8dA
17 17 31ec469f9b556f11819937cf68ee53f2be927ebf 0 iEYEABECAAYFAksBuxAACgkQywK+sNU5EO+mBwCfagB+A0txzWZ6dRpug3LEoK7Z1QsAoKpbk8vsLjv6/oRDicSk/qBu33+m
18 18 439d7ea6fe3aa4ab9ec274a68846779153789de9 0 iEYEABECAAYFAksVw0kACgkQywK+sNU5EO/oZwCfdfBEkgp38xq6wN2F4nj+SzofrJIAnjmxt04vaJSeOOeHylHvk6lzuQsw
19 19 296a0b14a68621f6990c54fdba0083f6f20935bf 0 iEYEABECAAYFAks+jCoACgkQywK+sNU5EO9J8wCeMUGF9E/gS2UBsqIz56WS4HMPRPUAoI5J95mwEIK8Clrl7qFRidNI6APq
20 20 4aa619c4c2c09907034d9824ebb1dd0e878206eb 0 iEYEABECAAYFAktm9IsACgkQywK+sNU5EO9XGgCgk4HclRQhexEtooPE5GcUCdB6M8EAn2ptOhMVbIoO+JncA+tNACPFXh0O
21 21 ff2704a8ded37fbebd8b6eb5ec733731d725da8a 0 iEYEABECAAYFAkuRoSQACgkQywK+sNU5EO//3QCeJDc5r2uFyFCtAlpSA27DEE5rrxAAn2FSwTy9fhrB3QAdDQlwkEZcQzDh
22 22 2b01dab594167bc0dd33331dbaa6dca3dca1b3aa 0 iEYEABECAAYFAku1IwIACgkQywK+sNU5EO9MjgCdHLVwkTZlNHxhcznZKBL1rjN+J7cAoLLWi9LTL6f/TgBaPSKOy1ublbaW
23 23 39f725929f0c48c5fb3b90c071fc3066012456ca 0 iEYEABECAAYFAkvclvsACgkQywK+sNU5EO9FSwCeL9i5x8ALW/LE5+lCX6MFEAe4MhwAn1ev5o6SX6GrNdDfKweiemfO2VBk
24 24 fdcf80f26604f233dc4d8f0a5ef9d7470e317e8a 0 iEYEABECAAYFAkvsKTkACgkQywK+sNU5EO9qEACgiSiRGvTG2vXGJ65tUSOIYihTuFAAnRzRIqEVSw8M8/RGeUXRps0IzaCO
25 25 24fe2629c6fd0c74c90bd066e77387c2b02e8437 0 iEYEABECAAYFAkwFLRsACgkQywK+sNU5EO+pJACgp13tPI+pbwKZV+LeMjcQ4H6tCZYAoJebzhd6a8yYx6qiwpJxA9BXZNXy
26 26 f786fc4b8764cd2a5526d259cf2f94d8a66924d9 0 iEYEABECAAYFAkwsyxcACgkQywK+sNU5EO+crACfUpNAF57PmClkSri9nJcBjb2goN4AniPCNaKvnki7TnUsi1u2oxltpKKL
27 27 bf1774d95bde614af3956d92b20e2a0c68c5fec7 0 iEYEABECAAYFAkxVwccACgkQywK+sNU5EO+oFQCeJzwZ+we1fIIyBGCddHceOUAN++cAnjvT6A8ZWW0zV21NXIFF1qQmjxJd
28 28 c00f03a4982e467fb6b6bd45908767db6df4771d 0 iEYEABECAAYFAkxXDqsACgkQywK+sNU5EO/GJACfT9Rz4hZOxPQEs91JwtmfjevO84gAmwSmtfo5mmWSm8gtTUebCcdTv0Kf
29 29 ff5cec76b1c5b6be9c3bb923aae8c3c6d079d6b9 0 iD8DBQBMdo+qywK+sNU5EO8RAqQpAJ975BL2CCAiWMz9SXthNQ9xG181IwCgp4O+KViHPkufZVFn2aTKMNvcr1A=
30 30 93d8bff78c96fe7e33237b257558ee97290048a4 0 iD8DBQBMpfvdywK+sNU5EO8RAsxVAJ0UaL1XB51C76JUBhafc9GBefuMxwCdEWkTOzwvE0SarJBe9i008jhbqW4=
31 31 333421b9e0f96c7bc788e5667c146a58a9440a55 0 iD8DBQBMz0HOywK+sNU5EO8RAlsEAJ0USh6yOG7OrWkADGunVt9QimBQnwCbBqeMnKgSbwEw8jZwE3Iz1mdrYlo=
32 32 4438875ec01bd0fc32be92b0872eb6daeed4d44f 0 iD8DBQBM4WYUywK+sNU5EO8RAhCVAJ0dJswachwFAHALmk1x0RJehxzqPQCbBNskP9n/X689jB+btNTZTyKU/fw=
33 33 6aff4f144ad356311318b0011df0bb21f2c97429 0 iD8DBQBM9uxXywK+sNU5EO8RAv+4AKCDj4qKP16GdPaq1tP6BUwpM/M1OACfRyzLPp/qiiN8xJTWoWYSe/XjJug=
34 34 e3bf16703e2601de99e563cdb3a5d50b64e6d320 0 iD8DBQBNH8WqywK+sNU5EO8RAiQTAJ9sBO+TeiGro4si77VVaQaA6jcRUgCfSA28dBbjj0oFoQwvPoZjANiZBH8=
35 35 a6c855c32ea081da3c3b8ff628f1847ff271482f 0 iD8DBQBNSJJ+ywK+sNU5EO8RAoJaAKCweDEF70fu+r1Zn7pYDXdlk5RuSgCeO9gK/eit8Lin/1n3pO7aYguFLok=
36 36 2b2155623ee2559caf288fd333f30475966c4525 0 iD8DBQBNSJeBywK+sNU5EO8RAm1KAJ4hW9Cm9nHaaGJguchBaPLlAr+O3wCgqgmMok8bdAS06N6PL60PSTM//Gg=
37 37 2616325766e3504c8ae7c84bd15ee610901fe91d 0 iD8DBQBNbWy9ywK+sNU5EO8RAlWCAJ4mW8HbzjJj9GpK98muX7k+7EvEHwCfaTLbC/DH3QEsZBhEP+M8tzL6RU4=
38 38 aa1f3be38ab127280761889d2dca906ca465b5f4 0 iD8DBQBNeQq7ywK+sNU5EO8RAlEOAJ4tlEDdetE9lKfjGgjbkcR8PrC3egCfXCfF3qNVvU/2YYjpgvRwevjvDy0=
39 39 b032bec2c0a651ca0ddecb65714bfe6770f67d70 0 iD8DBQBNlg5kywK+sNU5EO8RAnGEAJ9gmEx6MfaR4XcG2m/93vwtfyzs3gCgltzx8/YdHPwqDwRX/WbpYgi33is=
40 40 3cb1e95676ad089596bd81d0937cad37d6e3b7fb 0 iD8DBQBNvTy4ywK+sNU5EO8RAmp8AJ9QnxK4jTJ7G722MyeBxf0UXEdGwACgtlM7BKtNQfbEH/fOW5y+45W88VI=
41 41 733af5d9f6b22387913e1d11350fb8cb7c1487dd 0 iD8DBQBN5q/8ywK+sNU5EO8RArRGAKCNGT94GKIYtSuwZ57z1sQbcw6uLACfffpbMV4NAPMl8womAwg+7ZPKnIU=
42 42 de9eb6b1da4fc522b1cab16d86ca166204c24f25 0 iD8DBQBODhfhywK+sNU5EO8RAr2+AJ4ugbAj8ae8/K0bYZzx3sascIAg1QCeK3b+zbbVVqd3b7CDpwFnaX8kTd4=
43 43 4a43e23b8c55b4566b8200bf69fe2158485a2634 0 iD8DBQBONzIMywK+sNU5EO8RAj5SAJ0aPS3+JHnyI6bHB2Fl0LImbDmagwCdGbDLp1S7TFobxXudOH49bX45Iik=
44 44 d629f1e89021103f1753addcef6b310e4435b184 0 iD8DBQBOWAsBywK+sNU5EO8RAht4AJwJl9oNFopuGkj5m8aKuf7bqPkoAQCeNrEm7UhFsZKYT5iUOjnMV7s2LaM=
45 45 351a9292e430e35766c552066ed3e87c557b803b 0 iD8DBQBOh3zUywK+sNU5EO8RApFMAKCD3Y/u3avDFndznwqfG5UeTHMlvACfUivPIVQZyDZnhZMq0UhC6zhCEQg=
46 46 384082750f2c51dc917d85a7145748330fa6ef4d 0 iD8DBQBOmd+OywK+sNU5EO8RAgDgAJ9V/X+G7VLwhTpHrZNiOHabzSyzYQCdE2kKfIevJUYB9QLAWCWP6DPwrwI=
47 47 41453d55b481ddfcc1dacb445179649e24ca861d 0 iD8DBQBOsFhpywK+sNU5EO8RAqM6AKCyfxUae3/zLuiLdQz+JR78690eMACfQ6JTBQib4AbE+rUDdkeFYg9K/+4=
48 48 195dbd1cef0c2f9f8bcf4ea303238105f716bda3 0 iD8DBQBO1/fWywK+sNU5EO8RAmoPAKCR5lpv1D6JLURHD8KVLSV4GRVEBgCgnd0Sy78ligNfqAMafmACRDvj7vo=
49 49 6344043924497cd06d781d9014c66802285072e4 0 iD8DBQBPALgmywK+sNU5EO8RAlfhAJ9nYOdWnhfVDHYtDTJAyJtXBAQS9wCgnefoSQt7QABkbGxM+Q85UYEBuD0=
50 50 db33555eafeaf9df1e18950e29439eaa706d399b 0 iD8DBQBPGdzxywK+sNU5EO8RAppkAJ9jOXhUVE/97CPgiMA0pMGiIYnesQCfengAszcBiSiKGugiI8Okc9ghU+Y=
51 51 2aa5b51f310fb3befd26bed99c02267f5c12c734 0 iD8DBQBPKZ9bywK+sNU5EO8RAt1TAJ45r1eJ0YqSkInzrrayg4TVCh0SnQCgm0GA/Ua74jnnDwVQ60lAwROuz1Q=
52 52 53e2cd303ecf8ca7c7eeebd785c34e5ed6b0f4a4 0 iD8DBQBPT/fvywK+sNU5EO8RAnfYAKCn7d0vwqIb100YfWm1F7nFD5B+FACeM02YHpQLSNsztrBCObtqcnfod7Q=
53 53 b9bd95e61b49c221c4cca24e6da7c946fc02f992 0 iD8DBQBPeLsIywK+sNU5EO8RAvpNAKCtKe2gitz8dYn52IRF0hFOPCR7AQCfRJL/RWCFweu2T1vH/mUOCf8SXXc=
54 54 d9e2f09d5488c395ae9ddbb320ceacd24757e055 0 iD8DBQBPju/dywK+sNU5EO8RArBYAJ9xtifdbk+hCOJO8OZa4JfHX8OYZQCeKPMBaBWiT8N/WHoOm1XU0q+iono=
55 55 00182b3d087909e3c3ae44761efecdde8f319ef3 0 iD8DBQBPoFhIywK+sNU5EO8RAhzhAKCBj1n2jxPTkZNJJ5pSp3soa+XHIgCgsZZpAQxOpXwCp0eCdNGe0+pmxmg=
56 56 5983de86462c5a9f42a3ad0f5e90ce5b1d221d25 0 iD8DBQBPovNWywK+sNU5EO8RAhgiAJ980T91FdPTRMmVONDhpkMsZwVIMACgg3bKvoWSeuCW28llUhAJtUjrMv0=
57 57 85a358df5bbbe404ca25730c9c459b34263441dc 0 iD8DBQBPyZsWywK+sNU5EO8RAnpLAJ48qrGDJRT+pteS0mSQ11haqHstPwCdG4ccGbk+0JHb7aNy8/NRGAOqn9w=
58 58 b013baa3898e117959984fc64c29d8c784d2f28b 0 iD8DBQBP8QOPywK+sNU5EO8RAqimAKCFRSx0lvG6y8vne2IhNG062Hn0dACeMLI5/zhpWpHBIVeAAquYfx2XFeA=
59 59 7f5094bb3f423fc799e471aac2aee81a7ce57a0b 0 iD8DBQBQGiL8ywK+sNU5EO8RAq5oAJ4rMMCPx6O+OuzNXVOexogedWz/QgCeIiIxLd76I4pXO48tdXhr0hQcBuM=
60 60 072209ae4ddb654eb2d5fd35bff358c738414432 0 iD8DBQBQQkq0ywK+sNU5EO8RArDTAJ9nk5CySnNAjAXYvqvx4uWCw9ThZwCgqmFRehH/l+oTwj3f8nw8u8qTCdc=
61 61 b3f0f9a39c4e1d0250048cd803ab03542d6f140a 0 iD8DBQBQamltywK+sNU5EO8RAlsqAJ4qF/m6aFu4mJCOKTiAP5RvZFK02ACfawYShUZO6OXEFfveU0aAxDR0M1k=
62 62 d118a4f4fd16d9b558ec3f3e87bfee772861d2b7 0 iD8DBQBQgPV5ywK+sNU5EO8RArylAJ0abcx5NlDjyv3ZDWpAfRIHyRsJtQCgn4TMuEayqgxzrvadQZHdTEU2g38=
63 63 195ad823b5d58c68903a6153a25e3fb4ed25239d 0 iD8DBQBQkuT9ywK+sNU5EO8RAhB4AKCeerItoK2Jipm2cVf4euGofAa/WACeJj3TVd4pFILpb+ogj7ebweFLJi0=
64 64 0c10cf8191469e7c3c8844922e17e71a176cb7cb 0 iD8DBQBQvQWoywK+sNU5EO8RAnq3AJoCn98u4geFx5YaQaeh99gFhCd7bQCgjoBwBSUyOvGd0yBy60E3Vv3VZhM=
65 65 a4765077b65e6ae29ba42bab7834717b5072d5ba 0 iD8DBQBQ486sywK+sNU5EO8RAhmJAJ90aLfLKZhmcZN7kqphigQJxiFOQACeJ5IUZxjGKH4xzi3MrgIcx9n+dB0=
66 66 f5fbe15ca7449f2c9a3cf817c86d0ae68b307214 0 iD8DBQBQ+yuYywK+sNU5EO8RAm9JAJoD/UciWvpGeKBcpGtZJBFJVcL/HACghDXSgQ+xQDjB+6uGrdgAQsRR1Lg=
67 67 a6088c05e43a8aee0472ca3a4f6f8d7dd914ebbf 0 iD8DBQBRDDROywK+sNU5EO8RAh75AJ9uJCGoCWnP0Lv/+XuYs4hvUl+sAgCcD36QgAnuw8IQXrvv684BAXAnHcA=
68 68 7511d4df752e61fe7ae4f3682e0a0008573b0402 0 iD8DBQBRFYaoywK+sNU5EO8RAuErAJoDyhXn+lptU3+AevVdwAIeNFyR2gCdHzPHyWd+JDeWCUR+pSOBi8O2ppM=
69 69 5b7175377babacce80a6c1e12366d8032a6d4340 0 iD8DBQBRMCYgywK+sNU5EO8RAq1/AKCWKlt9ysibyQgYwoxxIOZv5J8rpwCcDSHQaaf1fFZUTnQsOePwcM2Y/Sg=
70 70 50c922c1b5145dab8baefefb0437d363b6a6c21c 0 iD8DBQBRWnUnywK+sNU5EO8RAuQRAJwM42cJqJPeqJ0jVNdMqKMDqr4dSACeP0cRVGz1gitMuV0x8f3mrZrqc7I=
71 71 8a7bd2dccd44ed571afe7424cd7f95594f27c092 0 iD8DBQBRXfBvywK+sNU5EO8RAn+LAKCsMmflbuXjYRxlzFwId5ptm8TZcwCdGkyLbZcASBOkzQUm/WW1qfknJHU=
72 72 292cd385856d98bacb2c3086f8897bc660c2beea 0 iD8DBQBRcM0BywK+sNU5EO8RAjp4AKCJBykQbvXhKuvLSMxKx3a2TBiXcACfbr/kLg5GlZTF/XDPmY+PyHgI/GM=
73 73 23f785b38af38d2fca6b8f3db56b8007a84cd73a 0 iD8DBQBRgZwNywK+sNU5EO8RAmO4AJ4u2ILGuimRP6MJgE2t65LZ5dAdkACgiENEstIdrlFC80p+sWKD81kKIYI=
74 74 ddc7a6be20212d18f3e27d9d7e6f079a66d96f21 0 iD8DBQBRkswvywK+sNU5EO8RAiYYAJsHTHyHbJeAgmGvBTmDrfcKu4doUgCeLm7eGBjx7yAPUvEtxef8rAkQmXI=
75 75 cceaf7af4c9e9e6fa2dbfdcfe9856c5da69c4ffd 0 iD8DBQBRqnFLywK+sNU5EO8RAsWNAJ9RR6t+y1DLFc2HeH0eN9VfZAKF9gCeJ8ezvhtKq/LMs0/nvcgKQc/d5jk=
76 76 009794acc6e37a650f0fae37872e733382ac1c0c 0 iD8DBQBR0guxywK+sNU5EO8RArNkAKCq9pMihVzP8Os5kCmgbWpe5C37wgCgqzuPZTHvAsXF5wTyaSTMVa9Ccq4=
77 77 f0d7721d7322dcfb5af33599c2543f27335334bb 0 iD8DBQBR8taaywK+sNU5EO8RAqeEAJ4idDhhDuEsgsUjeQgWNj498matHACfT67gSF5w0ylsrBx1Hb52HkGXDm0=
78 78 f37b5a17e6a0ee17afde2cdde5393dd74715fb58 0 iD8DBQBR+ymFywK+sNU5EO8RAuSdAJkBMcd9DAZ3rWE9WGKPm2YZ8LBoXACfXn/wbEsVy7ZgJoUwiWmHSnQaWCI=
79 79 335a558f81dc73afeab4d7be63617392b130117f 0 iQIVAwUAUiZrIyBXgaxoKi1yAQK2iw//cquNqqSkc8Re5/TZT9I6NH+lh6DbOKjJP0Xl1Wqq0K+KSIUgZG4G32ovaEb2l5X0uY+3unRPiZ0ebl0YSw4Fb2ZiPIADXLBTOYRrY2Wwd3tpJeGI6wEgZt3SfcITV/g7NJrCjT3FlYoSOIayrExM80InSdcEM0Q3Rx6HKzY2acyxzgZeAtAW5ohFvHilSvY6p5Gcm4+QptMxvw45GPdreUmjeXZxNXNXZ8P+MjMz/QJbai/N7PjmK8lqnhkBsT48Ng/KhhmOkGntNJ2/ImBWLFGcWngSvJ7sfWwnyhndvGhe0Hq1NcCf7I8TjNDxU5TR+m+uW7xjXdLoDbUjBdX4sKXnh8ZjbYiODKBOrrDq25cf8nA/tnpKyE/qsVy60kOk6loY4XKiYmn1V49Ta0emmDx0hqo3HgxHHsHX0NDnGdWGol7cPRET0RzVobKq1A0jnrhPooWidvLh9bPzLonrWDo+ib+DuySoRkuYUK4pgZJ2mbg6daFOBEZygkSyRB8bo1UQUP7EgQDrWe4khb/5GHEfDkrQz3qu/sXvc0Ir1mOUWBFPHC2DjjCn/oMJuUkG1SwM8l2Bfv7h67ssES6YQ2+RjOix4yid7EXS/Ogl45PzCIPSI5+BbNs10JhE0w5uErBHlF53EDTe/TSLc+GU6DB6PP6dH912Njdr3jpNSUQ=
80 80 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
@@ -1,200 +1,202 b''
1 1 d40cc5aacc31ed673d9b5b24f98bee78c283062c 0.4f
2 2 1c590d34bf61e2ea12c71738e5a746cd74586157 0.4e
3 3 7eca4cfa8aad5fce9a04f7d8acadcd0452e2f34e 0.4d
4 4 b4d0c3786ad3e47beacf8412157326a32b6d25a4 0.4c
5 5 f40273b0ad7b3a6d3012fd37736d0611f41ecf54 0.5
6 6 0a28dfe59f8fab54a5118c5be4f40da34a53cdb7 0.5b
7 7 12e0fdbc57a0be78f0e817fd1d170a3615cd35da 0.6
8 8 4ccf3de52989b14c3d84e1097f59e39a992e00bd 0.6b
9 9 eac9c8efcd9bd8244e72fb6821f769f450457a32 0.6c
10 10 979c049974485125e1f9357f6bbe9c1b548a64c3 0.7
11 11 3a56574f329a368d645853e0f9e09472aee62349 0.8
12 12 6a03cff2b0f5d30281e6addefe96b993582f2eac 0.8.1
13 13 35fb62a3a673d5322f6274a44ba6456e5e4b3b37 0.9
14 14 2be3001847cb18a23c403439d9e7d0ace30804e9 0.9.1
15 15 36a957364b1b89c150f2d0e60a99befe0ee08bd3 0.9.2
16 16 27230c29bfec36d5540fbe1c976810aefecfd1d2 0.9.3
17 17 fb4b6d5fe100b0886f8bc3d6731ec0e5ed5c4694 0.9.4
18 18 23889160905a1b09fffe1c07378e9fc1827606eb 0.9.5
19 19 bae2e9c838e90a393bae3973a7850280413e091a 1.0
20 20 d5cbbe2c49cee22a9fbeb9ea41daa0ac4e26b846 1.0.1
21 21 d2375bbee6d47e62ba8e415c86e83a465dc4dce9 1.0.2
22 22 2a67430f92f15ea5159c26b09ec4839a0c549a26 1.1
23 23 3773e510d433969e277b1863c317b674cbee2065 1.1.1
24 24 11a4eb81fb4f4742451591489e2797dc47903277 1.1.2
25 25 11efa41037e280d08cfb07c09ad485df30fb0ea8 1.2
26 26 02981000012e3adf40c4849bd7b3d5618f9ce82d 1.2.1
27 27 196d40e7c885fa6e95f89134809b3ec7bdbca34b 1.3
28 28 3ef6c14a1e8e83a31226f5881b7fe6095bbfa6f6 1.3.1
29 29 31ec469f9b556f11819937cf68ee53f2be927ebf 1.4
30 30 439d7ea6fe3aa4ab9ec274a68846779153789de9 1.4.1
31 31 296a0b14a68621f6990c54fdba0083f6f20935bf 1.4.2
32 32 4aa619c4c2c09907034d9824ebb1dd0e878206eb 1.4.3
33 33 ff2704a8ded37fbebd8b6eb5ec733731d725da8a 1.5
34 34 2b01dab594167bc0dd33331dbaa6dca3dca1b3aa 1.5.1
35 35 39f725929f0c48c5fb3b90c071fc3066012456ca 1.5.2
36 36 fdcf80f26604f233dc4d8f0a5ef9d7470e317e8a 1.5.3
37 37 24fe2629c6fd0c74c90bd066e77387c2b02e8437 1.5.4
38 38 f786fc4b8764cd2a5526d259cf2f94d8a66924d9 1.6
39 39 bf1774d95bde614af3956d92b20e2a0c68c5fec7 1.6.1
40 40 c00f03a4982e467fb6b6bd45908767db6df4771d 1.6.2
41 41 ff5cec76b1c5b6be9c3bb923aae8c3c6d079d6b9 1.6.3
42 42 93d8bff78c96fe7e33237b257558ee97290048a4 1.6.4
43 43 333421b9e0f96c7bc788e5667c146a58a9440a55 1.7
44 44 4438875ec01bd0fc32be92b0872eb6daeed4d44f 1.7.1
45 45 6aff4f144ad356311318b0011df0bb21f2c97429 1.7.2
46 46 e3bf16703e2601de99e563cdb3a5d50b64e6d320 1.7.3
47 47 a6c855c32ea081da3c3b8ff628f1847ff271482f 1.7.4
48 48 2b2155623ee2559caf288fd333f30475966c4525 1.7.5
49 49 2616325766e3504c8ae7c84bd15ee610901fe91d 1.8
50 50 aa1f3be38ab127280761889d2dca906ca465b5f4 1.8.1
51 51 b032bec2c0a651ca0ddecb65714bfe6770f67d70 1.8.2
52 52 3cb1e95676ad089596bd81d0937cad37d6e3b7fb 1.8.3
53 53 733af5d9f6b22387913e1d11350fb8cb7c1487dd 1.8.4
54 54 de9eb6b1da4fc522b1cab16d86ca166204c24f25 1.9
55 55 4a43e23b8c55b4566b8200bf69fe2158485a2634 1.9.1
56 56 d629f1e89021103f1753addcef6b310e4435b184 1.9.2
57 57 351a9292e430e35766c552066ed3e87c557b803b 1.9.3
58 58 384082750f2c51dc917d85a7145748330fa6ef4d 2.0-rc
59 59 41453d55b481ddfcc1dacb445179649e24ca861d 2.0
60 60 195dbd1cef0c2f9f8bcf4ea303238105f716bda3 2.0.1
61 61 6344043924497cd06d781d9014c66802285072e4 2.0.2
62 62 db33555eafeaf9df1e18950e29439eaa706d399b 2.1-rc
63 63 2aa5b51f310fb3befd26bed99c02267f5c12c734 2.1
64 64 53e2cd303ecf8ca7c7eeebd785c34e5ed6b0f4a4 2.1.1
65 65 b9bd95e61b49c221c4cca24e6da7c946fc02f992 2.1.2
66 66 d9e2f09d5488c395ae9ddbb320ceacd24757e055 2.2-rc
67 67 00182b3d087909e3c3ae44761efecdde8f319ef3 2.2
68 68 5983de86462c5a9f42a3ad0f5e90ce5b1d221d25 2.2.1
69 69 85a358df5bbbe404ca25730c9c459b34263441dc 2.2.2
70 70 b013baa3898e117959984fc64c29d8c784d2f28b 2.2.3
71 71 a06e2681dd1786e2354d84a5fa9c1c88dd4fa3e0 2.3-rc
72 72 7f5094bb3f423fc799e471aac2aee81a7ce57a0b 2.3
73 73 072209ae4ddb654eb2d5fd35bff358c738414432 2.3.1
74 74 b3f0f9a39c4e1d0250048cd803ab03542d6f140a 2.3.2
75 75 d118a4f4fd16d9b558ec3f3e87bfee772861d2b7 2.4-rc
76 76 195ad823b5d58c68903a6153a25e3fb4ed25239d 2.4
77 77 0c10cf8191469e7c3c8844922e17e71a176cb7cb 2.4.1
78 78 a4765077b65e6ae29ba42bab7834717b5072d5ba 2.4.2
79 79 f5fbe15ca7449f2c9a3cf817c86d0ae68b307214 2.5-rc
80 80 a6088c05e43a8aee0472ca3a4f6f8d7dd914ebbf 2.5
81 81 7511d4df752e61fe7ae4f3682e0a0008573b0402 2.5.1
82 82 5b7175377babacce80a6c1e12366d8032a6d4340 2.5.2
83 83 50c922c1b5145dab8baefefb0437d363b6a6c21c 2.5.3
84 84 8a7bd2dccd44ed571afe7424cd7f95594f27c092 2.5.4
85 85 292cd385856d98bacb2c3086f8897bc660c2beea 2.6-rc
86 86 23f785b38af38d2fca6b8f3db56b8007a84cd73a 2.6
87 87 ddc7a6be20212d18f3e27d9d7e6f079a66d96f21 2.6.1
88 88 cceaf7af4c9e9e6fa2dbfdcfe9856c5da69c4ffd 2.6.2
89 89 009794acc6e37a650f0fae37872e733382ac1c0c 2.6.3
90 90 f0d7721d7322dcfb5af33599c2543f27335334bb 2.7-rc
91 91 f37b5a17e6a0ee17afde2cdde5393dd74715fb58 2.7
92 92 335a558f81dc73afeab4d7be63617392b130117f 2.7.1
93 93 e7fa36d2ad3a7944a52dca126458d6f482db3524 2.7.2
94 94 1596f2d8f2421314b1ddead8f7d0c91009358994 2.8-rc
95 95 d825e4025e39d1c39db943cdc89818abd0a87c27 2.8
96 96 209e04a06467e2969c0cc6501335be0406d46ef0 2.8.1
97 97 ca387377df7a3a67dbb90b6336b781cdadc3ef41 2.8.2
98 98 8862469e16f9236208581b20de5f96bd13cc039d 2.9-rc
99 99 3cec5134e9c4bceab6a00c60f52a4f80677a78f2 2.9
100 100 b96cb15ec9e04d8ac5ee08b34fcbbe4200588965 2.9.1
101 101 3f83fc5cfe715d292069ee8417c83804f6c6c1e4 2.9.2
102 102 564f55b251224f16508dd1311452db7780dafe2b 3.0-rc
103 103 2195ac506c6ababe86985b932f4948837c0891b5 3.0
104 104 269c80ee5b3cb3684fa8edc61501b3506d02eb10 3.0.1
105 105 2d8cd3d0e83c7336c0cb45a9f88638363f993848 3.0.2
106 106 6c36dc6cd61a0e1b563f1d51e55bdf4dacf12162 3.1-rc
107 107 3178e49892020336491cdc6945885c4de26ffa8b 3.1
108 108 5dc91146f35369949ea56b40172308158b59063a 3.1.1
109 109 f768c888aaa68d12dd7f509dcc7f01c9584357d0 3.1.2
110 110 7f8d16af8cae246fa5a48e723d48d58b015aed94 3.2-rc
111 111 ced632394371a36953ce4d394f86278ae51a2aae 3.2
112 112 643c58303fb0ec020907af28b9e486be299ba043 3.2.1
113 113 902554884335e5ca3661d63be9978eb4aec3f68a 3.2.2
114 114 6dad422ecc5adb63d9fa649eeb8e05a5f9bc4900 3.2.3
115 115 1265a3a71d75396f5d4cf6935ae7d9ba5407a547 3.2.4
116 116 db8e3f7948b1fdeb9ad12d448fc3525759908b9f 3.3-rc
117 117 fbdd5195528fae4f41feebc1838215c110b25d6a 3.3
118 118 5b4ed033390bf6e2879c8f5c28c84e1ee3b87231 3.3.1
119 119 07a92bbd02e5e3a625e0820389b47786b02b2cea 3.3.2
120 120 2e2e9a0750f91a6fe0ad88e4de34f8efefdcab08 3.3.3
121 121 e89f909edffad558b56f4affa8239e4832f88de0 3.4-rc
122 122 8cc6036bca532e06681c5a8fa37efaa812de67b5 3.4
123 123 ed18f4acf435a2824c6f49fba40f42b9df5da7ad 3.4.1
124 124 540cd0ddac49c1125b2e013aa2ff18ecbd4dd954 3.4.2
125 125 96a38d44ba093bd1d1ecfd34119e94056030278b 3.5-rc
126 126 21aa1c313b05b1a85f8ffa1120d51579ddf6bf24 3.5
127 127 1a45e49a6bed023deb229102a8903234d18054d3 3.5.1
128 128 9a466b9f9792e3ad7ae3fc6c43c3ff2e136b718d 3.5.2
129 129 b66e3ca0b90c3095ea28dfd39aa24247bebf5c20 3.6-rc
130 130 47dd34f2e7272be9e3b2a5a83cd0d20be44293f4 3.6
131 131 1aa5083cbebbe7575c88f3402ab377539b484897 3.6.1
132 132 2d437a0f3355834a9485bbbeb30a52a052c98f19 3.6.2
133 133 ea389970c08449440587712117f178d33bab3f1e 3.6.3
134 134 158bdc8965720ca4061f8f8d806563cfc7cdb62e 3.7-rc
135 135 2408645de650d8a29a6ce9e7dce601d8dd0d1474 3.7
136 136 b698abf971e7377d9b7ec7fc8c52df45255b0329 3.7.1
137 137 d493d64757eb45ada99fcb3693e479a51b7782da 3.7.2
138 138 ae279d4a19e9683214cbd1fe8298cf0b50571432 3.7.3
139 139 740156eedf2c450aee58b1a90b0e826f47c5da64 3.8-rc
140 140 f85de28eae32e7d3064b1a1321309071bbaaa069 3.8
141 141 a56296f55a5e1038ea5016dace2076b693c28a56 3.8.1
142 142 aaabed77791a75968a12b8c43ad263631a23ee81 3.8.2
143 143 a9764ab80e11bcf6a37255db7dd079011f767c6c 3.8.3
144 144 26a5d605b8683a292bb89aea11f37a81b06ac016 3.8.4
145 145 519bb4f9d3a47a6e83c2b414d58811ed38f503c2 3.9-rc
146 146 299546f84e68dbb9bd026f0f3a974ce4bdb93686 3.9
147 147 ccd436f7db6d5d7b9af89715179b911d031d44f1 3.9.1
148 148 149433e68974eb5c63ccb03f794d8b57339a80c4 3.9.2
149 149 438173c415874f6ac653efc1099dec9c9150e90f 4.0-rc
150 150 eab27446995210c334c3d06f1a659e3b9b5da769 4.0
151 151 b3b1ae98f6a0e14c1e1ba806a6c18e193b6dae5c 4.0.1
152 152 e69874dc1f4e142746ff3df91e678a09c6fc208c 4.0.2
153 153 a1dd2c0c479e0550040542e392e87bc91262517e 4.1-rc
154 154 e1526da1e6d84e03146151c9b6e6950fe9a83d7d 4.1
155 155 25703b624d27e3917d978af56d6ad59331e0464a 4.1.1
156 156 ed5b25874d998ababb181a939dd37a16ea644435 4.1.2
157 157 77eaf9539499a1b8be259ffe7ada787d07857f80 4.1.3
158 158 616e788321cc4ae9975b7f0c54c849f36d82182b 4.2-rc
159 159 bb96d4a497432722623ae60d9bc734a1e360179e 4.2
160 160 c850f0ed54c1d42f9aa079ad528f8127e5775217 4.2.1
161 161 26c49ed51a698ec016d2b4c6b44ca3c3f73cc788 4.2.2
162 162 857876ebaed4e315f63157bd157d6ce553c7ab73 4.3-rc
163 163 5544af8622863796a0027566f6b646e10d522c4c 4.3
164 164 943c91326b23954e6e1c6960d0239511f9530258 4.2.3
165 165 3fee7f7d2da04226914c2258cc2884dc27384fd7 4.3.1
166 166 920977f72c7b70acfdaf56ab35360584d7845827 4.3.2
167 167 2f427b57bf9019c6dc3750baa539dc22c1be50f6 4.3.3
168 168 1e2454b60e5936f5e77498cab2648db469504487 4.4-rc
169 169 0ccb43d4cf01d013ae05917ec4f305509f851b2d 4.4
170 170 cabc840ffdee8a72f3689fb77dd74d04fdc2bc04 4.4.1
171 171 a92b9f8e11ba330614cdfd6af0e03b15c1ff3797 4.4.2
172 172 27b6df1b5adbdf647cf5c6675b40575e1b197c60 4.5-rc
173 173 d334afc585e29577f271c5eda03378736a16ca6b 4.5
174 174 369aadf7a3264b03c8b09efce715bc41e6ab4a9b 4.5.1
175 175 8bba684efde7f45add05f737952093bb2aa07155 4.5.2
176 176 7de7bd407251af2bc98e5b809c8598ee95830daf 4.5.3
177 177 ed5448edcbfa747b9154099e18630e49024fd47b 4.6rc0
178 178 1ec874717d8a93b19e0d50628443e0ee5efab3a9 4.6rc1
179 179 6614cac550aea66d19c601e45efd1b7bd08d7c40 4.6
180 180 9c5ced5276d6e7d54f7c3dadf5247b7ee98ec79c 4.6.1
181 181 0b63a6743010dfdbf8a8154186e119949bdaa1cc 4.6.2
182 182 e90130af47ce8dd53a3109aed9d15876b3e7dee8 4.7rc0
183 183 33ac6a72308a215e6086fbced347ec10aa963b0a 4.7
184 184 ede3bf31fe63677fdf5bd8db687977d4e3d792ed 4.7.1
185 185 5405cb1a79010ac50c58cd84e6f50c4556bf2a4c 4.7.2
186 186 956ec6f1320df26f3133ec40f3de866ea0695fd7 4.8rc0
187 187 a91a2837150bdcb27ae76b3646e6c93cd6a15904 4.8
188 188 1c8c54cf97256f4468da2eb4dbee24f7f3888e71 4.8.1
189 189 197f092b2cd9691e2a55d198f717b231af9be6f9 4.8.2
190 190 593718ff5844cad7a27ee3eb5adad89ac8550949 4.9rc0
191 191 83377b4b4ae0e9a6b8e579f7b0a693b8cf5c3b10 4.9
192 192 4ea21df312ec7159c5b3633096b6ecf68750b0dd 4.9.1
193 193 4a8d9ed864754837a185a642170cde24392f9abf 5.0rc0
194 194 07e479ef7c9639be0029f00e6a722b96dcc05fee 5.0
195 195 c3484ddbdb9621256d597ed86b90d229c59c2af9 5.0.1
196 196 97ada9b8d51bef24c5cb4cdca4243f0db694ab6e 5.0.2
197 197 e386b5f4f8360dbb43a576dd9b1368e386fefa5b 5.1rc0
198 198 e91930d712e8507d1bc1b2dffd96c83edc4cbed3 5.1
199 199 a4e32fd539ab41489a51b2aa88bda9a73b839562 5.1.1
200 200 181e52f2b62f4768aa0d988936c929dc7c4a41a0 5.1.2
201 59338f9561099de77c684c00f76507f11e46ebe8 5.2rc0
202 ca3dca416f8d5863ca6f5a4a6a6bb835dcd5feeb 5.2
@@ -1,261 +1,257 b''
1 1 # If you want to change PREFIX, do not just edit it below. The changed
2 2 # value wont get passed on to recursive make calls. You should instead
3 3 # override the variable on the command like:
4 4 #
5 5 # % make PREFIX=/opt/ install
6 6
7 7 export PREFIX=/usr/local
8 8 PYTHON?=python
9 9 $(eval HGROOT := $(shell pwd))
10 10 HGPYTHONS ?= $(HGROOT)/build/pythons
11 11 PURE=
12 12 PYFILESCMD=find mercurial hgext doc -name '*.py'
13 13 PYFILES:=$(shell $(PYFILESCMD))
14 14 DOCFILES=mercurial/help/*.txt
15 15 export LANGUAGE=C
16 16 export LC_ALL=C
17 17 TESTFLAGS ?= $(shell echo $$HGTESTFLAGS)
18 18 OSXVERSIONFLAGS ?= $(shell echo $$OSXVERSIONFLAGS)
19 19 CARGO = cargo
20 20
21 21 # Set this to e.g. "mingw32" to use a non-default compiler.
22 22 COMPILER=
23 23
24 24 COMPILERFLAG_tmp_ =
25 25 COMPILERFLAG_tmp_${COMPILER} ?= -c $(COMPILER)
26 26 COMPILERFLAG=${COMPILERFLAG_tmp_${COMPILER}}
27 27
28 28 help:
29 29 @echo 'Commonly used make targets:'
30 30 @echo ' all - build program and documentation'
31 31 @echo ' install - install program and man pages to $$PREFIX ($(PREFIX))'
32 32 @echo ' install-home - install with setup.py install --home=$$HOME ($(HOME))'
33 33 @echo ' local - build for inplace usage'
34 34 @echo ' tests - run all tests in the automatic test suite'
35 35 @echo ' test-foo - run only specified tests (e.g. test-merge1.t)'
36 36 @echo ' dist - run all tests and create a source tarball in dist/'
37 37 @echo ' clean - remove files created by other targets'
38 38 @echo ' (except installed files or dist source tarball)'
39 39 @echo ' update-pot - update i18n/hg.pot'
40 40 @echo
41 41 @echo 'Example for a system-wide installation under /usr/local:'
42 42 @echo ' make all && su -c "make install" && hg version'
43 43 @echo
44 44 @echo 'Example for a local installation (usable in this directory):'
45 45 @echo ' make local && ./hg version'
46 46
47 47 all: build doc
48 48
49 49 local:
50 50 $(PYTHON) setup.py $(PURE) \
51 51 build_py -c -d . \
52 52 build_ext $(COMPILERFLAG) -i \
53 53 build_hgexe $(COMPILERFLAG) -i \
54 54 build_mo
55 55 env HGRCPATH= $(PYTHON) hg version
56 56
57 57 build:
58 58 $(PYTHON) setup.py $(PURE) build $(COMPILERFLAG)
59 59
60 60 wheel:
61 61 FORCE_SETUPTOOLS=1 $(PYTHON) setup.py $(PURE) bdist_wheel $(COMPILERFLAG)
62 62
63 63 doc:
64 64 $(MAKE) -C doc
65 65
66 66 cleanbutpackages:
67 67 -$(PYTHON) setup.py clean --all # ignore errors from this command
68 68 find contrib doc hgext hgext3rd i18n mercurial tests hgdemandimport \
69 69 \( -name '*.py[cdo]' -o -name '*.so' \) -exec rm -f '{}' ';'
70 70 rm -f MANIFEST MANIFEST.in hgext/__index__.py tests/*.err
71 71 rm -f mercurial/__modulepolicy__.py
72 72 if test -d .hg; then rm -f mercurial/__version__.py; fi
73 73 rm -rf build mercurial/locale
74 74 $(MAKE) -C doc clean
75 75 $(MAKE) -C contrib/chg distclean
76 76 rm -rf rust/target
77 77 rm -f mercurial/rustext.so
78 78
79 79 clean: cleanbutpackages
80 80 rm -rf packages
81 81
82 82 install: install-bin install-doc
83 83
84 84 install-bin: build
85 85 $(PYTHON) setup.py $(PURE) install --root="$(DESTDIR)/" --prefix="$(PREFIX)" --force
86 86
87 87 install-doc: doc
88 88 cd doc && $(MAKE) $(MFLAGS) install
89 89
90 90 install-home: install-home-bin install-home-doc
91 91
92 92 install-home-bin: build
93 93 $(PYTHON) setup.py $(PURE) install --home="$(HOME)" --prefix="" --force
94 94
95 95 install-home-doc: doc
96 96 cd doc && $(MAKE) $(MFLAGS) PREFIX="$(HOME)" install
97 97
98 98 MANIFEST-doc:
99 99 $(MAKE) -C doc MANIFEST
100 100
101 101 MANIFEST.in: MANIFEST-doc
102 102 hg manifest | sed -e 's/^/include /' > MANIFEST.in
103 103 echo include mercurial/__version__.py >> MANIFEST.in
104 104 sed -e 's/^/include /' < doc/MANIFEST >> MANIFEST.in
105 105
106 106 dist: tests dist-notests
107 107
108 108 dist-notests: doc MANIFEST.in
109 109 TAR_OPTIONS="--owner=root --group=root --mode=u+w,go-w,a+rX-s" $(PYTHON) setup.py -q sdist
110 110
111 111 check: tests
112 112
113 113 tests:
114 114 # Run Rust tests if cargo is installed
115 115 if command -v $(CARGO) >/dev/null 2>&1; then \
116 116 $(MAKE) rust-tests; \
117 117 fi
118 118 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS)
119 119
120 120 test-%:
121 121 cd tests && $(PYTHON) run-tests.py $(TESTFLAGS) $@
122 122
123 123 testpy-%:
124 124 @echo Looking for Python $* in $(HGPYTHONS)
125 125 [ -e $(HGPYTHONS)/$*/bin/python ] || ( \
126 126 cd $$(mktemp --directory --tmpdir) && \
127 127 $(MAKE) -f $(HGROOT)/contrib/Makefile.python PYTHONVER=$* PREFIX=$(HGPYTHONS)/$* python )
128 128 cd tests && $(HGPYTHONS)/$*/bin/python run-tests.py $(TESTFLAGS)
129 129
130 130 rust-tests: py_feature = $(shell $(PYTHON) -c \
131 131 'import sys; print(["python27-bin", "python3-bin"][sys.version_info[0] >= 3])')
132 132 rust-tests:
133 133 cd $(HGROOT)/rust/hg-cpython \
134 134 && $(CARGO) test --quiet --all \
135 135 --no-default-features --features "$(py_feature)"
136 136
137 137 check-code:
138 138 hg manifest | xargs python contrib/check-code.py
139 139
140 140 format-c:
141 141 clang-format --style file -i \
142 142 `hg files 'set:(**.c or **.cc or **.h) and not "listfile:contrib/clang-format-ignorelist"'`
143 143
144 144 update-pot: i18n/hg.pot
145 145
146 146 i18n/hg.pot: $(PYFILES) $(DOCFILES) i18n/posplit i18n/hggettext
147 147 $(PYTHON) i18n/hggettext mercurial/commands.py \
148 148 hgext/*.py hgext/*/__init__.py \
149 149 mercurial/fileset.py mercurial/revset.py \
150 150 mercurial/templatefilters.py \
151 151 mercurial/templatefuncs.py \
152 152 mercurial/templatekw.py \
153 153 mercurial/filemerge.py \
154 154 mercurial/hgweb/webcommands.py \
155 155 mercurial/util.py \
156 156 $(DOCFILES) > i18n/hg.pot.tmp
157 157 # All strings marked for translation in Mercurial contain
158 158 # ASCII characters only. But some files contain string
159 159 # literals like this '\037\213'. xgettext thinks it has to
160 160 # parse them even though they are not marked for translation.
161 161 # Extracting with an explicit encoding of ISO-8859-1 will make
162 162 # xgettext "parse" and ignore them.
163 163 $(PYFILESCMD) | xargs \
164 164 xgettext --package-name "Mercurial" \
165 165 --msgid-bugs-address "<mercurial-devel@mercurial-scm.org>" \
166 166 --copyright-holder "Matt Mackall <mpm@selenic.com> and others" \
167 167 --from-code ISO-8859-1 --join --sort-by-file --add-comments=i18n: \
168 168 -d hg -p i18n -o hg.pot.tmp
169 169 $(PYTHON) i18n/posplit i18n/hg.pot.tmp
170 170 # The target file is not created before the last step. So it never is in
171 171 # an intermediate state.
172 172 mv -f i18n/hg.pot.tmp i18n/hg.pot
173 173
174 174 %.po: i18n/hg.pot
175 175 # work on a temporary copy for never having a half completed target
176 176 cp $@ $@.tmp
177 177 msgmerge --no-location --update $@.tmp $^
178 178 mv -f $@.tmp $@
179 179
180 180 # Packaging targets
181 181
182 182 packaging_targets := \
183 183 centos5 \
184 184 centos6 \
185 185 centos7 \
186 centos8 \
186 187 deb \
187 188 docker-centos5 \
188 189 docker-centos6 \
189 190 docker-centos7 \
191 docker-centos8 \
190 192 docker-debian-jessie \
191 193 docker-debian-stretch \
192 docker-fedora20 \
193 docker-fedora21 \
194 docker-fedora28 \
195 docker-fedora29 \
194 docker-fedora \
196 195 docker-ubuntu-trusty \
197 196 docker-ubuntu-trusty-ppa \
198 197 docker-ubuntu-xenial \
199 198 docker-ubuntu-xenial-ppa \
200 199 docker-ubuntu-artful \
201 200 docker-ubuntu-artful-ppa \
202 201 docker-ubuntu-bionic \
203 202 docker-ubuntu-bionic-ppa \
204 fedora20 \
205 fedora21 \
206 fedora28 \
207 fedora29 \
203 fedora \
208 204 linux-wheels \
209 205 linux-wheels-x86_64 \
210 206 linux-wheels-i686 \
211 207 ppa
212 208
213 209 # Forward packaging targets for convenience.
214 210 $(packaging_targets):
215 211 $(MAKE) -C contrib/packaging $@
216 212
217 213 osx:
218 214 rm -rf build/mercurial
219 215 /usr/bin/python2.7 setup.py install --optimize=1 \
220 216 --root=build/mercurial/ --prefix=/usr/local/ \
221 217 --install-lib=/Library/Python/2.7/site-packages/
222 218 make -C doc all install DESTDIR="$(PWD)/build/mercurial/"
223 219 # Place a bogon .DS_Store file in the target dir so we can be
224 220 # sure it doesn't get included in the final package.
225 221 touch build/mercurial/.DS_Store
226 222 # install zsh completions - this location appears to be
227 223 # searched by default as of macOS Sierra.
228 224 install -d build/mercurial/usr/local/share/zsh/site-functions/
229 225 install -m 0644 contrib/zsh_completion build/mercurial/usr/local/share/zsh/site-functions/_hg
230 226 # install bash completions - there doesn't appear to be a
231 227 # place that's searched by default for bash, so we'll follow
232 228 # the lead of Apple's git install and just put it in a
233 229 # location of our own.
234 230 install -d build/mercurial/usr/local/hg/contrib/
235 231 install -m 0644 contrib/bash_completion build/mercurial/usr/local/hg/contrib/hg-completion.bash
236 232 make -C contrib/chg \
237 233 HGPATH=/usr/local/bin/hg \
238 234 PYTHON=/usr/bin/python2.7 \
239 235 HGEXTDIR=/Library/Python/2.7/site-packages/hgext \
240 236 DESTDIR=../../build/mercurial \
241 237 PREFIX=/usr/local \
242 238 clean install
243 239 mkdir -p $${OUTPUTDIR:-dist}
244 240 HGVER=$$(python contrib/genosxversion.py $(OSXVERSIONFLAGS) build/mercurial/Library/Python/2.7/site-packages/mercurial/__version__.py) && \
245 241 OSXVER=$$(sw_vers -productVersion | cut -d. -f1,2) && \
246 242 pkgbuild --filter \\.DS_Store --root build/mercurial/ \
247 243 --identifier org.mercurial-scm.mercurial \
248 244 --version "$${HGVER}" \
249 245 build/mercurial.pkg && \
250 246 productbuild --distribution contrib/packaging/macosx/distribution.xml \
251 247 --package-path build/ \
252 248 --version "$${HGVER}" \
253 249 --resources contrib/packaging/macosx/ \
254 250 "$${OUTPUTDIR:-dist/}"/Mercurial-"$${HGVER}"-macosx"$${OSXVER}".pkg
255 251
256 252 .PHONY: help all local build doc cleanbutpackages clean install install-bin \
257 253 install-doc install-home install-home-bin install-home-doc \
258 254 dist dist-notests check tests rust-tests check-code format-c \
259 255 update-pot \
260 256 $(packaging_targets) \
261 257 osx
@@ -1,17 +1,15 b''
1 1 [tool.black]
2 2 line-length = 80
3 3 exclude = '''
4 4 build/
5 5 | wheelhouse/
6 6 | dist/
7 7 | packages/
8 8 | \.hg/
9 9 | \.mypy_cache/
10 10 | \.venv/
11 11 | mercurial/thirdparty/
12 | hgext/fsmonitor/pywatchman/
13 12 | contrib/python-zstandard/
14 | contrib/grey.py
15 13 '''
16 14 skip-string-normalization = true
17 15 quiet = true
@@ -1,1262 +1,1266 b''
1 1 # aws.py - Automation code for Amazon Web Services
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 10 import contextlib
11 11 import copy
12 12 import hashlib
13 13 import json
14 14 import os
15 15 import pathlib
16 16 import subprocess
17 17 import time
18 18
19 19 import boto3
20 20 import botocore.exceptions
21 21
22 22 from .linux import BOOTSTRAP_DEBIAN
23 23 from .ssh import (
24 24 exec_command as ssh_exec_command,
25 25 wait_for_ssh,
26 26 )
27 27 from .winrm import (
28 28 run_powershell,
29 29 wait_for_winrm,
30 30 )
31 31
32 32
33 33 SOURCE_ROOT = pathlib.Path(
34 34 os.path.abspath(__file__)
35 35 ).parent.parent.parent.parent
36 36
37 37 INSTALL_WINDOWS_DEPENDENCIES = (
38 38 SOURCE_ROOT / 'contrib' / 'install-windows-dependencies.ps1'
39 39 )
40 40
41 41
42 42 INSTANCE_TYPES_WITH_STORAGE = {
43 43 'c5d',
44 44 'd2',
45 45 'h1',
46 46 'i3',
47 47 'm5ad',
48 48 'm5d',
49 49 'r5d',
50 50 'r5ad',
51 51 'x1',
52 52 'z1d',
53 53 }
54 54
55 55
56 56 AMAZON_ACCOUNT_ID = '801119661308'
57 57 DEBIAN_ACCOUNT_ID = '379101102735'
58 58 DEBIAN_ACCOUNT_ID_2 = '136693071363'
59 59 UBUNTU_ACCOUNT_ID = '099720109477'
60 60
61 61
62 62 WINDOWS_BASE_IMAGE_NAME = 'Windows_Server-2019-English-Full-Base-2019.07.12'
63 63
64 64
65 65 KEY_PAIRS = {
66 66 'automation',
67 67 }
68 68
69 69
70 70 SECURITY_GROUPS = {
71 71 'linux-dev-1': {
72 72 'description': 'Mercurial Linux instances that perform build/test automation',
73 73 'ingress': [
74 74 {
75 75 'FromPort': 22,
76 76 'ToPort': 22,
77 77 'IpProtocol': 'tcp',
78 78 'IpRanges': [
79 79 {
80 80 'CidrIp': '0.0.0.0/0',
81 81 'Description': 'SSH from entire Internet',
82 82 },
83 83 ],
84 84 },
85 85 ],
86 86 },
87 87 'windows-dev-1': {
88 88 'description': 'Mercurial Windows instances that perform build automation',
89 89 'ingress': [
90 90 {
91 91 'FromPort': 22,
92 92 'ToPort': 22,
93 93 'IpProtocol': 'tcp',
94 94 'IpRanges': [
95 95 {
96 96 'CidrIp': '0.0.0.0/0',
97 97 'Description': 'SSH from entire Internet',
98 98 },
99 99 ],
100 100 },
101 101 {
102 102 'FromPort': 3389,
103 103 'ToPort': 3389,
104 104 'IpProtocol': 'tcp',
105 105 'IpRanges': [
106 106 {
107 107 'CidrIp': '0.0.0.0/0',
108 108 'Description': 'RDP from entire Internet',
109 109 },
110 110 ],
111 111 },
112 112 {
113 113 'FromPort': 5985,
114 114 'ToPort': 5986,
115 115 'IpProtocol': 'tcp',
116 116 'IpRanges': [
117 117 {
118 118 'CidrIp': '0.0.0.0/0',
119 119 'Description': 'PowerShell Remoting (Windows Remote Management)',
120 120 },
121 121 ],
122 122 },
123 123 ],
124 124 },
125 125 }
126 126
127 127
128 128 IAM_ROLES = {
129 129 'ephemeral-ec2-role-1': {
130 130 'description': 'Mercurial temporary EC2 instances',
131 131 'policy_arns': [
132 132 'arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM',
133 133 ],
134 134 },
135 135 }
136 136
137 137
138 138 ASSUME_ROLE_POLICY_DOCUMENT = '''
139 139 {
140 140 "Version": "2012-10-17",
141 141 "Statement": [
142 142 {
143 143 "Effect": "Allow",
144 144 "Principal": {
145 145 "Service": "ec2.amazonaws.com"
146 146 },
147 147 "Action": "sts:AssumeRole"
148 148 }
149 149 ]
150 150 }
151 151 '''.strip()
152 152
153 153
154 154 IAM_INSTANCE_PROFILES = {
155 155 'ephemeral-ec2-1': {'roles': ['ephemeral-ec2-role-1',],}
156 156 }
157 157
158 158
159 159 # User Data for Windows EC2 instance. Mainly used to set the password
160 160 # and configure WinRM.
161 161 # Inspired by the User Data script used by Packer
162 162 # (from https://www.packer.io/intro/getting-started/build-image.html).
163 163 WINDOWS_USER_DATA = r'''
164 164 <powershell>
165 165
166 166 # TODO enable this once we figure out what is failing.
167 167 #$ErrorActionPreference = "stop"
168 168
169 169 # Set administrator password
170 170 net user Administrator "%s"
171 171 wmic useraccount where "name='Administrator'" set PasswordExpires=FALSE
172 172
173 173 # First, make sure WinRM can't be connected to
174 174 netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes action=block
175 175
176 176 # Delete any existing WinRM listeners
177 177 winrm delete winrm/config/listener?Address=*+Transport=HTTP 2>$Null
178 178 winrm delete winrm/config/listener?Address=*+Transport=HTTPS 2>$Null
179 179
180 180 # Create a new WinRM listener and configure
181 181 winrm create winrm/config/listener?Address=*+Transport=HTTP
182 182 winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="0"}'
183 183 winrm set winrm/config '@{MaxTimeoutms="7200000"}'
184 184 winrm set winrm/config/service '@{AllowUnencrypted="true"}'
185 185 winrm set winrm/config/service '@{MaxConcurrentOperationsPerUser="12000"}'
186 186 winrm set winrm/config/service/auth '@{Basic="true"}'
187 187 winrm set winrm/config/client/auth '@{Basic="true"}'
188 188
189 189 # Configure UAC to allow privilege elevation in remote shells
190 190 $Key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
191 191 $Setting = 'LocalAccountTokenFilterPolicy'
192 192 Set-ItemProperty -Path $Key -Name $Setting -Value 1 -Force
193 193
194 # Avoid long usernames in the temp directory path because the '~' causes extra quoting in ssh output
195 [System.Environment]::SetEnvironmentVariable('TMP', 'C:\Temp', [System.EnvironmentVariableTarget]::User)
196 [System.Environment]::SetEnvironmentVariable('TEMP', 'C:\Temp', [System.EnvironmentVariableTarget]::User)
197
194 198 # Configure and restart the WinRM Service; Enable the required firewall exception
195 199 Stop-Service -Name WinRM
196 200 Set-Service -Name WinRM -StartupType Automatic
197 201 netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new action=allow localip=any remoteip=any
198 202 Start-Service -Name WinRM
199 203
200 204 # Disable firewall on private network interfaces so prompts don't appear.
201 205 Set-NetFirewallProfile -Name private -Enabled false
202 206 </powershell>
203 207 '''.lstrip()
204 208
205 209
206 210 WINDOWS_BOOTSTRAP_POWERSHELL = '''
207 211 Write-Output "installing PowerShell dependencies"
208 212 Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
209 213 Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
210 214 Install-Module -Name OpenSSHUtils -RequiredVersion 0.0.2.0
211 215
212 216 Write-Output "installing OpenSSL server"
213 217 Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
214 218 # Various tools will attempt to use older versions of .NET. So we enable
215 219 # the feature that provides them so it doesn't have to be auto-enabled
216 220 # later.
217 221 Write-Output "enabling .NET Framework feature"
218 222 Install-WindowsFeature -Name Net-Framework-Core
219 223 '''
220 224
221 225
222 226 class AWSConnection:
223 227 """Manages the state of a connection with AWS."""
224 228
225 229 def __init__(self, automation, region: str, ensure_ec2_state: bool = True):
226 230 self.automation = automation
227 231 self.local_state_path = automation.state_path
228 232
229 233 self.prefix = 'hg-'
230 234
231 235 self.session = boto3.session.Session(region_name=region)
232 236 self.ec2client = self.session.client('ec2')
233 237 self.ec2resource = self.session.resource('ec2')
234 238 self.iamclient = self.session.client('iam')
235 239 self.iamresource = self.session.resource('iam')
236 240 self.security_groups = {}
237 241
238 242 if ensure_ec2_state:
239 243 ensure_key_pairs(automation.state_path, self.ec2resource)
240 244 self.security_groups = ensure_security_groups(self.ec2resource)
241 245 ensure_iam_state(self.iamclient, self.iamresource)
242 246
243 247 def key_pair_path_private(self, name):
244 248 """Path to a key pair private key file."""
245 249 return self.local_state_path / 'keys' / ('keypair-%s' % name)
246 250
247 251 def key_pair_path_public(self, name):
248 252 return self.local_state_path / 'keys' / ('keypair-%s.pub' % name)
249 253
250 254
251 255 def rsa_key_fingerprint(p: pathlib.Path):
252 256 """Compute the fingerprint of an RSA private key."""
253 257
254 258 # TODO use rsa package.
255 259 res = subprocess.run(
256 260 [
257 261 'openssl',
258 262 'pkcs8',
259 263 '-in',
260 264 str(p),
261 265 '-nocrypt',
262 266 '-topk8',
263 267 '-outform',
264 268 'DER',
265 269 ],
266 270 capture_output=True,
267 271 check=True,
268 272 )
269 273
270 274 sha1 = hashlib.sha1(res.stdout).hexdigest()
271 275 return ':'.join(a + b for a, b in zip(sha1[::2], sha1[1::2]))
272 276
273 277
274 278 def ensure_key_pairs(state_path: pathlib.Path, ec2resource, prefix='hg-'):
275 279 remote_existing = {}
276 280
277 281 for kpi in ec2resource.key_pairs.all():
278 282 if kpi.name.startswith(prefix):
279 283 remote_existing[kpi.name[len(prefix) :]] = kpi.key_fingerprint
280 284
281 285 # Validate that we have these keys locally.
282 286 key_path = state_path / 'keys'
283 287 key_path.mkdir(exist_ok=True, mode=0o700)
284 288
285 289 def remove_remote(name):
286 290 print('deleting key pair %s' % name)
287 291 key = ec2resource.KeyPair(name)
288 292 key.delete()
289 293
290 294 def remove_local(name):
291 295 pub_full = key_path / ('keypair-%s.pub' % name)
292 296 priv_full = key_path / ('keypair-%s' % name)
293 297
294 298 print('removing %s' % pub_full)
295 299 pub_full.unlink()
296 300 print('removing %s' % priv_full)
297 301 priv_full.unlink()
298 302
299 303 local_existing = {}
300 304
301 305 for f in sorted(os.listdir(key_path)):
302 306 if not f.startswith('keypair-') or not f.endswith('.pub'):
303 307 continue
304 308
305 309 name = f[len('keypair-') : -len('.pub')]
306 310
307 311 pub_full = key_path / f
308 312 priv_full = key_path / ('keypair-%s' % name)
309 313
310 314 with open(pub_full, 'r', encoding='ascii') as fh:
311 315 data = fh.read()
312 316
313 317 if not data.startswith('ssh-rsa '):
314 318 print(
315 319 'unexpected format for key pair file: %s; removing' % pub_full
316 320 )
317 321 pub_full.unlink()
318 322 priv_full.unlink()
319 323 continue
320 324
321 325 local_existing[name] = rsa_key_fingerprint(priv_full)
322 326
323 327 for name in sorted(set(remote_existing) | set(local_existing)):
324 328 if name not in local_existing:
325 329 actual = '%s%s' % (prefix, name)
326 330 print('remote key %s does not exist locally' % name)
327 331 remove_remote(actual)
328 332 del remote_existing[name]
329 333
330 334 elif name not in remote_existing:
331 335 print('local key %s does not exist remotely' % name)
332 336 remove_local(name)
333 337 del local_existing[name]
334 338
335 339 elif remote_existing[name] != local_existing[name]:
336 340 print(
337 341 'key fingerprint mismatch for %s; '
338 342 'removing from local and remote' % name
339 343 )
340 344 remove_local(name)
341 345 remove_remote('%s%s' % (prefix, name))
342 346 del local_existing[name]
343 347 del remote_existing[name]
344 348
345 349 missing = KEY_PAIRS - set(remote_existing)
346 350
347 351 for name in sorted(missing):
348 352 actual = '%s%s' % (prefix, name)
349 353 print('creating key pair %s' % actual)
350 354
351 355 priv_full = key_path / ('keypair-%s' % name)
352 356 pub_full = key_path / ('keypair-%s.pub' % name)
353 357
354 358 kp = ec2resource.create_key_pair(KeyName=actual)
355 359
356 360 with priv_full.open('w', encoding='ascii') as fh:
357 361 fh.write(kp.key_material)
358 362 fh.write('\n')
359 363
360 364 priv_full.chmod(0o0600)
361 365
362 366 # SSH public key can be extracted via `ssh-keygen`.
363 367 with pub_full.open('w', encoding='ascii') as fh:
364 368 subprocess.run(
365 369 ['ssh-keygen', '-y', '-f', str(priv_full)],
366 370 stdout=fh,
367 371 check=True,
368 372 )
369 373
370 374 pub_full.chmod(0o0600)
371 375
372 376
373 377 def delete_instance_profile(profile):
374 378 for role in profile.roles:
375 379 print(
376 380 'removing role %s from instance profile %s'
377 381 % (role.name, profile.name)
378 382 )
379 383 profile.remove_role(RoleName=role.name)
380 384
381 385 print('deleting instance profile %s' % profile.name)
382 386 profile.delete()
383 387
384 388
385 389 def ensure_iam_state(iamclient, iamresource, prefix='hg-'):
386 390 """Ensure IAM state is in sync with our canonical definition."""
387 391
388 392 remote_profiles = {}
389 393
390 394 for profile in iamresource.instance_profiles.all():
391 395 if profile.name.startswith(prefix):
392 396 remote_profiles[profile.name[len(prefix) :]] = profile
393 397
394 398 for name in sorted(set(remote_profiles) - set(IAM_INSTANCE_PROFILES)):
395 399 delete_instance_profile(remote_profiles[name])
396 400 del remote_profiles[name]
397 401
398 402 remote_roles = {}
399 403
400 404 for role in iamresource.roles.all():
401 405 if role.name.startswith(prefix):
402 406 remote_roles[role.name[len(prefix) :]] = role
403 407
404 408 for name in sorted(set(remote_roles) - set(IAM_ROLES)):
405 409 role = remote_roles[name]
406 410
407 411 print('removing role %s' % role.name)
408 412 role.delete()
409 413 del remote_roles[name]
410 414
411 415 # We've purged remote state that doesn't belong. Create missing
412 416 # instance profiles and roles.
413 417 for name in sorted(set(IAM_INSTANCE_PROFILES) - set(remote_profiles)):
414 418 actual = '%s%s' % (prefix, name)
415 419 print('creating IAM instance profile %s' % actual)
416 420
417 421 profile = iamresource.create_instance_profile(
418 422 InstanceProfileName=actual
419 423 )
420 424 remote_profiles[name] = profile
421 425
422 426 waiter = iamclient.get_waiter('instance_profile_exists')
423 427 waiter.wait(InstanceProfileName=actual)
424 428 print('IAM instance profile %s is available' % actual)
425 429
426 430 for name in sorted(set(IAM_ROLES) - set(remote_roles)):
427 431 entry = IAM_ROLES[name]
428 432
429 433 actual = '%s%s' % (prefix, name)
430 434 print('creating IAM role %s' % actual)
431 435
432 436 role = iamresource.create_role(
433 437 RoleName=actual,
434 438 Description=entry['description'],
435 439 AssumeRolePolicyDocument=ASSUME_ROLE_POLICY_DOCUMENT,
436 440 )
437 441
438 442 waiter = iamclient.get_waiter('role_exists')
439 443 waiter.wait(RoleName=actual)
440 444 print('IAM role %s is available' % actual)
441 445
442 446 remote_roles[name] = role
443 447
444 448 for arn in entry['policy_arns']:
445 449 print('attaching policy %s to %s' % (arn, role.name))
446 450 role.attach_policy(PolicyArn=arn)
447 451
448 452 # Now reconcile state of profiles.
449 453 for name, meta in sorted(IAM_INSTANCE_PROFILES.items()):
450 454 profile = remote_profiles[name]
451 455 wanted = {'%s%s' % (prefix, role) for role in meta['roles']}
452 456 have = {role.name for role in profile.roles}
453 457
454 458 for role in sorted(have - wanted):
455 459 print('removing role %s from %s' % (role, profile.name))
456 460 profile.remove_role(RoleName=role)
457 461
458 462 for role in sorted(wanted - have):
459 463 print('adding role %s to %s' % (role, profile.name))
460 464 profile.add_role(RoleName=role)
461 465
462 466
463 467 def find_image(ec2resource, owner_id, name):
464 468 """Find an AMI by its owner ID and name."""
465 469
466 470 images = ec2resource.images.filter(
467 471 Filters=[
468 472 {'Name': 'owner-id', 'Values': [owner_id],},
469 473 {'Name': 'state', 'Values': ['available'],},
470 474 {'Name': 'image-type', 'Values': ['machine'],},
471 475 {'Name': 'name', 'Values': [name],},
472 476 ]
473 477 )
474 478
475 479 for image in images:
476 480 return image
477 481
478 482 raise Exception('unable to find image for %s' % name)
479 483
480 484
481 485 def ensure_security_groups(ec2resource, prefix='hg-'):
482 486 """Ensure all necessary Mercurial security groups are present.
483 487
484 488 All security groups are prefixed with ``hg-`` by default. Any security
485 489 groups having this prefix but aren't in our list are deleted.
486 490 """
487 491 existing = {}
488 492
489 493 for group in ec2resource.security_groups.all():
490 494 if group.group_name.startswith(prefix):
491 495 existing[group.group_name[len(prefix) :]] = group
492 496
493 497 purge = set(existing) - set(SECURITY_GROUPS)
494 498
495 499 for name in sorted(purge):
496 500 group = existing[name]
497 501 print('removing legacy security group: %s' % group.group_name)
498 502 group.delete()
499 503
500 504 security_groups = {}
501 505
502 506 for name, group in sorted(SECURITY_GROUPS.items()):
503 507 if name in existing:
504 508 security_groups[name] = existing[name]
505 509 continue
506 510
507 511 actual = '%s%s' % (prefix, name)
508 512 print('adding security group %s' % actual)
509 513
510 514 group_res = ec2resource.create_security_group(
511 515 Description=group['description'], GroupName=actual,
512 516 )
513 517
514 518 group_res.authorize_ingress(IpPermissions=group['ingress'],)
515 519
516 520 security_groups[name] = group_res
517 521
518 522 return security_groups
519 523
520 524
521 525 def terminate_ec2_instances(ec2resource, prefix='hg-'):
522 526 """Terminate all EC2 instances managed by us."""
523 527 waiting = []
524 528
525 529 for instance in ec2resource.instances.all():
526 530 if instance.state['Name'] == 'terminated':
527 531 continue
528 532
529 533 for tag in instance.tags or []:
530 534 if tag['Key'] == 'Name' and tag['Value'].startswith(prefix):
531 535 print('terminating %s' % instance.id)
532 536 instance.terminate()
533 537 waiting.append(instance)
534 538
535 539 for instance in waiting:
536 540 instance.wait_until_terminated()
537 541
538 542
539 543 def remove_resources(c, prefix='hg-'):
540 544 """Purge all of our resources in this EC2 region."""
541 545 ec2resource = c.ec2resource
542 546 iamresource = c.iamresource
543 547
544 548 terminate_ec2_instances(ec2resource, prefix=prefix)
545 549
546 550 for image in ec2resource.images.filter(Owners=['self']):
547 551 if image.name.startswith(prefix):
548 552 remove_ami(ec2resource, image)
549 553
550 554 for group in ec2resource.security_groups.all():
551 555 if group.group_name.startswith(prefix):
552 556 print('removing security group %s' % group.group_name)
553 557 group.delete()
554 558
555 559 for profile in iamresource.instance_profiles.all():
556 560 if profile.name.startswith(prefix):
557 561 delete_instance_profile(profile)
558 562
559 563 for role in iamresource.roles.all():
560 564 if role.name.startswith(prefix):
561 565 for p in role.attached_policies.all():
562 566 print('detaching policy %s from %s' % (p.arn, role.name))
563 567 role.detach_policy(PolicyArn=p.arn)
564 568
565 569 print('removing role %s' % role.name)
566 570 role.delete()
567 571
568 572
569 573 def wait_for_ip_addresses(instances):
570 574 """Wait for the public IP addresses of an iterable of instances."""
571 575 for instance in instances:
572 576 while True:
573 577 if not instance.public_ip_address:
574 578 time.sleep(2)
575 579 instance.reload()
576 580 continue
577 581
578 582 print(
579 583 'public IP address for %s: %s'
580 584 % (instance.id, instance.public_ip_address)
581 585 )
582 586 break
583 587
584 588
585 589 def remove_ami(ec2resource, image):
586 590 """Remove an AMI and its underlying snapshots."""
587 591 snapshots = []
588 592
589 593 for device in image.block_device_mappings:
590 594 if 'Ebs' in device:
591 595 snapshots.append(ec2resource.Snapshot(device['Ebs']['SnapshotId']))
592 596
593 597 print('deregistering %s' % image.id)
594 598 image.deregister()
595 599
596 600 for snapshot in snapshots:
597 601 print('deleting snapshot %s' % snapshot.id)
598 602 snapshot.delete()
599 603
600 604
601 605 def wait_for_ssm(ssmclient, instances):
602 606 """Wait for SSM to come online for an iterable of instance IDs."""
603 607 while True:
604 608 res = ssmclient.describe_instance_information(
605 609 Filters=[
606 610 {'Key': 'InstanceIds', 'Values': [i.id for i in instances],},
607 611 ],
608 612 )
609 613
610 614 available = len(res['InstanceInformationList'])
611 615 wanted = len(instances)
612 616
613 617 print('%d/%d instances available in SSM' % (available, wanted))
614 618
615 619 if available == wanted:
616 620 return
617 621
618 622 time.sleep(2)
619 623
620 624
621 625 def run_ssm_command(ssmclient, instances, document_name, parameters):
622 626 """Run a PowerShell script on an EC2 instance."""
623 627
624 628 res = ssmclient.send_command(
625 629 InstanceIds=[i.id for i in instances],
626 630 DocumentName=document_name,
627 631 Parameters=parameters,
628 632 CloudWatchOutputConfig={'CloudWatchOutputEnabled': True,},
629 633 )
630 634
631 635 command_id = res['Command']['CommandId']
632 636
633 637 for instance in instances:
634 638 while True:
635 639 try:
636 640 res = ssmclient.get_command_invocation(
637 641 CommandId=command_id, InstanceId=instance.id,
638 642 )
639 643 except botocore.exceptions.ClientError as e:
640 644 if e.response['Error']['Code'] == 'InvocationDoesNotExist':
641 645 print('could not find SSM command invocation; waiting')
642 646 time.sleep(1)
643 647 continue
644 648 else:
645 649 raise
646 650
647 651 if res['Status'] == 'Success':
648 652 break
649 653 elif res['Status'] in ('Pending', 'InProgress', 'Delayed'):
650 654 time.sleep(2)
651 655 else:
652 656 raise Exception(
653 657 'command failed on %s: %s' % (instance.id, res['Status'])
654 658 )
655 659
656 660
657 661 @contextlib.contextmanager
658 662 def temporary_ec2_instances(ec2resource, config):
659 663 """Create temporary EC2 instances.
660 664
661 665 This is a proxy to ``ec2client.run_instances(**config)`` that takes care of
662 666 managing the lifecycle of the instances.
663 667
664 668 When the context manager exits, the instances are terminated.
665 669
666 670 The context manager evaluates to the list of data structures
667 671 describing each created instance. The instances may not be available
668 672 for work immediately: it is up to the caller to wait for the instance
669 673 to start responding.
670 674 """
671 675
672 676 ids = None
673 677
674 678 try:
675 679 res = ec2resource.create_instances(**config)
676 680
677 681 ids = [i.id for i in res]
678 682 print('started instances: %s' % ' '.join(ids))
679 683
680 684 yield res
681 685 finally:
682 686 if ids:
683 687 print('terminating instances: %s' % ' '.join(ids))
684 688 for instance in res:
685 689 instance.terminate()
686 690 print('terminated %d instances' % len(ids))
687 691
688 692
689 693 @contextlib.contextmanager
690 694 def create_temp_windows_ec2_instances(
691 695 c: AWSConnection, config, bootstrap: bool = False
692 696 ):
693 697 """Create temporary Windows EC2 instances.
694 698
695 699 This is a higher-level wrapper around ``create_temp_ec2_instances()`` that
696 700 configures the Windows instance for Windows Remote Management. The emitted
697 701 instances will have a ``winrm_client`` attribute containing a
698 702 ``pypsrp.client.Client`` instance bound to the instance.
699 703 """
700 704 if 'IamInstanceProfile' in config:
701 705 raise ValueError('IamInstanceProfile cannot be provided in config')
702 706 if 'UserData' in config:
703 707 raise ValueError('UserData cannot be provided in config')
704 708
705 709 password = c.automation.default_password()
706 710
707 711 config = copy.deepcopy(config)
708 712 config['IamInstanceProfile'] = {
709 713 'Name': 'hg-ephemeral-ec2-1',
710 714 }
711 715 config.setdefault('TagSpecifications', []).append(
712 716 {
713 717 'ResourceType': 'instance',
714 718 'Tags': [{'Key': 'Name', 'Value': 'hg-temp-windows'}],
715 719 }
716 720 )
717 721
718 722 if bootstrap:
719 723 config['UserData'] = WINDOWS_USER_DATA % password
720 724
721 725 with temporary_ec2_instances(c.ec2resource, config) as instances:
722 726 wait_for_ip_addresses(instances)
723 727
724 728 print('waiting for Windows Remote Management service...')
725 729
726 730 for instance in instances:
727 731 client = wait_for_winrm(
728 732 instance.public_ip_address, 'Administrator', password
729 733 )
730 734 print('established WinRM connection to %s' % instance.id)
731 735 instance.winrm_client = client
732 736
733 737 yield instances
734 738
735 739
736 740 def resolve_fingerprint(fingerprint):
737 741 fingerprint = json.dumps(fingerprint, sort_keys=True)
738 742 return hashlib.sha256(fingerprint.encode('utf-8')).hexdigest()
739 743
740 744
741 745 def find_and_reconcile_image(ec2resource, name, fingerprint):
742 746 """Attempt to find an existing EC2 AMI with a name and fingerprint.
743 747
744 748 If an image with the specified fingerprint is found, it is returned.
745 749 Otherwise None is returned.
746 750
747 751 Existing images for the specified name that don't have the specified
748 752 fingerprint or are missing required metadata or deleted.
749 753 """
750 754 # Find existing AMIs with this name and delete the ones that are invalid.
751 755 # Store a reference to a good image so it can be returned one the
752 756 # image state is reconciled.
753 757 images = ec2resource.images.filter(
754 758 Filters=[{'Name': 'name', 'Values': [name]}]
755 759 )
756 760
757 761 existing_image = None
758 762
759 763 for image in images:
760 764 if image.tags is None:
761 765 print(
762 766 'image %s for %s lacks required tags; removing'
763 767 % (image.id, image.name)
764 768 )
765 769 remove_ami(ec2resource, image)
766 770 else:
767 771 tags = {t['Key']: t['Value'] for t in image.tags}
768 772
769 773 if tags.get('HGIMAGEFINGERPRINT') == fingerprint:
770 774 existing_image = image
771 775 else:
772 776 print(
773 777 'image %s for %s has wrong fingerprint; removing'
774 778 % (image.id, image.name)
775 779 )
776 780 remove_ami(ec2resource, image)
777 781
778 782 return existing_image
779 783
780 784
781 785 def create_ami_from_instance(
782 786 ec2client, instance, name, description, fingerprint
783 787 ):
784 788 """Create an AMI from a running instance.
785 789
786 790 Returns the ``ec2resource.Image`` representing the created AMI.
787 791 """
788 792 instance.stop()
789 793
790 794 ec2client.get_waiter('instance_stopped').wait(
791 795 InstanceIds=[instance.id], WaiterConfig={'Delay': 5,}
792 796 )
793 797 print('%s is stopped' % instance.id)
794 798
795 799 image = instance.create_image(Name=name, Description=description,)
796 800
797 801 image.create_tags(
798 802 Tags=[{'Key': 'HGIMAGEFINGERPRINT', 'Value': fingerprint,},]
799 803 )
800 804
801 805 print('waiting for image %s' % image.id)
802 806
803 807 ec2client.get_waiter('image_available').wait(ImageIds=[image.id],)
804 808
805 809 print('image %s available as %s' % (image.id, image.name))
806 810
807 811 return image
808 812
809 813
810 814 def ensure_linux_dev_ami(c: AWSConnection, distro='debian10', prefix='hg-'):
811 815 """Ensures a Linux development AMI is available and up-to-date.
812 816
813 817 Returns an ``ec2.Image`` of either an existing AMI or a newly-built one.
814 818 """
815 819 ec2client = c.ec2client
816 820 ec2resource = c.ec2resource
817 821
818 822 name = '%s%s-%s' % (prefix, 'linux-dev', distro)
819 823
820 824 if distro == 'debian9':
821 825 image = find_image(
822 826 ec2resource,
823 827 DEBIAN_ACCOUNT_ID,
824 828 'debian-stretch-hvm-x86_64-gp2-2019-09-08-17994',
825 829 )
826 830 ssh_username = 'admin'
827 831 elif distro == 'debian10':
828 832 image = find_image(
829 833 ec2resource, DEBIAN_ACCOUNT_ID_2, 'debian-10-amd64-20190909-10',
830 834 )
831 835 ssh_username = 'admin'
832 836 elif distro == 'ubuntu18.04':
833 837 image = find_image(
834 838 ec2resource,
835 839 UBUNTU_ACCOUNT_ID,
836 840 'ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-20190918',
837 841 )
838 842 ssh_username = 'ubuntu'
839 843 elif distro == 'ubuntu19.04':
840 844 image = find_image(
841 845 ec2resource,
842 846 UBUNTU_ACCOUNT_ID,
843 847 'ubuntu/images/hvm-ssd/ubuntu-disco-19.04-amd64-server-20190918',
844 848 )
845 849 ssh_username = 'ubuntu'
846 850 else:
847 851 raise ValueError('unsupported Linux distro: %s' % distro)
848 852
849 853 config = {
850 854 'BlockDeviceMappings': [
851 855 {
852 856 'DeviceName': image.block_device_mappings[0]['DeviceName'],
853 857 'Ebs': {
854 858 'DeleteOnTermination': True,
855 859 'VolumeSize': 10,
856 860 'VolumeType': 'gp2',
857 861 },
858 862 },
859 863 ],
860 864 'EbsOptimized': True,
861 865 'ImageId': image.id,
862 866 'InstanceInitiatedShutdownBehavior': 'stop',
863 867 # 8 VCPUs for compiling Python.
864 868 'InstanceType': 't3.2xlarge',
865 869 'KeyName': '%sautomation' % prefix,
866 870 'MaxCount': 1,
867 871 'MinCount': 1,
868 872 'SecurityGroupIds': [c.security_groups['linux-dev-1'].id],
869 873 }
870 874
871 875 requirements2_path = (
872 876 pathlib.Path(__file__).parent.parent / 'linux-requirements-py2.txt'
873 877 )
874 878 requirements3_path = (
875 879 pathlib.Path(__file__).parent.parent / 'linux-requirements-py3.txt'
876 880 )
877 881 with requirements2_path.open('r', encoding='utf-8') as fh:
878 882 requirements2 = fh.read()
879 883 with requirements3_path.open('r', encoding='utf-8') as fh:
880 884 requirements3 = fh.read()
881 885
882 886 # Compute a deterministic fingerprint to determine whether image needs to
883 887 # be regenerated.
884 888 fingerprint = resolve_fingerprint(
885 889 {
886 890 'instance_config': config,
887 891 'bootstrap_script': BOOTSTRAP_DEBIAN,
888 892 'requirements_py2': requirements2,
889 893 'requirements_py3': requirements3,
890 894 }
891 895 )
892 896
893 897 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint)
894 898
895 899 if existing_image:
896 900 return existing_image
897 901
898 902 print('no suitable %s image found; creating one...' % name)
899 903
900 904 with temporary_ec2_instances(ec2resource, config) as instances:
901 905 wait_for_ip_addresses(instances)
902 906
903 907 instance = instances[0]
904 908
905 909 client = wait_for_ssh(
906 910 instance.public_ip_address,
907 911 22,
908 912 username=ssh_username,
909 913 key_filename=str(c.key_pair_path_private('automation')),
910 914 )
911 915
912 916 home = '/home/%s' % ssh_username
913 917
914 918 with client:
915 919 print('connecting to SSH server')
916 920 sftp = client.open_sftp()
917 921
918 922 print('uploading bootstrap files')
919 923 with sftp.open('%s/bootstrap' % home, 'wb') as fh:
920 924 fh.write(BOOTSTRAP_DEBIAN)
921 925 fh.chmod(0o0700)
922 926
923 927 with sftp.open('%s/requirements-py2.txt' % home, 'wb') as fh:
924 928 fh.write(requirements2)
925 929 fh.chmod(0o0700)
926 930
927 931 with sftp.open('%s/requirements-py3.txt' % home, 'wb') as fh:
928 932 fh.write(requirements3)
929 933 fh.chmod(0o0700)
930 934
931 935 print('executing bootstrap')
932 936 chan, stdin, stdout = ssh_exec_command(
933 937 client, '%s/bootstrap' % home
934 938 )
935 939 stdin.close()
936 940
937 941 for line in stdout:
938 942 print(line, end='')
939 943
940 944 res = chan.recv_exit_status()
941 945 if res:
942 946 raise Exception('non-0 exit from bootstrap: %d' % res)
943 947
944 948 print(
945 949 'bootstrap completed; stopping %s to create %s'
946 950 % (instance.id, name)
947 951 )
948 952
949 953 return create_ami_from_instance(
950 954 ec2client,
951 955 instance,
952 956 name,
953 957 'Mercurial Linux development environment',
954 958 fingerprint,
955 959 )
956 960
957 961
958 962 @contextlib.contextmanager
959 963 def temporary_linux_dev_instances(
960 964 c: AWSConnection,
961 965 image,
962 966 instance_type,
963 967 prefix='hg-',
964 968 ensure_extra_volume=False,
965 969 ):
966 970 """Create temporary Linux development EC2 instances.
967 971
968 972 Context manager resolves to a list of ``ec2.Instance`` that were created
969 973 and are running.
970 974
971 975 ``ensure_extra_volume`` can be set to ``True`` to require that instances
972 976 have a 2nd storage volume available other than the primary AMI volume.
973 977 For instance types with instance storage, this does nothing special.
974 978 But for instance types without instance storage, an additional EBS volume
975 979 will be added to the instance.
976 980
977 981 Instances have an ``ssh_client`` attribute containing a paramiko SSHClient
978 982 instance bound to the instance.
979 983
980 984 Instances have an ``ssh_private_key_path`` attributing containing the
981 985 str path to the SSH private key to connect to the instance.
982 986 """
983 987
984 988 block_device_mappings = [
985 989 {
986 990 'DeviceName': image.block_device_mappings[0]['DeviceName'],
987 991 'Ebs': {
988 992 'DeleteOnTermination': True,
989 993 'VolumeSize': 12,
990 994 'VolumeType': 'gp2',
991 995 },
992 996 }
993 997 ]
994 998
995 999 # This is not an exhaustive list of instance types having instance storage.
996 1000 # But
997 1001 if ensure_extra_volume and not instance_type.startswith(
998 1002 tuple(INSTANCE_TYPES_WITH_STORAGE)
999 1003 ):
1000 1004 main_device = block_device_mappings[0]['DeviceName']
1001 1005
1002 1006 if main_device == 'xvda':
1003 1007 second_device = 'xvdb'
1004 1008 elif main_device == '/dev/sda1':
1005 1009 second_device = '/dev/sdb'
1006 1010 else:
1007 1011 raise ValueError(
1008 1012 'unhandled primary EBS device name: %s' % main_device
1009 1013 )
1010 1014
1011 1015 block_device_mappings.append(
1012 1016 {
1013 1017 'DeviceName': second_device,
1014 1018 'Ebs': {
1015 1019 'DeleteOnTermination': True,
1016 1020 'VolumeSize': 8,
1017 1021 'VolumeType': 'gp2',
1018 1022 },
1019 1023 }
1020 1024 )
1021 1025
1022 1026 config = {
1023 1027 'BlockDeviceMappings': block_device_mappings,
1024 1028 'EbsOptimized': True,
1025 1029 'ImageId': image.id,
1026 1030 'InstanceInitiatedShutdownBehavior': 'terminate',
1027 1031 'InstanceType': instance_type,
1028 1032 'KeyName': '%sautomation' % prefix,
1029 1033 'MaxCount': 1,
1030 1034 'MinCount': 1,
1031 1035 'SecurityGroupIds': [c.security_groups['linux-dev-1'].id],
1032 1036 }
1033 1037
1034 1038 with temporary_ec2_instances(c.ec2resource, config) as instances:
1035 1039 wait_for_ip_addresses(instances)
1036 1040
1037 1041 ssh_private_key_path = str(c.key_pair_path_private('automation'))
1038 1042
1039 1043 for instance in instances:
1040 1044 client = wait_for_ssh(
1041 1045 instance.public_ip_address,
1042 1046 22,
1043 1047 username='hg',
1044 1048 key_filename=ssh_private_key_path,
1045 1049 )
1046 1050
1047 1051 instance.ssh_client = client
1048 1052 instance.ssh_private_key_path = ssh_private_key_path
1049 1053
1050 1054 try:
1051 1055 yield instances
1052 1056 finally:
1053 1057 for instance in instances:
1054 1058 instance.ssh_client.close()
1055 1059
1056 1060
1057 1061 def ensure_windows_dev_ami(
1058 1062 c: AWSConnection, prefix='hg-', base_image_name=WINDOWS_BASE_IMAGE_NAME
1059 1063 ):
1060 1064 """Ensure Windows Development AMI is available and up-to-date.
1061 1065
1062 1066 If necessary, a modern AMI will be built by starting a temporary EC2
1063 1067 instance and bootstrapping it.
1064 1068
1065 1069 Obsolete AMIs will be deleted so there is only a single AMI having the
1066 1070 desired name.
1067 1071
1068 1072 Returns an ``ec2.Image`` of either an existing AMI or a newly-built
1069 1073 one.
1070 1074 """
1071 1075 ec2client = c.ec2client
1072 1076 ec2resource = c.ec2resource
1073 1077 ssmclient = c.session.client('ssm')
1074 1078
1075 1079 name = '%s%s' % (prefix, 'windows-dev')
1076 1080
1077 1081 image = find_image(ec2resource, AMAZON_ACCOUNT_ID, base_image_name)
1078 1082
1079 1083 config = {
1080 1084 'BlockDeviceMappings': [
1081 1085 {
1082 1086 'DeviceName': '/dev/sda1',
1083 1087 'Ebs': {
1084 1088 'DeleteOnTermination': True,
1085 1089 'VolumeSize': 32,
1086 1090 'VolumeType': 'gp2',
1087 1091 },
1088 1092 }
1089 1093 ],
1090 1094 'ImageId': image.id,
1091 1095 'InstanceInitiatedShutdownBehavior': 'stop',
1092 1096 'InstanceType': 't3.medium',
1093 1097 'KeyName': '%sautomation' % prefix,
1094 1098 'MaxCount': 1,
1095 1099 'MinCount': 1,
1096 1100 'SecurityGroupIds': [c.security_groups['windows-dev-1'].id],
1097 1101 }
1098 1102
1099 1103 commands = [
1100 1104 # Need to start the service so sshd_config is generated.
1101 1105 'Start-Service sshd',
1102 1106 'Write-Output "modifying sshd_config"',
1103 1107 r'$content = Get-Content C:\ProgramData\ssh\sshd_config',
1104 1108 '$content = $content -replace "Match Group administrators","" -replace "AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys",""',
1105 1109 r'$content | Set-Content C:\ProgramData\ssh\sshd_config',
1106 1110 'Import-Module OpenSSHUtils',
1107 1111 r'Repair-SshdConfigPermission C:\ProgramData\ssh\sshd_config -Confirm:$false',
1108 1112 'Restart-Service sshd',
1109 1113 'Write-Output "installing OpenSSL client"',
1110 1114 'Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0',
1111 1115 'Set-Service -Name sshd -StartupType "Automatic"',
1112 1116 'Write-Output "OpenSSH server running"',
1113 1117 ]
1114 1118
1115 1119 with INSTALL_WINDOWS_DEPENDENCIES.open('r', encoding='utf-8') as fh:
1116 1120 commands.extend(l.rstrip() for l in fh)
1117 1121
1118 1122 # Schedule run of EC2Launch on next boot. This ensures that UserData
1119 1123 # is executed.
1120 1124 # We disable setComputerName because it forces a reboot.
1121 1125 # We set an explicit admin password because this causes UserData to run
1122 1126 # as Administrator instead of System.
1123 1127 commands.extend(
1124 1128 [
1125 1129 r'''Set-Content -Path C:\ProgramData\Amazon\EC2-Windows\Launch\Config\LaunchConfig.json '''
1126 1130 r'''-Value '{"setComputerName": false, "setWallpaper": true, "addDnsSuffixList": true, '''
1127 1131 r'''"extendBootVolumeSize": true, "handleUserData": true, '''
1128 1132 r'''"adminPasswordType": "Specify", "adminPassword": "%s"}' '''
1129 1133 % c.automation.default_password(),
1130 1134 r'C:\ProgramData\Amazon\EC2-Windows\Launch\Scripts\InitializeInstance.ps1 '
1131 1135 r'–Schedule',
1132 1136 ]
1133 1137 )
1134 1138
1135 1139 # Disable Windows Defender when bootstrapping because it just slows
1136 1140 # things down.
1137 1141 commands.insert(0, 'Set-MpPreference -DisableRealtimeMonitoring $true')
1138 1142 commands.append('Set-MpPreference -DisableRealtimeMonitoring $false')
1139 1143
1140 1144 # Compute a deterministic fingerprint to determine whether image needs
1141 1145 # to be regenerated.
1142 1146 fingerprint = resolve_fingerprint(
1143 1147 {
1144 1148 'instance_config': config,
1145 1149 'user_data': WINDOWS_USER_DATA,
1146 1150 'initial_bootstrap': WINDOWS_BOOTSTRAP_POWERSHELL,
1147 1151 'bootstrap_commands': commands,
1148 1152 'base_image_name': base_image_name,
1149 1153 }
1150 1154 )
1151 1155
1152 1156 existing_image = find_and_reconcile_image(ec2resource, name, fingerprint)
1153 1157
1154 1158 if existing_image:
1155 1159 return existing_image
1156 1160
1157 1161 print('no suitable Windows development image found; creating one...')
1158 1162
1159 1163 with create_temp_windows_ec2_instances(
1160 1164 c, config, bootstrap=True
1161 1165 ) as instances:
1162 1166 assert len(instances) == 1
1163 1167 instance = instances[0]
1164 1168
1165 1169 wait_for_ssm(ssmclient, [instance])
1166 1170
1167 1171 # On first boot, install various Windows updates.
1168 1172 # We would ideally use PowerShell Remoting for this. However, there are
1169 1173 # trust issues that make it difficult to invoke Windows Update
1170 1174 # remotely. So we use SSM, which has a mechanism for running Windows
1171 1175 # Update.
1172 1176 print('installing Windows features...')
1173 1177 run_ssm_command(
1174 1178 ssmclient,
1175 1179 [instance],
1176 1180 'AWS-RunPowerShellScript',
1177 1181 {'commands': WINDOWS_BOOTSTRAP_POWERSHELL.split('\n'),},
1178 1182 )
1179 1183
1180 1184 # Reboot so all updates are fully applied.
1181 1185 #
1182 1186 # We don't use instance.reboot() here because it is asynchronous and
1183 1187 # we don't know when exactly the instance has rebooted. It could take
1184 1188 # a while to stop and we may start trying to interact with the instance
1185 1189 # before it has rebooted.
1186 1190 print('rebooting instance %s' % instance.id)
1187 1191 instance.stop()
1188 1192 ec2client.get_waiter('instance_stopped').wait(
1189 1193 InstanceIds=[instance.id], WaiterConfig={'Delay': 5,}
1190 1194 )
1191 1195
1192 1196 instance.start()
1193 1197 wait_for_ip_addresses([instance])
1194 1198
1195 1199 # There is a race condition here between the User Data PS script running
1196 1200 # and us connecting to WinRM. This can manifest as
1197 1201 # "AuthorizationManager check failed" failures during run_powershell().
1198 1202 # TODO figure out a workaround.
1199 1203
1200 1204 print('waiting for Windows Remote Management to come back...')
1201 1205 client = wait_for_winrm(
1202 1206 instance.public_ip_address,
1203 1207 'Administrator',
1204 1208 c.automation.default_password(),
1205 1209 )
1206 1210 print('established WinRM connection to %s' % instance.id)
1207 1211 instance.winrm_client = client
1208 1212
1209 1213 print('bootstrapping instance...')
1210 1214 run_powershell(instance.winrm_client, '\n'.join(commands))
1211 1215
1212 1216 print('bootstrap completed; stopping %s to create image' % instance.id)
1213 1217 return create_ami_from_instance(
1214 1218 ec2client,
1215 1219 instance,
1216 1220 name,
1217 1221 'Mercurial Windows development environment',
1218 1222 fingerprint,
1219 1223 )
1220 1224
1221 1225
1222 1226 @contextlib.contextmanager
1223 1227 def temporary_windows_dev_instances(
1224 1228 c: AWSConnection,
1225 1229 image,
1226 1230 instance_type,
1227 1231 prefix='hg-',
1228 1232 disable_antivirus=False,
1229 1233 ):
1230 1234 """Create a temporary Windows development EC2 instance.
1231 1235
1232 1236 Context manager resolves to the list of ``EC2.Instance`` that were created.
1233 1237 """
1234 1238 config = {
1235 1239 'BlockDeviceMappings': [
1236 1240 {
1237 1241 'DeviceName': '/dev/sda1',
1238 1242 'Ebs': {
1239 1243 'DeleteOnTermination': True,
1240 1244 'VolumeSize': 32,
1241 1245 'VolumeType': 'gp2',
1242 1246 },
1243 1247 }
1244 1248 ],
1245 1249 'ImageId': image.id,
1246 1250 'InstanceInitiatedShutdownBehavior': 'stop',
1247 1251 'InstanceType': instance_type,
1248 1252 'KeyName': '%sautomation' % prefix,
1249 1253 'MaxCount': 1,
1250 1254 'MinCount': 1,
1251 1255 'SecurityGroupIds': [c.security_groups['windows-dev-1'].id],
1252 1256 }
1253 1257
1254 1258 with create_temp_windows_ec2_instances(c, config) as instances:
1255 1259 if disable_antivirus:
1256 1260 for instance in instances:
1257 1261 run_powershell(
1258 1262 instance.winrm_client,
1259 1263 'Set-MpPreference -DisableRealtimeMonitoring $true',
1260 1264 )
1261 1265
1262 1266 yield instances
@@ -1,594 +1,595 b''
1 1 # linux.py - Linux specific automation functionality
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # no-check-code because Python 3 native.
9 9
10 10 import os
11 11 import pathlib
12 12 import shlex
13 13 import subprocess
14 14 import tempfile
15 15
16 16 from .ssh import exec_command
17 17
18 18
19 19 # Linux distributions that are supported.
20 20 DISTROS = {
21 21 'debian9',
22 22 'debian10',
23 23 'ubuntu18.04',
24 24 'ubuntu19.04',
25 25 }
26 26
27 27 INSTALL_PYTHONS = r'''
28 PYENV2_VERSIONS="2.7.16 pypy2.7-7.1.1"
29 PYENV3_VERSIONS="3.5.7 3.6.9 3.7.4 3.8.0 pypy3.5-7.0.0 pypy3.6-7.1.1"
28 PYENV2_VERSIONS="2.7.17 pypy2.7-7.2.0"
29 PYENV3_VERSIONS="3.5.7 3.6.9 3.7.5 3.8.0 pypy3.5-7.0.0 pypy3.6-7.2.0"
30 30
31 31 git clone https://github.com/pyenv/pyenv.git /hgdev/pyenv
32 32 pushd /hgdev/pyenv
33 git checkout d6d6bc8bb08bcdcbf4eb79509aa7061011ade1c4
33 git checkout 0e7cfc3b3d4eca46ad83d632e1505f5932cd179b
34 34 popd
35 35
36 36 export PYENV_ROOT="/hgdev/pyenv"
37 37 export PATH="$PYENV_ROOT/bin:$PATH"
38 38
39 39 # pip 19.2.3.
40 40 PIP_SHA256=57e3643ff19f018f8a00dfaa6b7e4620e3c1a7a2171fd218425366ec006b3bfe
41 41 wget -O get-pip.py --progress dot:mega https://github.com/pypa/get-pip/raw/309a56c5fd94bd1134053a541cb4657a4e47e09d/get-pip.py
42 42 echo "${PIP_SHA256} get-pip.py" | sha256sum --check -
43 43
44 44 VIRTUALENV_SHA256=f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2
45 45 VIRTUALENV_TARBALL=virtualenv-16.7.5.tar.gz
46 46 wget -O ${VIRTUALENV_TARBALL} --progress dot:mega https://files.pythonhosted.org/packages/66/f0/6867af06d2e2f511e4e1d7094ff663acdebc4f15d4a0cb0fed1007395124/${VIRTUALENV_TARBALL}
47 47 echo "${VIRTUALENV_SHA256} ${VIRTUALENV_TARBALL}" | sha256sum --check -
48 48
49 49 for v in ${PYENV2_VERSIONS}; do
50 50 pyenv install -v ${v}
51 51 ${PYENV_ROOT}/versions/${v}/bin/python get-pip.py
52 52 ${PYENV_ROOT}/versions/${v}/bin/pip install ${VIRTUALENV_TARBALL}
53 53 ${PYENV_ROOT}/versions/${v}/bin/pip install -r /hgdev/requirements-py2.txt
54 54 done
55 55
56 56 for v in ${PYENV3_VERSIONS}; do
57 57 pyenv install -v ${v}
58 58 ${PYENV_ROOT}/versions/${v}/bin/python get-pip.py
59 59 ${PYENV_ROOT}/versions/${v}/bin/pip install -r /hgdev/requirements-py3.txt
60 60 done
61 61
62 62 pyenv global ${PYENV2_VERSIONS} ${PYENV3_VERSIONS} system
63 63 '''.lstrip().replace(
64 64 '\r\n', '\n'
65 65 )
66 66
67 67
68 68 INSTALL_RUST = r'''
69 69 RUSTUP_INIT_SHA256=a46fe67199b7bcbbde2dcbc23ae08db6f29883e260e23899a88b9073effc9076
70 70 wget -O rustup-init --progress dot:mega https://static.rust-lang.org/rustup/archive/1.18.3/x86_64-unknown-linux-gnu/rustup-init
71 71 echo "${RUSTUP_INIT_SHA256} rustup-init" | sha256sum --check -
72 72
73 73 chmod +x rustup-init
74 74 sudo -H -u hg -g hg ./rustup-init -y
75 75 sudo -H -u hg -g hg /home/hg/.cargo/bin/rustup install 1.31.1 1.34.2
76 76 sudo -H -u hg -g hg /home/hg/.cargo/bin/rustup component add clippy
77 77 '''
78 78
79 79
80 80 BOOTSTRAP_VIRTUALENV = r'''
81 81 /usr/bin/virtualenv /hgdev/venv-bootstrap
82 82
83 83 HG_SHA256=35fc8ba5e0379c1b3affa2757e83fb0509e8ac314cbd9f1fd133cf265d16e49f
84 84 HG_TARBALL=mercurial-5.1.1.tar.gz
85 85
86 86 wget -O ${HG_TARBALL} --progress dot:mega https://www.mercurial-scm.org/release/${HG_TARBALL}
87 87 echo "${HG_SHA256} ${HG_TARBALL}" | sha256sum --check -
88 88
89 89 /hgdev/venv-bootstrap/bin/pip install ${HG_TARBALL}
90 90 '''.lstrip().replace(
91 91 '\r\n', '\n'
92 92 )
93 93
94 94
95 95 BOOTSTRAP_DEBIAN = (
96 96 r'''
97 97 #!/bin/bash
98 98
99 99 set -ex
100 100
101 101 DISTRO=`grep DISTRIB_ID /etc/lsb-release | awk -F= '{{print $2}}'`
102 102 DEBIAN_VERSION=`cat /etc/debian_version`
103 103 LSB_RELEASE=`lsb_release -cs`
104 104
105 105 sudo /usr/sbin/groupadd hg
106 106 sudo /usr/sbin/groupadd docker
107 107 sudo /usr/sbin/useradd -g hg -G sudo,docker -d /home/hg -m -s /bin/bash hg
108 108 sudo mkdir /home/hg/.ssh
109 109 sudo cp ~/.ssh/authorized_keys /home/hg/.ssh/authorized_keys
110 110 sudo chown -R hg:hg /home/hg/.ssh
111 111 sudo chmod 700 /home/hg/.ssh
112 112 sudo chmod 600 /home/hg/.ssh/authorized_keys
113 113
114 114 cat << EOF | sudo tee /etc/sudoers.d/90-hg
115 115 hg ALL=(ALL) NOPASSWD:ALL
116 116 EOF
117 117
118 118 sudo apt-get update
119 119 sudo DEBIAN_FRONTEND=noninteractive apt-get -yq dist-upgrade
120 120
121 121 # Install packages necessary to set up Docker Apt repo.
122 122 sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install --no-install-recommends \
123 123 apt-transport-https \
124 124 gnupg
125 125
126 126 cat > docker-apt-key << EOF
127 127 -----BEGIN PGP PUBLIC KEY BLOCK-----
128 128
129 129 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
186 186 YT90qFF93M3v01BbxP+EIY2/9tiIPbrd
187 187 =0YYh
188 188 -----END PGP PUBLIC KEY BLOCK-----
189 189 EOF
190 190
191 191 sudo apt-key add docker-apt-key
192 192
193 193 if [ "$LSB_RELEASE" = "stretch" ]; then
194 194 cat << EOF | sudo tee -a /etc/apt/sources.list
195 195 # Need backports for clang-format-6.0
196 196 deb http://deb.debian.org/debian stretch-backports main
197 197 EOF
198 198 fi
199 199
200 200 if [ "$LSB_RELEASE" = "stretch" -o "$LSB_RELEASE" = "buster" ]; then
201 201 cat << EOF | sudo tee -a /etc/apt/sources.list
202 202 # Sources are useful if we want to compile things locally.
203 203 deb-src http://deb.debian.org/debian $LSB_RELEASE main
204 204 deb-src http://security.debian.org/debian-security $LSB_RELEASE/updates main
205 205 deb-src http://deb.debian.org/debian $LSB_RELEASE-updates main
206 206 deb-src http://deb.debian.org/debian $LSB_RELEASE-backports main
207 207
208 208 deb [arch=amd64] https://download.docker.com/linux/debian $LSB_RELEASE stable
209 209 EOF
210 210
211 211 elif [ "$DISTRO" = "Ubuntu" ]; then
212 212 cat << EOF | sudo tee -a /etc/apt/sources.list
213 213 deb [arch=amd64] https://download.docker.com/linux/ubuntu $LSB_RELEASE stable
214 214 EOF
215 215
216 216 fi
217 217
218 218 sudo apt-get update
219 219
220 220 PACKAGES="\
221 221 awscli \
222 222 btrfs-progs \
223 223 build-essential \
224 224 bzr \
225 225 clang-format-6.0 \
226 226 cvs \
227 227 darcs \
228 228 debhelper \
229 229 devscripts \
230 230 docker-ce \
231 231 dpkg-dev \
232 232 dstat \
233 233 emacs \
234 234 gettext \
235 235 git \
236 236 htop \
237 237 iotop \
238 238 jfsutils \
239 239 libbz2-dev \
240 240 libexpat1-dev \
241 241 libffi-dev \
242 242 libgdbm-dev \
243 243 liblzma-dev \
244 244 libncurses5-dev \
245 245 libnss3-dev \
246 246 libreadline-dev \
247 247 libsqlite3-dev \
248 248 libssl-dev \
249 249 netbase \
250 250 ntfs-3g \
251 251 nvme-cli \
252 252 pyflakes \
253 253 pyflakes3 \
254 254 pylint \
255 255 pylint3 \
256 256 python-all-dev \
257 257 python-dev \
258 258 python-docutils \
259 259 python-fuzzywuzzy \
260 260 python-pygments \
261 261 python-subversion \
262 262 python-vcr \
263 263 python3-boto3 \
264 264 python3-dev \
265 265 python3-docutils \
266 266 python3-fuzzywuzzy \
267 267 python3-pygments \
268 268 python3-vcr \
269 python3-venv \
269 270 rsync \
270 271 sqlite3 \
271 272 subversion \
272 273 tcl-dev \
273 274 tk-dev \
274 275 tla \
275 276 unzip \
276 277 uuid-dev \
277 278 vim \
278 279 virtualenv \
279 280 wget \
280 281 xfsprogs \
281 282 zip \
282 283 zlib1g-dev"
283 284
284 285 if [ "LSB_RELEASE" = "stretch" ]; then
285 286 PACKAGES="$PACKAGES linux-perf"
286 287 elif [ "$DISTRO" = "Ubuntu" ]; then
287 288 PACKAGES="$PACKAGES linux-tools-common"
288 289 fi
289 290
290 291 # Monotone only available in older releases.
291 292 if [ "$LSB_RELEASE" = "stretch" -o "$LSB_RELEASE" = "xenial" ]; then
292 293 PACKAGES="$PACKAGES monotone"
293 294 fi
294 295
295 296 sudo DEBIAN_FRONTEND=noninteractive apt-get -yq install --no-install-recommends $PACKAGES
296 297
297 298 # Create clang-format symlink so test harness finds it.
298 299 sudo update-alternatives --install /usr/bin/clang-format clang-format \
299 300 /usr/bin/clang-format-6.0 1000
300 301
301 302 sudo mkdir /hgdev
302 303 # Will be normalized to hg:hg later.
303 304 sudo chown `whoami` /hgdev
304 305
305 306 {install_rust}
306 307
307 308 cp requirements-py2.txt /hgdev/requirements-py2.txt
308 309 cp requirements-py3.txt /hgdev/requirements-py3.txt
309 310
310 311 # Disable the pip version check because it uses the network and can
311 312 # be annoying.
312 313 cat << EOF | sudo tee -a /etc/pip.conf
313 314 [global]
314 315 disable-pip-version-check = True
315 316 EOF
316 317
317 318 {install_pythons}
318 319 {bootstrap_virtualenv}
319 320
320 321 /hgdev/venv-bootstrap/bin/hg clone https://www.mercurial-scm.org/repo/hg /hgdev/src
321 322
322 323 # Mark the repo as non-publishing.
323 324 cat >> /hgdev/src/.hg/hgrc << EOF
324 325 [phases]
325 326 publish = false
326 327 EOF
327 328
328 329 sudo chown -R hg:hg /hgdev
329 330 '''.lstrip()
330 331 .format(
331 332 install_rust=INSTALL_RUST,
332 333 install_pythons=INSTALL_PYTHONS,
333 334 bootstrap_virtualenv=BOOTSTRAP_VIRTUALENV,
334 335 )
335 336 .replace('\r\n', '\n')
336 337 )
337 338
338 339
339 340 # Prepares /hgdev for operations.
340 341 PREPARE_HGDEV = '''
341 342 #!/bin/bash
342 343
343 344 set -e
344 345
345 346 FS=$1
346 347
347 348 ensure_device() {
348 349 if [ -z "${DEVICE}" ]; then
349 350 echo "could not find block device to format"
350 351 exit 1
351 352 fi
352 353 }
353 354
354 355 # Determine device to partition for extra filesystem.
355 356 # If only 1 volume is present, it will be the root volume and
356 357 # should be /dev/nvme0. If multiple volumes are present, the
357 358 # root volume could be nvme0 or nvme1. Use whichever one doesn't have
358 359 # a partition.
359 360 if [ -e /dev/nvme1n1 ]; then
360 361 if [ -e /dev/nvme0n1p1 ]; then
361 362 DEVICE=/dev/nvme1n1
362 363 else
363 364 DEVICE=/dev/nvme0n1
364 365 fi
365 366 else
366 367 DEVICE=
367 368 fi
368 369
369 370 sudo mkdir /hgwork
370 371
371 372 if [ "${FS}" != "default" -a "${FS}" != "tmpfs" ]; then
372 373 ensure_device
373 374 echo "creating ${FS} filesystem on ${DEVICE}"
374 375 fi
375 376
376 377 if [ "${FS}" = "default" ]; then
377 378 :
378 379
379 380 elif [ "${FS}" = "btrfs" ]; then
380 381 sudo mkfs.btrfs ${DEVICE}
381 382 sudo mount ${DEVICE} /hgwork
382 383
383 384 elif [ "${FS}" = "ext3" ]; then
384 385 # lazy_journal_init speeds up filesystem creation at the expense of
385 386 # integrity if things crash. We are an ephemeral instance, so we don't
386 387 # care about integrity.
387 388 sudo mkfs.ext3 -E lazy_journal_init=1 ${DEVICE}
388 389 sudo mount ${DEVICE} /hgwork
389 390
390 391 elif [ "${FS}" = "ext4" ]; then
391 392 sudo mkfs.ext4 -E lazy_journal_init=1 ${DEVICE}
392 393 sudo mount ${DEVICE} /hgwork
393 394
394 395 elif [ "${FS}" = "jfs" ]; then
395 396 sudo mkfs.jfs ${DEVICE}
396 397 sudo mount ${DEVICE} /hgwork
397 398
398 399 elif [ "${FS}" = "tmpfs" ]; then
399 400 echo "creating tmpfs volume in /hgwork"
400 401 sudo mount -t tmpfs -o size=1024M tmpfs /hgwork
401 402
402 403 elif [ "${FS}" = "xfs" ]; then
403 404 sudo mkfs.xfs ${DEVICE}
404 405 sudo mount ${DEVICE} /hgwork
405 406
406 407 else
407 408 echo "unsupported filesystem: ${FS}"
408 409 exit 1
409 410 fi
410 411
411 412 echo "/hgwork ready"
412 413
413 414 sudo chown hg:hg /hgwork
414 415 mkdir /hgwork/tmp
415 416 chown hg:hg /hgwork/tmp
416 417
417 418 rsync -a /hgdev/src /hgwork/
418 419 '''.lstrip().replace(
419 420 '\r\n', '\n'
420 421 )
421 422
422 423
423 424 HG_UPDATE_CLEAN = '''
424 425 set -ex
425 426
426 427 HG=/hgdev/venv-bootstrap/bin/hg
427 428
428 429 cd /hgwork/src
429 430 ${HG} --config extensions.purge= purge --all
430 431 ${HG} update -C $1
431 432 ${HG} log -r .
432 433 '''.lstrip().replace(
433 434 '\r\n', '\n'
434 435 )
435 436
436 437
437 438 def prepare_exec_environment(ssh_client, filesystem='default'):
438 439 """Prepare an EC2 instance to execute things.
439 440
440 441 The AMI has an ``/hgdev`` bootstrapped with various Python installs
441 442 and a clone of the Mercurial repo.
442 443
443 444 In EC2, EBS volumes launched from snapshots have wonky performance behavior.
444 445 Notably, blocks have to be copied on first access, which makes volume
445 446 I/O extremely slow on fresh volumes.
446 447
447 448 Furthermore, we may want to run operations, tests, etc on alternative
448 449 filesystems so we examine behavior on different filesystems.
449 450
450 451 This function is used to facilitate executing operations on alternate
451 452 volumes.
452 453 """
453 454 sftp = ssh_client.open_sftp()
454 455
455 456 with sftp.open('/hgdev/prepare-hgdev', 'wb') as fh:
456 457 fh.write(PREPARE_HGDEV)
457 458 fh.chmod(0o0777)
458 459
459 460 command = 'sudo /hgdev/prepare-hgdev %s' % filesystem
460 461 chan, stdin, stdout = exec_command(ssh_client, command)
461 462 stdin.close()
462 463
463 464 for line in stdout:
464 465 print(line, end='')
465 466
466 467 res = chan.recv_exit_status()
467 468
468 469 if res:
469 470 raise Exception('non-0 exit code updating working directory; %d' % res)
470 471
471 472
472 473 def synchronize_hg(
473 474 source_path: pathlib.Path, ec2_instance, revision: str = None
474 475 ):
475 476 """Synchronize a local Mercurial source path to remote EC2 instance."""
476 477
477 478 with tempfile.TemporaryDirectory() as temp_dir:
478 479 temp_dir = pathlib.Path(temp_dir)
479 480
480 481 ssh_dir = temp_dir / '.ssh'
481 482 ssh_dir.mkdir()
482 483 ssh_dir.chmod(0o0700)
483 484
484 485 public_ip = ec2_instance.public_ip_address
485 486
486 487 ssh_config = ssh_dir / 'config'
487 488
488 489 with ssh_config.open('w', encoding='utf-8') as fh:
489 490 fh.write('Host %s\n' % public_ip)
490 491 fh.write(' User hg\n')
491 492 fh.write(' StrictHostKeyChecking no\n')
492 493 fh.write(' UserKnownHostsFile %s\n' % (ssh_dir / 'known_hosts'))
493 494 fh.write(' IdentityFile %s\n' % ec2_instance.ssh_private_key_path)
494 495
495 496 if not (source_path / '.hg').is_dir():
496 497 raise Exception(
497 498 '%s is not a Mercurial repository; synchronization '
498 499 'not yet supported' % source_path
499 500 )
500 501
501 502 env = dict(os.environ)
502 503 env['HGPLAIN'] = '1'
503 504 env['HGENCODING'] = 'utf-8'
504 505
505 506 hg_bin = source_path / 'hg'
506 507
507 508 res = subprocess.run(
508 509 ['python2.7', str(hg_bin), 'log', '-r', revision, '-T', '{node}'],
509 510 cwd=str(source_path),
510 511 env=env,
511 512 check=True,
512 513 capture_output=True,
513 514 )
514 515
515 516 full_revision = res.stdout.decode('ascii')
516 517
517 518 args = [
518 519 'python2.7',
519 520 str(hg_bin),
520 521 '--config',
521 522 'ui.ssh=ssh -F %s' % ssh_config,
522 523 '--config',
523 524 'ui.remotecmd=/hgdev/venv-bootstrap/bin/hg',
524 525 # Also ensure .hgtags changes are present so auto version
525 526 # calculation works.
526 527 'push',
527 528 '-f',
528 529 '-r',
529 530 full_revision,
530 531 '-r',
531 532 'file(.hgtags)',
532 533 'ssh://%s//hgwork/src' % public_ip,
533 534 ]
534 535
535 536 res = subprocess.run(args, cwd=str(source_path), env=env)
536 537
537 538 # Allow 1 (no-op) to not trigger error.
538 539 if res.returncode not in (0, 1):
539 540 res.check_returncode()
540 541
541 542 # TODO support synchronizing dirty working directory.
542 543
543 544 sftp = ec2_instance.ssh_client.open_sftp()
544 545
545 546 with sftp.open('/hgdev/hgup', 'wb') as fh:
546 547 fh.write(HG_UPDATE_CLEAN)
547 548 fh.chmod(0o0700)
548 549
549 550 chan, stdin, stdout = exec_command(
550 551 ec2_instance.ssh_client, '/hgdev/hgup %s' % full_revision
551 552 )
552 553 stdin.close()
553 554
554 555 for line in stdout:
555 556 print(line, end='')
556 557
557 558 res = chan.recv_exit_status()
558 559
559 560 if res:
560 561 raise Exception(
561 562 'non-0 exit code updating working directory; %d' % res
562 563 )
563 564
564 565
565 566 def run_tests(ssh_client, python_version, test_flags=None):
566 567 """Run tests on a remote Linux machine via an SSH client."""
567 568 test_flags = test_flags or []
568 569
569 570 print('running tests')
570 571
571 572 if python_version == 'system2':
572 573 python = '/usr/bin/python2'
573 574 elif python_version == 'system3':
574 575 python = '/usr/bin/python3'
575 576 elif python_version.startswith('pypy'):
576 577 python = '/hgdev/pyenv/shims/%s' % python_version
577 578 else:
578 579 python = '/hgdev/pyenv/shims/python%s' % python_version
579 580
580 581 test_flags = ' '.join(shlex.quote(a) for a in test_flags)
581 582
582 583 command = (
583 584 '/bin/sh -c "export TMPDIR=/hgwork/tmp; '
584 585 'cd /hgwork/src/tests && %s run-tests.py %s"' % (python, test_flags)
585 586 )
586 587
587 588 chan, stdin, stdout = exec_command(ssh_client, command)
588 589
589 590 stdin.close()
590 591
591 592 for line in stdout:
592 593 print(line, end='')
593 594
594 595 return chan.recv_exit_status()
@@ -1,149 +1,184 b''
1 1 #
2 2 # This file is autogenerated by pip-compile
3 3 # To update, run:
4 4 #
5 5 # pip-compile --generate-hashes --output-file=contrib/automation/linux-requirements-py3.txt contrib/automation/linux-requirements.txt.in
6 6 #
7 appdirs==1.4.3 \
8 --hash=sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92 \
9 --hash=sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e \
10 # via black
7 11 astroid==2.2.5 \
8 12 --hash=sha256:6560e1e1749f68c64a4b5dee4e091fce798d2f0d84ebe638cf0e0585a343acf4 \
9 13 --hash=sha256:b65db1bbaac9f9f4d190199bb8680af6f6f84fd3769a5ea883df8a91fe68b4c4 \
10 14 # via pylint
15 attrs==19.3.0 \
16 --hash=sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c \
17 --hash=sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72 \
18 # via black
19 black==19.10b0 ; python_version >= "3.6" and platform_python_implementation != "PyPy" \
20 --hash=sha256:1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b \
21 --hash=sha256:c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539
22 click==7.0 \
23 --hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \
24 --hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7 \
25 # via black
11 26 docutils==0.15.2 \
12 27 --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \
13 28 --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \
14 29 --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99
15 30 fuzzywuzzy==0.17.0 \
16 31 --hash=sha256:5ac7c0b3f4658d2743aa17da53a55598144edbc5bee3c6863840636e6926f254 \
17 32 --hash=sha256:6f49de47db00e1c71d40ad16da42284ac357936fa9b66bea1df63fed07122d62
18 33 idna==2.8 \
19 34 --hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \
20 35 --hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c \
21 36 # via yarl
22 37 isort==4.3.21 \
23 38 --hash=sha256:54da7e92468955c4fceacd0c86bd0ec997b0e1ee80d97f67c35a78b719dccab1 \
24 39 --hash=sha256:6e811fcb295968434526407adb8796944f1988c5b65e8139058f2014cbe100fd \
25 40 # via pylint
26 41 lazy-object-proxy==1.4.1 \
27 42 --hash=sha256:159a745e61422217881c4de71f9eafd9d703b93af95618635849fe469a283661 \
28 43 --hash=sha256:23f63c0821cc96a23332e45dfaa83266feff8adc72b9bcaef86c202af765244f \
29 44 --hash=sha256:3b11be575475db2e8a6e11215f5aa95b9ec14de658628776e10d96fa0b4dac13 \
30 45 --hash=sha256:3f447aff8bc61ca8b42b73304f6a44fa0d915487de144652816f950a3f1ab821 \
31 46 --hash=sha256:4ba73f6089cd9b9478bc0a4fa807b47dbdb8fad1d8f31a0f0a5dbf26a4527a71 \
32 47 --hash=sha256:4f53eadd9932055eac465bd3ca1bd610e4d7141e1278012bd1f28646aebc1d0e \
33 48 --hash=sha256:64483bd7154580158ea90de5b8e5e6fc29a16a9b4db24f10193f0c1ae3f9d1ea \
34 49 --hash=sha256:6f72d42b0d04bfee2397aa1862262654b56922c20a9bb66bb76b6f0e5e4f9229 \
35 50 --hash=sha256:7c7f1ec07b227bdc561299fa2328e85000f90179a2f44ea30579d38e037cb3d4 \
36 51 --hash=sha256:7c8b1ba1e15c10b13cad4171cfa77f5bb5ec2580abc5a353907780805ebe158e \
37 52 --hash=sha256:8559b94b823f85342e10d3d9ca4ba5478168e1ac5658a8a2f18c991ba9c52c20 \
38 53 --hash=sha256:a262c7dfb046f00e12a2bdd1bafaed2408114a89ac414b0af8755c696eb3fc16 \
39 54 --hash=sha256:acce4e3267610c4fdb6632b3886fe3f2f7dd641158a843cf6b6a68e4ce81477b \
40 55 --hash=sha256:be089bb6b83fac7f29d357b2dc4cf2b8eb8d98fe9d9ff89f9ea6012970a853c7 \
41 56 --hash=sha256:bfab710d859c779f273cc48fb86af38d6e9210f38287df0069a63e40b45a2f5c \
42 57 --hash=sha256:c10d29019927301d524a22ced72706380de7cfc50f767217485a912b4c8bd82a \
43 58 --hash=sha256:dd6e2b598849b3d7aee2295ac765a578879830fb8966f70be8cd472e6069932e \
44 59 --hash=sha256:e408f1eacc0a68fed0c08da45f31d0ebb38079f043328dce69ff133b95c29dc1 \
45 60 # via astroid
46 61 mccabe==0.6.1 \
47 62 --hash=sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42 \
48 63 --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f \
49 64 # via pylint
50 65 multidict==4.5.2 \
51 66 --hash=sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f \
52 67 --hash=sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3 \
53 68 --hash=sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef \
54 69 --hash=sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b \
55 70 --hash=sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73 \
56 71 --hash=sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc \
57 72 --hash=sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3 \
58 73 --hash=sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd \
59 74 --hash=sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351 \
60 75 --hash=sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941 \
61 76 --hash=sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d \
62 77 --hash=sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1 \
63 78 --hash=sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b \
64 79 --hash=sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a \
65 80 --hash=sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3 \
66 81 --hash=sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7 \
67 82 --hash=sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0 \
68 83 --hash=sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0 \
69 84 --hash=sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014 \
70 85 --hash=sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5 \
71 86 --hash=sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036 \
72 87 --hash=sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d \
73 88 --hash=sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a \
74 89 --hash=sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce \
75 90 --hash=sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1 \
76 91 --hash=sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a \
77 92 --hash=sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9 \
78 93 --hash=sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7 \
79 94 --hash=sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b \
80 95 # via yarl
96 pathspec==0.6.0 \
97 --hash=sha256:e285ccc8b0785beadd4c18e5708b12bb8fcf529a1e61215b3feff1d1e559ea5c \
98 # via black
81 99 pyflakes==2.1.1 \
82 100 --hash=sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0 \
83 101 --hash=sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2
84 102 pygments==2.4.2 \
85 103 --hash=sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127 \
86 104 --hash=sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297
87 105 pylint==2.3.1 \
88 106 --hash=sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09 \
89 107 --hash=sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1
90 108 python-levenshtein==0.12.0 \
91 109 --hash=sha256:033a11de5e3d19ea25c9302d11224e1a1898fe5abd23c61c7c360c25195e3eb1
92 110 pyyaml==5.1.2 \
93 111 --hash=sha256:0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9 \
94 112 --hash=sha256:01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4 \
95 113 --hash=sha256:5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8 \
96 114 --hash=sha256:5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696 \
97 115 --hash=sha256:7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34 \
98 116 --hash=sha256:7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9 \
99 117 --hash=sha256:87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73 \
100 118 --hash=sha256:9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299 \
101 119 --hash=sha256:a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b \
102 120 --hash=sha256:b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae \
103 121 --hash=sha256:b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681 \
104 122 --hash=sha256:bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41 \
105 123 --hash=sha256:f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8 \
106 124 # via vcrpy
125 regex==2019.11.1 \
126 --hash=sha256:15454b37c5a278f46f7aa2d9339bda450c300617ca2fca6558d05d870245edc7 \
127 --hash=sha256:1ad40708c255943a227e778b022c6497c129ad614bb7a2a2f916e12e8a359ee7 \
128 --hash=sha256:5e00f65cc507d13ab4dfa92c1232d004fa202c1d43a32a13940ab8a5afe2fb96 \
129 --hash=sha256:604dc563a02a74d70ae1f55208ddc9bfb6d9f470f6d1a5054c4bd5ae58744ab1 \
130 --hash=sha256:720e34a539a76a1fedcebe4397290604cc2bdf6f81eca44adb9fb2ea071c0c69 \
131 --hash=sha256:7caf47e4a9ac6ef08cabd3442cc4ca3386db141fb3c8b2a7e202d0470028e910 \
132 --hash=sha256:c31eaf28c6fe75ea329add0022efeed249e37861c19681960f99bbc7db981fb2 \
133 --hash=sha256:c7393597191fc2043c744db021643549061e12abe0b3ff5c429d806de7b93b66 \
134 --hash=sha256:d2b302f8cdd82c8f48e9de749d1d17f85ce9a0f082880b9a4859f66b07037dc6 \
135 --hash=sha256:e3d8dd0ec0ea280cf89026b0898971f5750a7bd92cb62c51af5a52abd020054a \
136 --hash=sha256:ec032cbfed59bd5a4b8eab943c310acfaaa81394e14f44454ad5c9eba4f24a74 \
137 # via black
107 138 six==1.12.0 \
108 139 --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
109 140 --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 \
110 141 # via astroid, vcrpy
142 toml==0.10.0 \
143 --hash=sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c \
144 --hash=sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e \
145 # via black
111 146 typed-ast==1.4.0 ; python_version >= "3.0" and platform_python_implementation != "PyPy" \
112 147 --hash=sha256:18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e \
113 148 --hash=sha256:262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e \
114 149 --hash=sha256:2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0 \
115 150 --hash=sha256:354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c \
116 151 --hash=sha256:4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631 \
117 152 --hash=sha256:630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4 \
118 153 --hash=sha256:66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34 \
119 154 --hash=sha256:71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b \
120 155 --hash=sha256:95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a \
121 156 --hash=sha256:bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233 \
122 157 --hash=sha256:cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1 \
123 158 --hash=sha256:d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36 \
124 159 --hash=sha256:d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d \
125 160 --hash=sha256:d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a \
126 161 --hash=sha256:ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12
127 162 vcrpy==2.0.1 \
128 163 --hash=sha256:127e79cf7b569d071d1bd761b83f7b62b2ce2a2eb63ceca7aa67cba8f2602ea3 \
129 164 --hash=sha256:57be64aa8e9883a4117d0b15de28af62275c001abcdb00b6dc2d4406073d9a4f
130 165 wrapt==1.11.2 \
131 166 --hash=sha256:565a021fd19419476b9362b05eeaa094178de64f8361e44468f9e9d7843901e1 \
132 167 # via astroid, vcrpy
133 168 yarl==1.3.0 \
134 169 --hash=sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9 \
135 170 --hash=sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f \
136 171 --hash=sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb \
137 172 --hash=sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320 \
138 173 --hash=sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842 \
139 174 --hash=sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0 \
140 175 --hash=sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829 \
141 176 --hash=sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310 \
142 177 --hash=sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4 \
143 178 --hash=sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8 \
144 179 --hash=sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1 \
145 180 # via vcrpy
146 181
147 182 # WARNING: The following packages were not pinned, but pip requires them to be
148 183 # pinned when the requirements file includes hashes. Consider using the --allow-unsafe flag.
149 # setuptools==41.0.1 # via python-levenshtein
184 # setuptools==41.6.0 # via python-levenshtein
@@ -1,12 +1,14 b''
1 # black pulls in typed-ast, which doesn't install on PyPy.
2 black ; python_version >= '3.6' and platform_python_implementation != 'PyPy'
1 3 # Bazaar doesn't work with Python 3 nor PyPy.
2 4 bzr ; python_version <= '2.7' and platform_python_implementation == 'CPython'
3 5 docutils
4 6 fuzzywuzzy
5 7 pyflakes
6 8 pygments
7 9 pylint
8 10 # Needed to avoid warnings from fuzzywuzzy.
9 11 python-Levenshtein
10 12 # typed-ast dependency doesn't install on PyPy.
11 13 typed-ast ; python_version >= '3.0' and platform_python_implementation != 'PyPy'
12 14 vcrpy
@@ -1,345 +1,345 b''
1 1 #!/usr/bin/env python3
2 2 #
3 3 # byteify-strings.py - transform string literals to be Python 3 safe
4 4 #
5 5 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
6 6 #
7 7 # This software may be used and distributed according to the terms of the
8 8 # GNU General Public License version 2 or any later version.
9 9
10 10 from __future__ import absolute_import, print_function
11 11
12 12 import argparse
13 13 import contextlib
14 14 import errno
15 15 import os
16 16 import sys
17 17 import tempfile
18 18 import token
19 19 import tokenize
20 20
21 21
22 22 def adjusttokenpos(t, ofs):
23 23 """Adjust start/end column of the given token"""
24 24 return t._replace(
25 25 start=(t.start[0], t.start[1] + ofs), end=(t.end[0], t.end[1] + ofs)
26 26 )
27 27
28 28
29 29 def replacetokens(tokens, opts):
30 30 """Transform a stream of tokens from raw to Python 3.
31 31
32 32 Returns a generator of possibly rewritten tokens.
33 33
34 34 The input token list may be mutated as part of processing. However,
35 35 its changes do not necessarily match the output token stream.
36 36 """
37 37 sysstrtokens = set()
38 38
39 39 # The following utility functions access the tokens list and i index of
40 40 # the for i, t enumerate(tokens) loop below
41 41 def _isop(j, *o):
42 42 """Assert that tokens[j] is an OP with one of the given values"""
43 43 try:
44 44 return tokens[j].type == token.OP and tokens[j].string in o
45 45 except IndexError:
46 46 return False
47 47
48 48 def _findargnofcall(n):
49 49 """Find arg n of a call expression (start at 0)
50 50
51 51 Returns index of the first token of that argument, or None if
52 52 there is not that many arguments.
53 53
54 54 Assumes that token[i + 1] is '('.
55 55
56 56 """
57 57 nested = 0
58 58 for j in range(i + 2, len(tokens)):
59 59 if _isop(j, ')', ']', '}'):
60 60 # end of call, tuple, subscription or dict / set
61 61 nested -= 1
62 62 if nested < 0:
63 63 return None
64 64 elif n == 0:
65 65 # this is the starting position of arg
66 66 return j
67 67 elif _isop(j, '(', '[', '{'):
68 68 nested += 1
69 69 elif _isop(j, ',') and nested == 0:
70 70 n -= 1
71 71
72 72 return None
73 73
74 74 def _ensuresysstr(j):
75 75 """Make sure the token at j is a system string
76 76
77 77 Remember the given token so the string transformer won't add
78 78 the byte prefix.
79 79
80 80 Ignores tokens that are not strings. Assumes bounds checking has
81 81 already been done.
82 82
83 83 """
84 84 k = j
85 85 currtoken = tokens[k]
86 86 while currtoken.type in (token.STRING, token.NEWLINE, tokenize.NL):
87 87 k += 1
88 88 if currtoken.type == token.STRING and currtoken.string.startswith(
89 89 ("'", '"')
90 90 ):
91 91 sysstrtokens.add(currtoken)
92 92 try:
93 93 currtoken = tokens[k]
94 94 except IndexError:
95 95 break
96 96
97 97 def _isitemaccess(j):
98 98 """Assert the next tokens form an item access on `tokens[j]` and that
99 99 `tokens[j]` is a name.
100 100 """
101 101 try:
102 102 return (
103 103 tokens[j].type == token.NAME
104 104 and _isop(j + 1, '[')
105 105 and tokens[j + 2].type == token.STRING
106 106 and _isop(j + 3, ']')
107 107 )
108 108 except IndexError:
109 109 return False
110 110
111 111 def _ismethodcall(j, *methodnames):
112 112 """Assert the next tokens form a call to `methodname` with a string
113 113 as first argument on `tokens[j]` and that `tokens[j]` is a name.
114 114 """
115 115 try:
116 116 return (
117 117 tokens[j].type == token.NAME
118 118 and _isop(j + 1, '.')
119 119 and tokens[j + 2].type == token.NAME
120 120 and tokens[j + 2].string in methodnames
121 121 and _isop(j + 3, '(')
122 122 and tokens[j + 4].type == token.STRING
123 123 )
124 124 except IndexError:
125 125 return False
126 126
127 127 coldelta = 0 # column increment for new opening parens
128 128 coloffset = -1 # column offset for the current line (-1: TBD)
129 129 parens = [(0, 0, 0, -1)] # stack of (line, end-column, column-offset, type)
130 130 ignorenextline = False # don't transform the next line
131 131 insideignoreblock = False # don't transform until turned off
132 132 for i, t in enumerate(tokens):
133 133 # Compute the column offset for the current line, such that
134 134 # the current line will be aligned to the last opening paren
135 135 # as before.
136 136 if coloffset < 0:
137 137 lastparen = parens[-1]
138 138 if t.start[1] == lastparen[1]:
139 139 coloffset = lastparen[2]
140 140 elif t.start[1] + 1 == lastparen[1] and lastparen[3] not in (
141 141 token.NEWLINE,
142 142 tokenize.NL,
143 143 ):
144 144 # fix misaligned indent of s/util.Abort/error.Abort/
145 145 coloffset = lastparen[2] + (lastparen[1] - t.start[1])
146 146 else:
147 147 coloffset = 0
148 148
149 149 # Reset per-line attributes at EOL.
150 150 if t.type in (token.NEWLINE, tokenize.NL):
151 151 yield adjusttokenpos(t, coloffset)
152 152 coldelta = 0
153 153 coloffset = -1
154 154 if not insideignoreblock:
155 155 ignorenextline = (
156 156 tokens[i - 1].type == token.COMMENT
157 157 and tokens[i - 1].string == "# no-py3-transform"
158 158 )
159 159 continue
160 160
161 161 if t.type == token.COMMENT:
162 162 if t.string == "# py3-transform: off":
163 163 insideignoreblock = True
164 164 if t.string == "# py3-transform: on":
165 165 insideignoreblock = False
166 166
167 167 if ignorenextline or insideignoreblock:
168 168 yield adjusttokenpos(t, coloffset)
169 169 continue
170 170
171 171 # Remember the last paren position.
172 172 if _isop(i, '(', '[', '{'):
173 173 parens.append(t.end + (coloffset + coldelta, tokens[i + 1].type))
174 174 elif _isop(i, ')', ']', '}'):
175 175 parens.pop()
176 176
177 177 # Convert most string literals to byte literals. String literals
178 178 # in Python 2 are bytes. String literals in Python 3 are unicode.
179 179 # Most strings in Mercurial are bytes and unicode strings are rare.
180 180 # Rather than rewrite all string literals to use ``b''`` to indicate
181 181 # byte strings, we apply this token transformer to insert the ``b``
182 182 # prefix nearly everywhere.
183 183 if t.type == token.STRING and t not in sysstrtokens:
184 184 s = t.string
185 185
186 186 # Preserve docstrings as string literals. This is inconsistent
187 187 # with regular unprefixed strings. However, the
188 188 # "from __future__" parsing (which allows a module docstring to
189 189 # exist before it) doesn't properly handle the docstring if it
190 190 # is b''' prefixed, leading to a SyntaxError. We leave all
191 191 # docstrings as unprefixed to avoid this. This means Mercurial
192 192 # components touching docstrings need to handle unicode,
193 193 # unfortunately.
194 194 if s[0:3] in ("'''", '"""'):
195 195 # If it's assigned to something, it's not a docstring
196 196 if not _isop(i - 1, '='):
197 197 yield adjusttokenpos(t, coloffset)
198 198 continue
199 199
200 200 # If the first character isn't a quote, it is likely a string
201 201 # prefixing character (such as 'b', 'u', or 'r'. Ignore.
202 202 if s[0] not in ("'", '"'):
203 203 yield adjusttokenpos(t, coloffset)
204 204 continue
205 205
206 206 # String literal. Prefix to make a b'' string.
207 207 yield adjusttokenpos(t._replace(string='b%s' % t.string), coloffset)
208 208 coldelta += 1
209 209 continue
210 210
211 211 # This looks like a function call.
212 212 if t.type == token.NAME and _isop(i + 1, '('):
213 213 fn = t.string
214 214
215 215 # *attr() builtins don't accept byte strings to 2nd argument.
216 216 if fn in (
217 217 'getattr',
218 218 'setattr',
219 219 'hasattr',
220 220 'safehasattr',
221 221 'wrapfunction',
222 222 'wrapclass',
223 223 'addattr',
224 224 ) and (opts['allow-attr-methods'] or not _isop(i - 1, '.')):
225 225 arg1idx = _findargnofcall(1)
226 226 if arg1idx is not None:
227 227 _ensuresysstr(arg1idx)
228 228
229 229 # .encode() and .decode() on str/bytes/unicode don't accept
230 230 # byte strings on Python 3.
231 231 elif fn in ('encode', 'decode') and _isop(i - 1, '.'):
232 232 for argn in range(2):
233 233 argidx = _findargnofcall(argn)
234 234 if argidx is not None:
235 235 _ensuresysstr(argidx)
236 236
237 237 # It changes iteritems/values to items/values as they are not
238 238 # present in Python 3 world.
239 239 elif opts['dictiter'] and fn in ('iteritems', 'itervalues'):
240 240 yield adjusttokenpos(t._replace(string=fn[4:]), coloffset)
241 241 continue
242 242
243 243 if t.type == token.NAME and t.string in opts['treat-as-kwargs']:
244 244 if _isitemaccess(i):
245 245 _ensuresysstr(i + 2)
246 246 if _ismethodcall(i, 'get', 'pop', 'setdefault', 'popitem'):
247 247 _ensuresysstr(i + 4)
248 248
249 249 # Looks like "if __name__ == '__main__'".
250 250 if (
251 251 t.type == token.NAME
252 252 and t.string == '__name__'
253 253 and _isop(i + 1, '==')
254 254 ):
255 255 _ensuresysstr(i + 2)
256 256
257 257 # Emit unmodified token.
258 258 yield adjusttokenpos(t, coloffset)
259 259
260 260
261 261 def process(fin, fout, opts):
262 262 tokens = tokenize.tokenize(fin.readline)
263 263 tokens = replacetokens(list(tokens), opts)
264 264 fout.write(tokenize.untokenize(tokens))
265 265
266 266
267 267 def tryunlink(fname):
268 268 try:
269 269 os.unlink(fname)
270 270 except OSError as err:
271 271 if err.errno != errno.ENOENT:
272 272 raise
273 273
274 274
275 275 @contextlib.contextmanager
276 276 def editinplace(fname):
277 277 n = os.path.basename(fname)
278 278 d = os.path.dirname(fname)
279 279 fp = tempfile.NamedTemporaryFile(
280 280 prefix='.%s-' % n, suffix='~', dir=d, delete=False
281 281 )
282 282 try:
283 283 yield fp
284 284 fp.close()
285 285 if os.name == 'nt':
286 286 tryunlink(fname)
287 287 os.rename(fp.name, fname)
288 288 finally:
289 289 fp.close()
290 290 tryunlink(fp.name)
291 291
292 292
293 293 def main():
294 294 ap = argparse.ArgumentParser()
295 295 ap.add_argument(
296 296 '--version', action='version', version='Byteify strings 1.0'
297 297 )
298 298 ap.add_argument(
299 299 '-i',
300 300 '--inplace',
301 301 action='store_true',
302 302 default=False,
303 303 help='edit files in place',
304 304 )
305 305 ap.add_argument(
306 306 '--dictiter',
307 307 action='store_true',
308 308 default=False,
309 309 help='rewrite iteritems() and itervalues()',
310 310 ),
311 311 ap.add_argument(
312 312 '--allow-attr-methods',
313 313 action='store_true',
314 314 default=False,
315 315 help='also handle attr*() when they are methods',
316 316 ),
317 317 ap.add_argument(
318 318 '--treat-as-kwargs',
319 319 nargs="+",
320 320 default=[],
321 321 help="ignore kwargs-like objects",
322 322 ),
323 323 ap.add_argument('files', metavar='FILE', nargs='+', help='source file')
324 324 args = ap.parse_args()
325 325 opts = {
326 326 'dictiter': args.dictiter,
327 327 'treat-as-kwargs': set(args.treat_as_kwargs),
328 328 'allow-attr-methods': args.allow_attr_methods,
329 329 }
330 330 for fname in args.files:
331 331 if args.inplace:
332 332 with editinplace(fname) as fout:
333 333 with open(fname, 'rb') as fin:
334 334 process(fin, fout, opts)
335 335 else:
336 336 with open(fname, 'rb') as fin:
337 337 fout = sys.stdout.buffer
338 338 process(fin, fout, opts)
339 339
340 340
341 341 if __name__ == '__main__':
342 if sys.version_info.major < 3:
343 print('This script must be run under Python 3.')
342 if sys.version_info[0:2] < (3, 7):
343 print('This script must be run under Python 3.7+')
344 344 sys.exit(3)
345 345 main()
@@ -1,15 +1,9 b''
1 1 [fix]
2 2 clang-format:command = clang-format --style file -i
3 3 clang-format:pattern = (**.c or **.cc or **.h) and not "listfile:contrib/clang-format-ignorelist"
4 4
5 5 rustfmt:command = rustfmt {rootpath}
6 6 rustfmt:pattern = set:**.rs
7 7
8 # We use black, but currently with
9 # https://github.com/psf/black/pull/826 applied. For now
10 # contrib/grey.py is our fork of black. You need to pip install
11 # git+https://github.com/python/black/@d9e71a75ccfefa3d9156a64c03313a0d4ad981e5
12 # to have the dependencies for grey.
13 #
14 # black:command = python3.7 contrib/grey.py --config=black.toml -
15 # black:pattern = set:**.py - hgext/fsmonitor/pywatchman/** - mercurial/thirdparty/** - "contrib/python-zstandard/** - contrib/grey.py"
8 black:command = black --config=black.toml -
9 black:pattern = set:**.py - mercurial/thirdparty/** - "contrib/python-zstandard/**"
@@ -1,813 +1,818 b''
1 1 #!/usr/bin/env python
2 2
3 3 from __future__ import absolute_import, print_function
4 4
5 5 import ast
6 6 import collections
7 import io
7 8 import os
8 9 import sys
9 10
10 11 # Import a minimal set of stdlib modules needed for list_stdlib_modules()
11 12 # to work when run from a virtualenv. The modules were chosen empirically
12 13 # so that the return value matches the return value without virtualenv.
13 14 if True: # disable lexical sorting checks
14 15 try:
15 16 import BaseHTTPServer as basehttpserver
16 17 except ImportError:
17 18 basehttpserver = None
18 19 import zlib
19 20
20 21 import testparseutil
21 22
22 23 # Whitelist of modules that symbols can be directly imported from.
23 24 allowsymbolimports = (
24 25 '__future__',
25 26 'bzrlib',
26 27 'hgclient',
27 28 'mercurial',
28 29 'mercurial.hgweb.common',
29 30 'mercurial.hgweb.request',
30 31 'mercurial.i18n',
31 32 'mercurial.interfaces',
32 33 'mercurial.node',
33 34 'mercurial.pycompat',
34 35 # for revlog to re-export constant to extensions
35 36 'mercurial.revlogutils.constants',
36 37 'mercurial.revlogutils.flagutil',
37 38 # for cffi modules to re-export pure functions
38 39 'mercurial.pure.base85',
39 40 'mercurial.pure.bdiff',
40 41 'mercurial.pure.mpatch',
41 42 'mercurial.pure.osutil',
42 43 'mercurial.pure.parsers',
43 44 # third-party imports should be directly imported
44 45 'mercurial.thirdparty',
45 46 'mercurial.thirdparty.attr',
46 47 'mercurial.thirdparty.zope',
47 48 'mercurial.thirdparty.zope.interface',
48 49 )
49 50
50 51 # Whitelist of symbols that can be directly imported.
51 52 directsymbols = ('demandimport',)
52 53
53 54 # Modules that must be aliased because they are commonly confused with
54 55 # common variables and can create aliasing and readability issues.
55 56 requirealias = {
56 57 'ui': 'uimod',
57 58 }
58 59
59 60
60 61 def usingabsolute(root):
61 62 """Whether absolute imports are being used."""
62 63 if sys.version_info[0] >= 3:
63 64 return True
64 65
65 66 for node in ast.walk(root):
66 67 if isinstance(node, ast.ImportFrom):
67 68 if node.module == '__future__':
68 69 for n in node.names:
69 70 if n.name == 'absolute_import':
70 71 return True
71 72
72 73 return False
73 74
74 75
75 76 def walklocal(root):
76 77 """Recursively yield all descendant nodes but not in a different scope"""
77 78 todo = collections.deque(ast.iter_child_nodes(root))
78 79 yield root, False
79 80 while todo:
80 81 node = todo.popleft()
81 82 newscope = isinstance(node, ast.FunctionDef)
82 83 if not newscope:
83 84 todo.extend(ast.iter_child_nodes(node))
84 85 yield node, newscope
85 86
86 87
87 88 def dotted_name_of_path(path):
88 89 """Given a relative path to a source file, return its dotted module name.
89 90
90 91 >>> dotted_name_of_path('mercurial/error.py')
91 92 'mercurial.error'
92 93 >>> dotted_name_of_path('zlibmodule.so')
93 94 'zlib'
94 95 """
95 96 parts = path.replace(os.sep, '/').split('/')
96 97 parts[-1] = parts[-1].split('.', 1)[0] # remove .py and .so and .ARCH.so
97 98 if parts[-1].endswith('module'):
98 99 parts[-1] = parts[-1][:-6]
99 100 return '.'.join(parts)
100 101
101 102
102 103 def fromlocalfunc(modulename, localmods):
103 104 """Get a function to examine which locally defined module the
104 105 target source imports via a specified name.
105 106
106 107 `modulename` is an `dotted_name_of_path()`-ed source file path,
107 108 which may have `.__init__` at the end of it, of the target source.
108 109
109 110 `localmods` is a set of absolute `dotted_name_of_path()`-ed source file
110 111 paths of locally defined (= Mercurial specific) modules.
111 112
112 113 This function assumes that module names not existing in
113 114 `localmods` are from the Python standard library.
114 115
115 116 This function returns the function, which takes `name` argument,
116 117 and returns `(absname, dottedpath, hassubmod)` tuple if `name`
117 118 matches against locally defined module. Otherwise, it returns
118 119 False.
119 120
120 121 It is assumed that `name` doesn't have `.__init__`.
121 122
122 123 `absname` is an absolute module name of specified `name`
123 124 (e.g. "hgext.convert"). This can be used to compose prefix for sub
124 125 modules or so.
125 126
126 127 `dottedpath` is a `dotted_name_of_path()`-ed source file path
127 128 (e.g. "hgext.convert.__init__") of `name`. This is used to look
128 129 module up in `localmods` again.
129 130
130 131 `hassubmod` is whether it may have sub modules under it (for
131 132 convenient, even though this is also equivalent to "absname !=
132 133 dottednpath")
133 134
134 135 >>> localmods = {'foo.__init__', 'foo.foo1',
135 136 ... 'foo.bar.__init__', 'foo.bar.bar1',
136 137 ... 'baz.__init__', 'baz.baz1'}
137 138 >>> fromlocal = fromlocalfunc('foo.xxx', localmods)
138 139 >>> # relative
139 140 >>> fromlocal('foo1')
140 141 ('foo.foo1', 'foo.foo1', False)
141 142 >>> fromlocal('bar')
142 143 ('foo.bar', 'foo.bar.__init__', True)
143 144 >>> fromlocal('bar.bar1')
144 145 ('foo.bar.bar1', 'foo.bar.bar1', False)
145 146 >>> # absolute
146 147 >>> fromlocal('baz')
147 148 ('baz', 'baz.__init__', True)
148 149 >>> fromlocal('baz.baz1')
149 150 ('baz.baz1', 'baz.baz1', False)
150 151 >>> # unknown = maybe standard library
151 152 >>> fromlocal('os')
152 153 False
153 154 >>> fromlocal(None, 1)
154 155 ('foo', 'foo.__init__', True)
155 156 >>> fromlocal('foo1', 1)
156 157 ('foo.foo1', 'foo.foo1', False)
157 158 >>> fromlocal2 = fromlocalfunc('foo.xxx.yyy', localmods)
158 159 >>> fromlocal2(None, 2)
159 160 ('foo', 'foo.__init__', True)
160 161 >>> fromlocal2('bar2', 1)
161 162 False
162 163 >>> fromlocal2('bar', 2)
163 164 ('foo.bar', 'foo.bar.__init__', True)
164 165 """
165 166 if not isinstance(modulename, str):
166 167 modulename = modulename.decode('ascii')
167 168 prefix = '.'.join(modulename.split('.')[:-1])
168 169 if prefix:
169 170 prefix += '.'
170 171
171 172 def fromlocal(name, level=0):
172 173 # name is false value when relative imports are used.
173 174 if not name:
174 175 # If relative imports are used, level must not be absolute.
175 176 assert level > 0
176 177 candidates = ['.'.join(modulename.split('.')[:-level])]
177 178 else:
178 179 if not level:
179 180 # Check relative name first.
180 181 candidates = [prefix + name, name]
181 182 else:
182 183 candidates = [
183 184 '.'.join(modulename.split('.')[:-level]) + '.' + name
184 185 ]
185 186
186 187 for n in candidates:
187 188 if n in localmods:
188 189 return (n, n, False)
189 190 dottedpath = n + '.__init__'
190 191 if dottedpath in localmods:
191 192 return (n, dottedpath, True)
192 193 return False
193 194
194 195 return fromlocal
195 196
196 197
197 198 def populateextmods(localmods):
198 199 """Populate C extension modules based on pure modules"""
199 200 newlocalmods = set(localmods)
200 201 for n in localmods:
201 202 if n.startswith('mercurial.pure.'):
202 203 m = n[len('mercurial.pure.') :]
203 204 newlocalmods.add('mercurial.cext.' + m)
204 205 newlocalmods.add('mercurial.cffi._' + m)
205 206 return newlocalmods
206 207
207 208
208 209 def list_stdlib_modules():
209 210 """List the modules present in the stdlib.
210 211
211 212 >>> py3 = sys.version_info[0] >= 3
212 213 >>> mods = set(list_stdlib_modules())
213 214 >>> 'BaseHTTPServer' in mods or py3
214 215 True
215 216
216 217 os.path isn't really a module, so it's missing:
217 218
218 219 >>> 'os.path' in mods
219 220 False
220 221
221 222 sys requires special treatment, because it's baked into the
222 223 interpreter, but it should still appear:
223 224
224 225 >>> 'sys' in mods
225 226 True
226 227
227 228 >>> 'collections' in mods
228 229 True
229 230
230 231 >>> 'cStringIO' in mods or py3
231 232 True
232 233
233 234 >>> 'cffi' in mods
234 235 True
235 236 """
236 237 for m in sys.builtin_module_names:
237 238 yield m
238 239 # These modules only exist on windows, but we should always
239 240 # consider them stdlib.
240 241 for m in ['msvcrt', '_winreg']:
241 242 yield m
242 243 yield '__builtin__'
243 244 yield 'builtins' # python3 only
244 245 yield 'importlib.abc' # python3 only
245 246 yield 'importlib.machinery' # python3 only
246 247 yield 'importlib.util' # python3 only
247 248 for m in 'fcntl', 'grp', 'pwd', 'termios': # Unix only
248 249 yield m
249 250 for m in 'cPickle', 'datetime': # in Python (not C) on PyPy
250 251 yield m
251 252 for m in ['cffi']:
252 253 yield m
253 254 stdlib_prefixes = {sys.prefix, sys.exec_prefix}
254 255 # We need to supplement the list of prefixes for the search to work
255 256 # when run from within a virtualenv.
256 257 for mod in (basehttpserver, zlib):
257 258 if mod is None:
258 259 continue
259 260 try:
260 261 # Not all module objects have a __file__ attribute.
261 262 filename = mod.__file__
262 263 except AttributeError:
263 264 continue
264 265 dirname = os.path.dirname(filename)
265 266 for prefix in stdlib_prefixes:
266 267 if dirname.startswith(prefix):
267 268 # Then this directory is redundant.
268 269 break
269 270 else:
270 271 stdlib_prefixes.add(dirname)
271 272 sourceroot = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
272 273 for libpath in sys.path:
273 274 # We want to walk everything in sys.path that starts with something in
274 275 # stdlib_prefixes, but not directories from the hg sources.
275 276 if os.path.abspath(libpath).startswith(sourceroot) or not any(
276 277 libpath.startswith(p) for p in stdlib_prefixes
277 278 ):
278 279 continue
279 280 for top, dirs, files in os.walk(libpath):
280 281 for i, d in reversed(list(enumerate(dirs))):
281 282 if (
282 283 not os.path.exists(os.path.join(top, d, '__init__.py'))
283 284 or top == libpath
284 285 and d in ('hgdemandimport', 'hgext', 'mercurial')
285 286 ):
286 287 del dirs[i]
287 288 for name in files:
288 289 if not name.endswith(('.py', '.so', '.pyc', '.pyo', '.pyd')):
289 290 continue
290 291 if name.startswith('__init__.py'):
291 292 full_path = top
292 293 else:
293 294 full_path = os.path.join(top, name)
294 295 rel_path = full_path[len(libpath) + 1 :]
295 296 mod = dotted_name_of_path(rel_path)
296 297 yield mod
297 298
298 299
299 300 stdlib_modules = set(list_stdlib_modules())
300 301
301 302
302 303 def imported_modules(source, modulename, f, localmods, ignore_nested=False):
303 304 """Given the source of a file as a string, yield the names
304 305 imported by that file.
305 306
306 307 Args:
307 308 source: The python source to examine as a string.
308 309 modulename: of specified python source (may have `__init__`)
309 310 localmods: set of locally defined module names (may have `__init__`)
310 311 ignore_nested: If true, import statements that do not start in
311 312 column zero will be ignored.
312 313
313 314 Returns:
314 315 A list of absolute module names imported by the given source.
315 316
316 317 >>> f = 'foo/xxx.py'
317 318 >>> modulename = 'foo.xxx'
318 319 >>> localmods = {'foo.__init__': True,
319 320 ... 'foo.foo1': True, 'foo.foo2': True,
320 321 ... 'foo.bar.__init__': True, 'foo.bar.bar1': True,
321 322 ... 'baz.__init__': True, 'baz.baz1': True }
322 323 >>> # standard library (= not locally defined ones)
323 324 >>> sorted(imported_modules(
324 325 ... 'from stdlib1 import foo, bar; import stdlib2',
325 326 ... modulename, f, localmods))
326 327 []
327 328 >>> # relative importing
328 329 >>> sorted(imported_modules(
329 330 ... 'import foo1; from bar import bar1',
330 331 ... modulename, f, localmods))
331 332 ['foo.bar.bar1', 'foo.foo1']
332 333 >>> sorted(imported_modules(
333 334 ... 'from bar.bar1 import name1, name2, name3',
334 335 ... modulename, f, localmods))
335 336 ['foo.bar.bar1']
336 337 >>> # absolute importing
337 338 >>> sorted(imported_modules(
338 339 ... 'from baz import baz1, name1',
339 340 ... modulename, f, localmods))
340 341 ['baz.__init__', 'baz.baz1']
341 342 >>> # mixed importing, even though it shouldn't be recommended
342 343 >>> sorted(imported_modules(
343 344 ... 'import stdlib, foo1, baz',
344 345 ... modulename, f, localmods))
345 346 ['baz.__init__', 'foo.foo1']
346 347 >>> # ignore_nested
347 348 >>> sorted(imported_modules(
348 349 ... '''import foo
349 350 ... def wat():
350 351 ... import bar
351 352 ... ''', modulename, f, localmods))
352 353 ['foo.__init__', 'foo.bar.__init__']
353 354 >>> sorted(imported_modules(
354 355 ... '''import foo
355 356 ... def wat():
356 357 ... import bar
357 358 ... ''', modulename, f, localmods, ignore_nested=True))
358 359 ['foo.__init__']
359 360 """
360 361 fromlocal = fromlocalfunc(modulename, localmods)
361 362 for node in ast.walk(ast.parse(source, f)):
362 363 if ignore_nested and getattr(node, 'col_offset', 0) > 0:
363 364 continue
364 365 if isinstance(node, ast.Import):
365 366 for n in node.names:
366 367 found = fromlocal(n.name)
367 368 if not found:
368 369 # this should import standard library
369 370 continue
370 371 yield found[1]
371 372 elif isinstance(node, ast.ImportFrom):
372 373 found = fromlocal(node.module, node.level)
373 374 if not found:
374 375 # this should import standard library
375 376 continue
376 377
377 378 absname, dottedpath, hassubmod = found
378 379 if not hassubmod:
379 380 # "dottedpath" is not a package; must be imported
380 381 yield dottedpath
381 382 # examination of "node.names" should be redundant
382 383 # e.g.: from mercurial.node import nullid, nullrev
383 384 continue
384 385
385 386 modnotfound = False
386 387 prefix = absname + '.'
387 388 for n in node.names:
388 389 found = fromlocal(prefix + n.name)
389 390 if not found:
390 391 # this should be a function or a property of "node.module"
391 392 modnotfound = True
392 393 continue
393 394 yield found[1]
394 395 if modnotfound:
395 396 # "dottedpath" is a package, but imported because of non-module
396 397 # lookup
397 398 yield dottedpath
398 399
399 400
400 401 def verify_import_convention(module, source, localmods):
401 402 """Verify imports match our established coding convention.
402 403
403 404 We have 2 conventions: legacy and modern. The modern convention is in
404 405 effect when using absolute imports.
405 406
406 407 The legacy convention only looks for mixed imports. The modern convention
407 408 is much more thorough.
408 409 """
409 410 root = ast.parse(source)
410 411 absolute = usingabsolute(root)
411 412
412 413 if absolute:
413 414 return verify_modern_convention(module, root, localmods)
414 415 else:
415 416 return verify_stdlib_on_own_line(root)
416 417
417 418
418 419 def verify_modern_convention(module, root, localmods, root_col_offset=0):
419 420 """Verify a file conforms to the modern import convention rules.
420 421
421 422 The rules of the modern convention are:
422 423
423 424 * Ordering is stdlib followed by local imports. Each group is lexically
424 425 sorted.
425 426 * Importing multiple modules via "import X, Y" is not allowed: use
426 427 separate import statements.
427 428 * Importing multiple modules via "from X import ..." is allowed if using
428 429 parenthesis and one entry per line.
429 430 * Only 1 relative import statement per import level ("from .", "from ..")
430 431 is allowed.
431 432 * Relative imports from higher levels must occur before lower levels. e.g.
432 433 "from .." must be before "from .".
433 434 * Imports from peer packages should use relative import (e.g. do not
434 435 "import mercurial.foo" from a "mercurial.*" module).
435 436 * Symbols can only be imported from specific modules (see
436 437 `allowsymbolimports`). For other modules, first import the module then
437 438 assign the symbol to a module-level variable. In addition, these imports
438 439 must be performed before other local imports. This rule only
439 440 applies to import statements outside of any blocks.
440 441 * Relative imports from the standard library are not allowed, unless that
441 442 library is also a local module.
442 443 * Certain modules must be aliased to alternate names to avoid aliasing
443 444 and readability problems. See `requirealias`.
444 445 """
445 446 if not isinstance(module, str):
446 447 module = module.decode('ascii')
447 448 topmodule = module.split('.')[0]
448 449 fromlocal = fromlocalfunc(module, localmods)
449 450
450 451 # Whether a local/non-stdlib import has been performed.
451 452 seenlocal = None
452 453 # Whether a local/non-stdlib, non-symbol import has been seen.
453 454 seennonsymbollocal = False
454 455 # The last name to be imported (for sorting).
455 456 lastname = None
456 457 laststdlib = None
457 458 # Relative import levels encountered so far.
458 459 seenlevels = set()
459 460
460 461 for node, newscope in walklocal(root):
461 462
462 463 def msg(fmt, *args):
463 464 return (fmt % args, node.lineno)
464 465
465 466 if newscope:
466 467 # Check for local imports in function
467 468 for r in verify_modern_convention(
468 469 module, node, localmods, node.col_offset + 4
469 470 ):
470 471 yield r
471 472 elif isinstance(node, ast.Import):
472 473 # Disallow "import foo, bar" and require separate imports
473 474 # for each module.
474 475 if len(node.names) > 1:
475 476 yield msg(
476 477 'multiple imported names: %s',
477 478 ', '.join(n.name for n in node.names),
478 479 )
479 480
480 481 name = node.names[0].name
481 482 asname = node.names[0].asname
482 483
483 484 stdlib = name in stdlib_modules
484 485
485 486 # Ignore sorting rules on imports inside blocks.
486 487 if node.col_offset == root_col_offset:
487 488 if lastname and name < lastname and laststdlib == stdlib:
488 489 yield msg(
489 490 'imports not lexically sorted: %s < %s', name, lastname
490 491 )
491 492
492 493 lastname = name
493 494 laststdlib = stdlib
494 495
495 496 # stdlib imports should be before local imports.
496 497 if stdlib and seenlocal and node.col_offset == root_col_offset:
497 498 yield msg(
498 499 'stdlib import "%s" follows local import: %s',
499 500 name,
500 501 seenlocal,
501 502 )
502 503
503 504 if not stdlib:
504 505 seenlocal = name
505 506
506 507 # Import of sibling modules should use relative imports.
507 508 topname = name.split('.')[0]
508 509 if topname == topmodule:
509 510 yield msg('import should be relative: %s', name)
510 511
511 512 if name in requirealias and asname != requirealias[name]:
512 513 yield msg(
513 514 '%s module must be "as" aliased to %s',
514 515 name,
515 516 requirealias[name],
516 517 )
517 518
518 519 elif isinstance(node, ast.ImportFrom):
519 520 # Resolve the full imported module name.
520 521 if node.level > 0:
521 522 fullname = '.'.join(module.split('.')[: -node.level])
522 523 if node.module:
523 524 fullname += '.%s' % node.module
524 525 else:
525 526 assert node.module
526 527 fullname = node.module
527 528
528 529 topname = fullname.split('.')[0]
529 530 if topname == topmodule:
530 531 yield msg('import should be relative: %s', fullname)
531 532
532 533 # __future__ is special since it needs to come first and use
533 534 # symbol import.
534 535 if fullname != '__future__':
535 536 if not fullname or (
536 537 fullname in stdlib_modules
537 538 and fullname not in localmods
538 539 and fullname + '.__init__' not in localmods
539 540 ):
540 541 yield msg('relative import of stdlib module')
541 542 else:
542 543 seenlocal = fullname
543 544
544 545 # Direct symbol import is only allowed from certain modules and
545 546 # must occur before non-symbol imports.
546 547 found = fromlocal(node.module, node.level)
547 548 if found and found[2]: # node.module is a package
548 549 prefix = found[0] + '.'
549 550 symbols = (
550 551 n.name for n in node.names if not fromlocal(prefix + n.name)
551 552 )
552 553 else:
553 554 symbols = (n.name for n in node.names)
554 555 symbols = [sym for sym in symbols if sym not in directsymbols]
555 556 if node.module and node.col_offset == root_col_offset:
556 557 if symbols and fullname not in allowsymbolimports:
557 558 yield msg(
558 559 'direct symbol import %s from %s',
559 560 ', '.join(symbols),
560 561 fullname,
561 562 )
562 563
563 564 if symbols and seennonsymbollocal:
564 565 yield msg(
565 566 'symbol import follows non-symbol import: %s', fullname
566 567 )
567 568 if not symbols and fullname not in stdlib_modules:
568 569 seennonsymbollocal = True
569 570
570 571 if not node.module:
571 572 assert node.level
572 573
573 574 # Only allow 1 group per level.
574 575 if (
575 576 node.level in seenlevels
576 577 and node.col_offset == root_col_offset
577 578 ):
578 579 yield msg(
579 580 'multiple "from %s import" statements', '.' * node.level
580 581 )
581 582
582 583 # Higher-level groups come before lower-level groups.
583 584 if any(node.level > l for l in seenlevels):
584 585 yield msg(
585 586 'higher-level import should come first: %s', fullname
586 587 )
587 588
588 589 seenlevels.add(node.level)
589 590
590 591 # Entries in "from .X import ( ... )" lists must be lexically
591 592 # sorted.
592 593 lastentryname = None
593 594
594 595 for n in node.names:
595 596 if lastentryname and n.name < lastentryname:
596 597 yield msg(
597 598 'imports from %s not lexically sorted: %s < %s',
598 599 fullname,
599 600 n.name,
600 601 lastentryname,
601 602 )
602 603
603 604 lastentryname = n.name
604 605
605 606 if n.name in requirealias and n.asname != requirealias[n.name]:
606 607 yield msg(
607 608 '%s from %s must be "as" aliased to %s',
608 609 n.name,
609 610 fullname,
610 611 requirealias[n.name],
611 612 )
612 613
613 614
614 615 def verify_stdlib_on_own_line(root):
615 616 """Given some python source, verify that stdlib imports are done
616 617 in separate statements from relative local module imports.
617 618
618 619 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, foo')))
619 620 [('mixed imports\\n stdlib: sys\\n relative: foo', 1)]
620 621 >>> list(verify_stdlib_on_own_line(ast.parse('import sys, os')))
621 622 []
622 623 >>> list(verify_stdlib_on_own_line(ast.parse('import foo, bar')))
623 624 []
624 625 """
625 626 for node in ast.walk(root):
626 627 if isinstance(node, ast.Import):
627 628 from_stdlib = {False: [], True: []}
628 629 for n in node.names:
629 630 from_stdlib[n.name in stdlib_modules].append(n.name)
630 631 if from_stdlib[True] and from_stdlib[False]:
631 632 yield (
632 633 'mixed imports\n stdlib: %s\n relative: %s'
633 634 % (
634 635 ', '.join(sorted(from_stdlib[True])),
635 636 ', '.join(sorted(from_stdlib[False])),
636 637 ),
637 638 node.lineno,
638 639 )
639 640
640 641
641 642 class CircularImport(Exception):
642 643 pass
643 644
644 645
645 646 def checkmod(mod, imports):
646 647 shortest = {}
647 648 visit = [[mod]]
648 649 while visit:
649 650 path = visit.pop(0)
650 651 for i in sorted(imports.get(path[-1], [])):
651 652 if len(path) < shortest.get(i, 1000):
652 653 shortest[i] = len(path)
653 654 if i in path:
654 655 if i == path[0]:
655 656 raise CircularImport(path)
656 657 continue
657 658 visit.append(path + [i])
658 659
659 660
660 661 def rotatecycle(cycle):
661 662 """arrange a cycle so that the lexicographically first module listed first
662 663
663 664 >>> rotatecycle(['foo', 'bar'])
664 665 ['bar', 'foo', 'bar']
665 666 """
666 667 lowest = min(cycle)
667 668 idx = cycle.index(lowest)
668 669 return cycle[idx:] + cycle[:idx] + [lowest]
669 670
670 671
671 672 def find_cycles(imports):
672 673 """Find cycles in an already-loaded import graph.
673 674
674 675 All module names recorded in `imports` should be absolute one.
675 676
676 677 >>> from __future__ import print_function
677 678 >>> imports = {'top.foo': ['top.bar', 'os.path', 'top.qux'],
678 679 ... 'top.bar': ['top.baz', 'sys'],
679 680 ... 'top.baz': ['top.foo'],
680 681 ... 'top.qux': ['top.foo']}
681 682 >>> print('\\n'.join(sorted(find_cycles(imports))))
682 683 top.bar -> top.baz -> top.foo -> top.bar
683 684 top.foo -> top.qux -> top.foo
684 685 """
685 686 cycles = set()
686 687 for mod in sorted(imports.keys()):
687 688 try:
688 689 checkmod(mod, imports)
689 690 except CircularImport as e:
690 691 cycle = e.args[0]
691 692 cycles.add(" -> ".join(rotatecycle(cycle)))
692 693 return cycles
693 694
694 695
695 696 def _cycle_sortkey(c):
696 697 return len(c), c
697 698
698 699
699 700 def embedded(f, modname, src):
700 701 """Extract embedded python code
701 702
702 703 >>> def _forcestr(thing):
703 704 ... if not isinstance(thing, str):
704 705 ... return thing.decode('ascii')
705 706 ... return thing
706 707 >>> def test(fn, lines):
707 708 ... for s, m, f, l in embedded(fn, b"example", lines):
708 709 ... print("%s %s %d" % (_forcestr(m), _forcestr(f), l))
709 710 ... print(repr(_forcestr(s)))
710 711 >>> lines = [
711 712 ... 'comment',
712 713 ... ' >>> from __future__ import print_function',
713 714 ... " >>> ' multiline",
714 715 ... " ... string'",
715 716 ... ' ',
716 717 ... 'comment',
717 718 ... ' $ cat > foo.py <<EOF',
718 719 ... ' > from __future__ import print_function',
719 720 ... ' > EOF',
720 721 ... ]
721 722 >>> test(b"example.t", lines)
722 723 example[2] doctest.py 1
723 724 "from __future__ import print_function\\n' multiline\\nstring'\\n\\n"
724 725 example[8] foo.py 7
725 726 'from __future__ import print_function\\n'
726 727 """
727 728 errors = []
728 729 for name, starts, ends, code in testparseutil.pyembedded(f, src, errors):
729 730 if not name:
730 731 # use 'doctest.py', in order to make already existing
731 732 # doctest above pass instantly
732 733 name = 'doctest.py'
733 734 # "starts" is "line number" (1-origin), but embedded() is
734 735 # expected to return "line offset" (0-origin). Therefore, this
735 736 # yields "starts - 1".
736 737 if not isinstance(modname, str):
737 738 modname = modname.decode('utf8')
738 739 yield code, "%s[%d]" % (modname, starts), name, starts - 1
739 740
740 741
741 742 def sources(f, modname):
742 743 """Yields possibly multiple sources from a filepath
743 744
744 745 input: filepath, modulename
745 746 yields: script(string), modulename, filepath, linenumber
746 747
747 748 For embedded scripts, the modulename and filepath will be different
748 749 from the function arguments. linenumber is an offset relative to
749 750 the input file.
750 751 """
751 752 py = False
752 753 if not f.endswith('.t'):
753 754 with open(f, 'rb') as src:
754 755 yield src.read(), modname, f, 0
755 756 py = True
756 757 if py or f.endswith('.t'):
757 with open(f, 'r') as src:
758 # Strictly speaking we should sniff for the magic header that denotes
759 # Python source file encoding. But in reality we don't use anything
760 # other than ASCII (mainly) and UTF-8 (in a few exceptions), so
761 # simplicity is fine.
762 with io.open(f, 'r', encoding='utf-8') as src:
758 763 for script, modname, t, line in embedded(f, modname, src):
759 764 yield script, modname.encode('utf8'), t, line
760 765
761 766
762 767 def main(argv):
763 768 if len(argv) < 2 or (argv[1] == '-' and len(argv) > 2):
764 769 print('Usage: %s {-|file [file] [file] ...}')
765 770 return 1
766 771 if argv[1] == '-':
767 772 argv = argv[:1]
768 773 argv.extend(l.rstrip() for l in sys.stdin.readlines())
769 774 localmodpaths = {}
770 775 used_imports = {}
771 776 any_errors = False
772 777 for source_path in argv[1:]:
773 778 modname = dotted_name_of_path(source_path)
774 779 localmodpaths[modname] = source_path
775 780 localmods = populateextmods(localmodpaths)
776 781 for localmodname, source_path in sorted(localmodpaths.items()):
777 782 if not isinstance(localmodname, bytes):
778 783 # This is only safe because all hg's files are ascii
779 784 localmodname = localmodname.encode('ascii')
780 785 for src, modname, name, line in sources(source_path, localmodname):
781 786 try:
782 787 used_imports[modname] = sorted(
783 788 imported_modules(
784 789 src, modname, name, localmods, ignore_nested=True
785 790 )
786 791 )
787 792 for error, lineno in verify_import_convention(
788 793 modname, src, localmods
789 794 ):
790 795 any_errors = True
791 796 print('%s:%d: %s' % (source_path, lineno + line, error))
792 797 except SyntaxError as e:
793 798 print(
794 799 '%s:%d: SyntaxError: %s' % (source_path, e.lineno + line, e)
795 800 )
796 801 cycles = find_cycles(used_imports)
797 802 if cycles:
798 803 firstmods = set()
799 804 for c in sorted(cycles, key=_cycle_sortkey):
800 805 first = c.split()[0]
801 806 # As a rough cut, ignore any cycle that starts with the
802 807 # same module as some other cycle. Otherwise we see lots
803 808 # of cycles that are effectively duplicates.
804 809 if first in firstmods:
805 810 continue
806 811 print('Import cycle:', c)
807 812 firstmods.add(first)
808 813 any_errors = True
809 814 return any_errors != 0
810 815
811 816
812 817 if __name__ == '__main__':
813 818 sys.exit(int(main(sys.argv)))
@@ -1,200 +1,200 b''
1 1 # install-dependencies.ps1 - Install Windows dependencies for building Mercurial
2 2 #
3 3 # Copyright 2019 Gregory Szorc <gregory.szorc@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 # This script can be used to bootstrap a Mercurial build environment on
9 9 # Windows.
10 10 #
11 11 # The script makes a lot of assumptions about how things should work.
12 12 # For example, the install location of Python is hardcoded to c:\hgdev\*.
13 13 #
14 14 # The script should be executed from a PowerShell with elevated privileges
15 15 # if you don't want to see a UAC prompt for various installers.
16 16 #
17 17 # The script is tested on Windows 10 and Windows Server 2019 (in EC2).
18 18
19 19 $VS_BUILD_TOOLS_URL = "https://download.visualstudio.microsoft.com/download/pr/a1603c02-8a66-4b83-b821-811e3610a7c4/aa2db8bb39e0cbd23e9940d8951e0bc3/vs_buildtools.exe"
20 20 $VS_BUILD_TOOLS_SHA256 = "911E292B8E6E5F46CBC17003BDCD2D27A70E616E8D5E6E69D5D489A605CAA139"
21 21
22 22 $VC9_PYTHON_URL = "https://download.microsoft.com/download/7/9/6/796EF2E4-801B-4FC4-AB28-B59FBF6D907B/VCForPython27.msi"
23 23 $VC9_PYTHON_SHA256 = "070474db76a2e625513a5835df4595df9324d820f9cc97eab2a596dcbc2f5cbf"
24 24
25 $PYTHON27_x64_URL = "https://www.python.org/ftp/python/2.7.16/python-2.7.16.amd64.msi"
26 $PYTHON27_x64_SHA256 = "7c0f45993019152d46041a7db4b947b919558fdb7a8f67bcd0535bc98d42b603"
27 $PYTHON27_X86_URL = "https://www.python.org/ftp/python/2.7.16/python-2.7.16.msi"
28 $PYTHON27_X86_SHA256 = "d57dc3e1ba490aee856c28b4915d09e3f49442461e46e481bc6b2d18207831d7"
25 $PYTHON27_x64_URL = "https://www.python.org/ftp/python/2.7.17/python-2.7.17.amd64.msi"
26 $PYTHON27_x64_SHA256 = "3b934447e3620e51d2daf5b2f258c9b617bcc686ca2f777a49aa3b47893abf1b"
27 $PYTHON27_X86_URL = "https://www.python.org/ftp/python/2.7.17/python-2.7.17.msi"
28 $PYTHON27_X86_SHA256 = "a4e3a321517c6b0c2693d6f712a0d18c82600b3d0c759c299b3d14384a17f863"
29 29
30 30 $PYTHON35_x86_URL = "https://www.python.org/ftp/python/3.5.4/python-3.5.4.exe"
31 31 $PYTHON35_x86_SHA256 = "F27C2D67FD9688E4970F3BFF799BB9D722A0D6C2C13B04848E1F7D620B524B0E"
32 32 $PYTHON35_x64_URL = "https://www.python.org/ftp/python/3.5.4/python-3.5.4-amd64.exe"
33 33 $PYTHON35_x64_SHA256 = "9B7741CC32357573A77D2EE64987717E527628C38FD7EAF3E2AACA853D45A1EE"
34 34
35 35 $PYTHON36_x86_URL = "https://www.python.org/ftp/python/3.6.8/python-3.6.8.exe"
36 36 $PYTHON36_x86_SHA256 = "89871D432BC06E4630D7B64CB1A8451E53C80E68DE29029976B12AAD7DBFA5A0"
37 37 $PYTHON36_x64_URL = "https://www.python.org/ftp/python/3.6.8/python-3.6.8-amd64.exe"
38 38 $PYTHON36_x64_SHA256 = "96088A58B7C43BC83B84E6B67F15E8706C614023DD64F9A5A14E81FF824ADADC"
39 39
40 $PYTHON37_x86_URL = "https://www.python.org/ftp/python/3.7.4/python-3.7.4.exe"
41 $PYTHON37_x86_SHA256 = "9a30ab5568ba37bfbcae5cdee19e9dc30765c42cf066f605221563ff8b20ee34"
42 $PYTHON37_X64_URL = "https://www.python.org/ftp/python/3.7.4/python-3.7.4-amd64.exe"
43 $PYTHON37_x64_SHA256 = "bab92f987320975c7826171a072bfd64f8f0941aaf2cdeba6924b7025c9968a3"
40 $PYTHON37_x86_URL = "https://www.python.org/ftp/python/3.7.5/python-3.7.5.exe"
41 $PYTHON37_x86_SHA256 = "3c2ae8f72b48e6e0c2b482206e322bf5d0344ff91abc3b3c200cec9e275c7168"
42 $PYTHON37_X64_URL = "https://www.python.org/ftp/python/3.7.5/python-3.7.5-amd64.exe"
43 $PYTHON37_x64_SHA256 = "f3d60c127e7a92ed547efa3321bf70cd96b75c53bf4b903147015257c1314981"
44 44
45 45 $PYTHON38_x86_URL = "https://www.python.org/ftp/python/3.8.0/python-3.8.0.exe"
46 46 $PYTHON38_x86_SHA256 = "b471908de5e10d8fb5c3351a5affb1172da7790c533e0c9ffbaeec9c11611b15"
47 47 $PYTHON38_x64_URL = "https://www.python.org/ftp/python/3.8.0/python-3.8.0-amd64.exe"
48 48 $PYTHON38_x64_SHA256 = "a9bbc6088a3e4c7112826e21bfee6277f7b6d93259f7c57176139231bb7071e4"
49 49
50 50 # PIP 19.2.3.
51 51 $PIP_URL = "https://github.com/pypa/get-pip/raw/309a56c5fd94bd1134053a541cb4657a4e47e09d/get-pip.py"
52 52 $PIP_SHA256 = "57e3643ff19f018f8a00dfaa6b7e4620e3c1a7a2171fd218425366ec006b3bfe"
53 53
54 54 $VIRTUALENV_URL = "https://files.pythonhosted.org/packages/66/f0/6867af06d2e2f511e4e1d7094ff663acdebc4f15d4a0cb0fed1007395124/virtualenv-16.7.5.tar.gz"
55 55 $VIRTUALENV_SHA256 = "f78d81b62d3147396ac33fc9d77579ddc42cc2a98dd9ea38886f616b33bc7fb2"
56 56
57 57 $INNO_SETUP_URL = "http://files.jrsoftware.org/is/5/innosetup-5.6.1-unicode.exe"
58 58 $INNO_SETUP_SHA256 = "27D49E9BC769E9D1B214C153011978DB90DC01C2ACD1DDCD9ED7B3FE3B96B538"
59 59
60 60 $MINGW_BIN_URL = "https://osdn.net/frs/redir.php?m=constant&f=mingw%2F68260%2Fmingw-get-0.6.3-mingw32-pre-20170905-1-bin.zip"
61 61 $MINGW_BIN_SHA256 = "2AB8EFD7C7D1FC8EAF8B2FA4DA4EEF8F3E47768284C021599BC7435839A046DF"
62 62
63 63 $MERCURIAL_WHEEL_FILENAME = "mercurial-5.1.2-cp27-cp27m-win_amd64.whl"
64 64 $MERCURIAL_WHEEL_URL = "https://files.pythonhosted.org/packages/6d/47/e031e47f7fe9b16e4e3383da47e2b0a7eae6e603996bc67a03ec4fa1b3f4/$MERCURIAL_WHEEL_FILENAME"
65 65 $MERCURIAL_WHEEL_SHA256 = "1d18c7f6ca1456f0f62ee65c9a50c14cbba48ce6e924930cdb10537f5c9eaf5f"
66 66
67 67 # Writing progress slows down downloads substantially. So disable it.
68 68 $progressPreference = 'silentlyContinue'
69 69
70 70 function Secure-Download($url, $path, $sha256) {
71 71 if (Test-Path -Path $path) {
72 72 Get-FileHash -Path $path -Algorithm SHA256 -OutVariable hash
73 73
74 74 if ($hash.Hash -eq $sha256) {
75 75 Write-Output "SHA256 of $path verified as $sha256"
76 76 return
77 77 }
78 78
79 79 Write-Output "hash mismatch on $path; downloading again"
80 80 }
81 81
82 82 Write-Output "downloading $url to $path"
83 83 Invoke-WebRequest -Uri $url -OutFile $path
84 84 Get-FileHash -Path $path -Algorithm SHA256 -OutVariable hash
85 85
86 86 if ($hash.Hash -ne $sha256) {
87 87 Remove-Item -Path $path
88 88 throw "hash mismatch when downloading $url; got $($hash.Hash), expected $sha256"
89 89 }
90 90 }
91 91
92 92 function Invoke-Process($path, $arguments) {
93 93 $p = Start-Process -FilePath $path -ArgumentList $arguments -Wait -PassThru -WindowStyle Hidden
94 94
95 95 if ($p.ExitCode -ne 0) {
96 96 throw "process exited non-0: $($p.ExitCode)"
97 97 }
98 98 }
99 99
100 100 function Install-Python3($name, $installer, $dest, $pip) {
101 101 Write-Output "installing $name"
102 102
103 103 # We hit this when running the script as part of Simple Systems Manager in
104 104 # EC2. The Python 3 installer doesn't seem to like per-user installs
105 105 # when running as the SYSTEM user. So enable global installs if executed in
106 106 # this mode.
107 107 if ($env:USERPROFILE -eq "C:\Windows\system32\config\systemprofile") {
108 108 Write-Output "running with SYSTEM account; installing for all users"
109 109 $allusers = "1"
110 110 }
111 111 else {
112 112 $allusers = "0"
113 113 }
114 114
115 115 Invoke-Process $installer "/quiet TargetDir=${dest} InstallAllUsers=${allusers} AssociateFiles=0 CompileAll=0 PrependPath=0 Include_doc=0 Include_launcher=0 InstallLauncherAllUsers=0 Include_pip=0 Include_test=0"
116 116 Invoke-Process ${dest}\python.exe $pip
117 117 }
118 118
119 119 function Install-Dependencies($prefix) {
120 120 if (!(Test-Path -Path $prefix\assets)) {
121 121 New-Item -Path $prefix\assets -ItemType Directory
122 122 }
123 123
124 124 $pip = "${prefix}\assets\get-pip.py"
125 125
126 126 Secure-Download $VC9_PYTHON_URL ${prefix}\assets\VCForPython27.msi $VC9_PYTHON_SHA256
127 127 Secure-Download $PYTHON27_x86_URL ${prefix}\assets\python27-x86.msi $PYTHON27_x86_SHA256
128 128 Secure-Download $PYTHON27_x64_URL ${prefix}\assets\python27-x64.msi $PYTHON27_x64_SHA256
129 129 Secure-Download $PYTHON35_x86_URL ${prefix}\assets\python35-x86.exe $PYTHON35_x86_SHA256
130 130 Secure-Download $PYTHON35_x64_URL ${prefix}\assets\python35-x64.exe $PYTHON35_x64_SHA256
131 131 Secure-Download $PYTHON36_x86_URL ${prefix}\assets\python36-x86.exe $PYTHON36_x86_SHA256
132 132 Secure-Download $PYTHON36_x64_URL ${prefix}\assets\python36-x64.exe $PYTHON36_x64_SHA256
133 133 Secure-Download $PYTHON37_x86_URL ${prefix}\assets\python37-x86.exe $PYTHON37_x86_SHA256
134 134 Secure-Download $PYTHON37_x64_URL ${prefix}\assets\python37-x64.exe $PYTHON37_x64_SHA256
135 135 Secure-Download $PYTHON38_x86_URL ${prefix}\assets\python38-x86.exe $PYTHON38_x86_SHA256
136 136 Secure-Download $PYTHON38_x64_URL ${prefix}\assets\python38-x64.exe $PYTHON38_x64_SHA256
137 137 Secure-Download $PIP_URL ${pip} $PIP_SHA256
138 138 Secure-Download $VIRTUALENV_URL ${prefix}\assets\virtualenv.tar.gz $VIRTUALENV_SHA256
139 139 Secure-Download $VS_BUILD_TOOLS_URL ${prefix}\assets\vs_buildtools.exe $VS_BUILD_TOOLS_SHA256
140 140 Secure-Download $INNO_SETUP_URL ${prefix}\assets\InnoSetup.exe $INNO_SETUP_SHA256
141 141 Secure-Download $MINGW_BIN_URL ${prefix}\assets\mingw-get-bin.zip $MINGW_BIN_SHA256
142 142 Secure-Download $MERCURIAL_WHEEL_URL ${prefix}\assets\${MERCURIAL_WHEEL_FILENAME} $MERCURIAL_WHEEL_SHA256
143 143
144 144 Write-Output "installing Python 2.7 32-bit"
145 145 Invoke-Process msiexec.exe "/i ${prefix}\assets\python27-x86.msi /l* ${prefix}\assets\python27-x86.log /q TARGETDIR=${prefix}\python27-x86 ALLUSERS="
146 146 Invoke-Process ${prefix}\python27-x86\python.exe ${prefix}\assets\get-pip.py
147 147 Invoke-Process ${prefix}\python27-x86\Scripts\pip.exe "install ${prefix}\assets\virtualenv.tar.gz"
148 148
149 149 Write-Output "installing Python 2.7 64-bit"
150 150 Invoke-Process msiexec.exe "/i ${prefix}\assets\python27-x64.msi /l* ${prefix}\assets\python27-x64.log /q TARGETDIR=${prefix}\python27-x64 ALLUSERS="
151 151 Invoke-Process ${prefix}\python27-x64\python.exe ${prefix}\assets\get-pip.py
152 152 Invoke-Process ${prefix}\python27-x64\Scripts\pip.exe "install ${prefix}\assets\virtualenv.tar.gz"
153 153
154 154 Install-Python3 "Python 3.5 32-bit" ${prefix}\assets\python35-x86.exe ${prefix}\python35-x86 ${pip}
155 155 Install-Python3 "Python 3.5 64-bit" ${prefix}\assets\python35-x64.exe ${prefix}\python35-x64 ${pip}
156 156 Install-Python3 "Python 3.6 32-bit" ${prefix}\assets\python36-x86.exe ${prefix}\python36-x86 ${pip}
157 157 Install-Python3 "Python 3.6 64-bit" ${prefix}\assets\python36-x64.exe ${prefix}\python36-x64 ${pip}
158 158 Install-Python3 "Python 3.7 32-bit" ${prefix}\assets\python37-x86.exe ${prefix}\python37-x86 ${pip}
159 159 Install-Python3 "Python 3.7 64-bit" ${prefix}\assets\python37-x64.exe ${prefix}\python37-x64 ${pip}
160 160 Install-Python3 "Python 3.8 32-bit" ${prefix}\assets\python38-x86.exe ${prefix}\python38-x86 ${pip}
161 161 Install-Python3 "Python 3.8 64-bit" ${prefix}\assets\python38-x64.exe ${prefix}\python38-x64 ${pip}
162 162
163 163 Write-Output "installing Visual Studio 2017 Build Tools and SDKs"
164 164 Invoke-Process ${prefix}\assets\vs_buildtools.exe "--quiet --wait --norestart --nocache --channelUri https://aka.ms/vs/15/release/channel --add Microsoft.VisualStudio.Workload.MSBuildTools --add Microsoft.VisualStudio.Component.Windows10SDK.17763 --add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.Windows10SDK --add Microsoft.VisualStudio.Component.VC.140"
165 165
166 166 Write-Output "installing Visual C++ 9.0 for Python 2.7"
167 167 Invoke-Process msiexec.exe "/i ${prefix}\assets\VCForPython27.msi /l* ${prefix}\assets\VCForPython27.log /q"
168 168
169 169 Write-Output "installing Inno Setup"
170 170 Invoke-Process ${prefix}\assets\InnoSetup.exe "/SP- /VERYSILENT /SUPPRESSMSGBOXES"
171 171
172 172 Write-Output "extracting MinGW base archive"
173 173 Expand-Archive -Path ${prefix}\assets\mingw-get-bin.zip -DestinationPath "${prefix}\MinGW" -Force
174 174
175 175 Write-Output "updating MinGW package catalogs"
176 176 Invoke-Process ${prefix}\MinGW\bin\mingw-get.exe "update"
177 177
178 178 Write-Output "installing MinGW packages"
179 179 Invoke-Process ${prefix}\MinGW\bin\mingw-get.exe "install msys-base msys-coreutils msys-diffutils msys-unzip"
180 180
181 181 # Construct a virtualenv useful for bootstrapping. It conveniently contains a
182 182 # Mercurial install.
183 183 Write-Output "creating bootstrap virtualenv with Mercurial"
184 184 Invoke-Process "$prefix\python27-x64\Scripts\virtualenv.exe" "${prefix}\venv-bootstrap"
185 185 Invoke-Process "${prefix}\venv-bootstrap\Scripts\pip.exe" "install ${prefix}\assets\${MERCURIAL_WHEEL_FILENAME}"
186 186 }
187 187
188 188 function Clone-Mercurial-Repo($prefix, $repo_url, $dest) {
189 189 Write-Output "cloning $repo_url to $dest"
190 190 # TODO Figure out why CA verification isn't working in EC2 and remove
191 191 # --insecure.
192 192 Invoke-Process "${prefix}\venv-bootstrap\Scripts\hg.exe" "clone --insecure $repo_url $dest"
193 193
194 194 # Mark repo as non-publishing by default for convenience.
195 195 Add-Content -Path "$dest\.hg\hgrc" -Value "`n[phases]`npublish = false"
196 196 }
197 197
198 198 $prefix = "c:\hgdev"
199 199 Install-Dependencies $prefix
200 200 Clone-Mercurial-Repo $prefix "https://www.mercurial-scm.org/repo/hg" $prefix\src
@@ -1,145 +1,136 b''
1 1 $(eval HGROOT := $(shell cd ../..; pwd))
2 2
3 3 DEBIAN_CODENAMES := \
4 4 stretch \
5 5 buster \
6 6 bullseye
7 7
8 8 UBUNTU_CODENAMES := \
9 9 xenial \
10 10 bionic \
11 11 cosmic \
12 12 disco
13 13
14 FEDORA_RELEASES := \
15 20 \
16 21 \
17 28 \
18 29
14 FEDORA_RELEASE := 31
19 15
20 16 CENTOS_RELEASES := \
21 17 5 \
22 18 6 \
23 7
19 7 \
20 8
24 21
25 22 # Build a Python for these CentOS releases.
26 23 CENTOS_WITH_PYTHON_RELEASES := 5 6
24 CENTOS_WITH_NONVERSIONED_PYTHON := 5 6 7
27 25
28 26 help:
29 27 @echo 'Packaging Make Targets'
30 28 @echo ''
31 29 @echo 'docker-centos{$(strip $(CENTOS_RELEASES))}'
32 30 @echo ' Build an RPM for a specific CentOS version using Docker.'
33 31 @echo ''
34 32 @echo 'docker-debian-{$(strip $(DEBIAN_CODENAMES))}'
35 33 @echo ' Build Debian packages specific to a Debian distro using Docker.'
36 34 @echo ''
37 @echo 'docker-fedora{$(strip $(FEDORA_RELEASES))}'
38 @echo ' Build an RPM for a specific Fedora version using Docker.'
35 @echo 'docker-fedora'
36 @echo ' Build an RPM for a Fedora $(FEDORA_RELEASE) using Docker.'
39 37 @echo ''
40 38 @echo 'docker-ubuntu-{$(strip $(UBUNTU_CODENAMES))}'
41 39 @echo ' Build Debian package specific to an Ubuntu distro using Docker.'
42 40 @echo ''
43 41 @echo 'docker-ubuntu-{$(strip $(UBUNTU_CODENAMES))}-ppa'
44 42 @echo ' Build a source-only Debian package specific to an Ubuntu distro'
45 43 @echo ' using Docker.'
46 44 @echo ''
47 45 @echo 'linux-wheels'
48 46 @echo ' Build Linux manylinux wheels using Docker.'
49 47 @echo ''
50 48 @echo 'linux-wheels-{x86_64, i686}'
51 49 @echo ' Build Linux manylinux wheels for a specific architecture using Docker'
52 50 @echo ''
53 51 @echo 'deb'
54 52 @echo ' Build a Debian package locally targeting the current system'
55 53 @echo ''
56 54 @echo 'ppa'
57 55 @echo ' Build a Debian source package locally targeting the current system'
58 56 @echo ''
59 57 @echo 'centos{$(strip $(CENTOS_RELEASES))}'
60 58 @echo ' Build an RPM for a specific CentOS version locally'
61 59 @echo ''
62 @echo 'fedora{$(strip $(FEDORA_RELEASES))}'
63 @echo ' Build an RPM for a specific Fedora version locally'
60 @echo 'fedora'
61 @echo ' Build an RPM for Fedora $(FEDORA_RELEASE) locally'
64 62
65 63 .PHONY: help
66 64
67 65 .PHONY: deb
68 66 deb:
69 67 ./builddeb
70 68
71 69 .PHONY: ppa
72 70 ppa:
73 71 ./builddeb --source-only
74 72
75 73 # Debian targets.
76 74 define debian_targets =
77 75 .PHONY: docker-debian-$(1)
78 76 docker-debian-$(1):
79 77 ./dockerdeb debian $(1)
80 78
81 79 endef
82 80
83 81 $(foreach codename,$(DEBIAN_CODENAMES),$(eval $(call debian_targets,$(codename))))
84 82
85 83 # Ubuntu targets.
86 84 define ubuntu_targets =
87 85 .PHONY: docker-ubuntu-$(1)
88 86 docker-ubuntu-$(1):
89 87 ./dockerdeb ubuntu $(1)
90 88
91 89 .PHONY: docker-ubuntu-$(1)-ppa
92 90 docker-ubuntu-$(1)-ppa:
93 91 ./dockerdeb ubuntu $(1) --source-only
94 92
95 93 endef
96 94
97 95 $(foreach codename,$(UBUNTU_CODENAMES),$(eval $(call ubuntu_targets,$(codename))))
98 96
99 97 # Fedora targets.
100 define fedora_targets
101 .PHONY: fedora$(1)
102 fedora$(1):
103 mkdir -p $$(HGROOT)/packages/fedora$(1)
98 .PHONY: fedora
99 fedora:
100 mkdir -p $(HGROOT)/packages/fedora$(FEDORA_RELEASE)
104 101 ./buildrpm
105 cp $$(HGROOT)/contrib/packaging/rpmbuild/RPMS/*/* $$(HGROOT)/packages/fedora$(1)
106 cp $$(HGROOT)/contrib/packaging/rpmbuild/SRPMS/* $$(HGROOT)/packages/fedora$(1)
102 cp $(HGROOT)/contrib/packaging/rpmbuild/RPMS/*/* $(HGROOT)/packages/fedora$(FEDORA_RELEASE)
103 cp $(HGROOT)/contrib/packaging/rpmbuild/SRPMS/* $(HGROOT)/packages/fedora$(FEDORA_RELEASE)
107 104 rm -rf $(HGROOT)/rpmbuild
108 105
109 .PHONY: docker-fedora$(1)
110 docker-fedora$(1):
111 mkdir -p $$(HGROOT)/packages/fedora$(1)
112 ./dockerrpm fedora$(1)
113
114 endef
115
116 $(foreach release,$(FEDORA_RELEASES),$(eval $(call fedora_targets,$(release))))
106 .PHONY: docker-fedora
107 docker-fedora:
108 ./dockerrpm fedora$(FEDORA_RELEASE)
117 109
118 110 # CentOS targets.
119 111 define centos_targets
120 112 .PHONY: centos$(1)
121 113 centos$(1):
122 114 mkdir -p $$(HGROOT)/packages/centos$(1)
123 ./buildrpm $$(if $$(filter $(1),$$(CENTOS_WITH_PYTHON_RELEASES)),--withpython)
115 ./buildrpm $$(if $$(filter $(1),$$(CENTOS_WITH_PYTHON_RELEASES)),--withpython,$$(if $$(filter $(1),$$(CENTOS_WITH_NONVERSIONED_PYTHON)),--python python,))
124 116 cp $$(HGROOT)/contrib/packaging/rpmbuild/RPMS/*/* $$(HGROOT)/packages/centos$(1)
125 117 cp $$(HGROOT)/contrib/packaging/rpmbuild/SRPMS/* $$(HGROOT)/packages/centos$(1)
126 118
127 119 .PHONY: docker-centos$(1)
128 120 docker-centos$(1):
129 mkdir -p $$(HGROOT)/packages/centos$(1)
130 ./dockerrpm centos$(1) $$(if $$(filter $(1),$$(CENTOS_WITH_PYTHON_RELEASES)),--withpython)
121 ./dockerrpm centos$(1) $$(if $$(filter $(1),$$(CENTOS_WITH_PYTHON_RELEASES)),--withpython,$$(if $$(filter $(1),$$(CENTOS_WITH_NONVERSIONED_PYTHON)),--python python,))
131 122
132 123 endef
133 124
134 125 $(foreach release,$(CENTOS_RELEASES),$(eval $(call centos_targets,$(release))))
135 126
136 127 .PHONY: linux-wheels
137 128 linux-wheels: linux-wheels-x86_64 linux-wheels-i686
138 129
139 130 .PHONY: linux-wheels-x86_64
140 131 linux-wheels-x86_64:
141 132 docker run -e "HGTEST_JOBS=$(shell nproc)" --rm -ti -v `pwd`/../..:/src quay.io/pypa/manylinux1_x86_64 /src/contrib/packaging/build-linux-wheels.sh
142 133
143 134 .PHONY: linux-wheels-i686
144 135 linux-wheels-i686:
145 136 docker run -e "HGTEST_JOBS=$(shell nproc)" --rm -ti -v `pwd`/../..:/src quay.io/pypa/manylinux1_i686 linux32 /src/contrib/packaging/build-linux-wheels.sh
@@ -1,115 +1,115 b''
1 1 #!/bin/sh -e
2 2 #
3 3 # Build a Mercurial debian package from the current repo
4 4 #
5 5 # Tested on Jessie (stable as of original script authoring.)
6 6
7 7 . $(dirname $0)/packagelib.sh
8 8
9 9 ROOTDIR=$(cd $(dirname $0)/../.. > /dev/null; pwd)
10 10
11 11 BUILD=1
12 12 CLEANUP=1
13 13 DISTID=`(lsb_release -is 2> /dev/null | tr '[:upper:]' '[:lower:]') || echo debian`
14 14 CODENAME=`lsb_release -cs 2> /dev/null || echo unknown`
15 15 DEBFLAGS=-b
16 16
17 17 cleanup() {
18 18 if [ "$CLEANUP" ]; then
19 19 rm -r "$ROOTDIR/debian";
20 20 fi
21 21 }
22 22
23 23 while [ "$1" ]; do
24 24 case "$1" in
25 25 --distid )
26 26 shift
27 27 DISTID="$1"
28 28 shift
29 29 ;;
30 30 --codename )
31 31 shift
32 32 CODENAME="$1"
33 33 shift
34 34 ;;
35 35 --cleanup )
36 36 shift
37 37 BUILD=
38 38 ;;
39 39 --build )
40 40 shift
41 41 CLEANUP=
42 42 ;;
43 43 --source-only )
44 44 shift
45 45 DEBFLAGS=-S
46 46 ;;
47 47 * )
48 48 echo "Invalid parameter $1!" 1>&2
49 49 exit 1
50 50 ;;
51 51 esac
52 52 done
53 53
54 54 cd "$ROOTDIR"
55 55
56 56 trap 'cleanup' EXIT
57 57
58 58 set -u
59 59
60 60 if [ ! -d .hg ]; then
61 61 printf "You are inside %s, which is not the root of a Mercurial repository\n" $(pwd) 1>&2
62 62 exit 1
63 63 fi
64 64
65 65 gethgversion
66 66 debver="$version"
67 67 if [ -n "$type" ] ; then
68 68 debver="$debver~$type"
69 69 fi
70 70 if [ -n "$distance" ] ; then
71 71 debver="$debver+$distance-$CODENAME-$node"
72 72 elif [ "$DEBFLAGS" = "-S" ] ; then
73 73 # for building a ppa (--source-only) for a release (distance == 0), we need
74 74 # to version the distroseries so that we can upload to launchpad
75 75 debver="$debver~${CODENAME}1"
76 76 fi
77 77
78 78 control=debian/control
79 79 changelog=debian/changelog
80 80
81 81 if [ "$BUILD" ]; then
82 82 if [ -d debian ] ; then
83 83 printf "Error! debian control directory already exists at %s/debian\n" $(pwd)
84 84 exit 1
85 85 fi
86 86
87 87 cp -r "$ROOTDIR"/contrib/packaging/debian debian
88 88
89 89 sed -i.tmp "s/__VERSION__/$debver/" $changelog
90 90 sed -i.tmp "s/__DATE__/$(date --rfc-2822)/" $changelog
91 91 sed -i.tmp "s/__CODENAME__/$CODENAME/" $changelog
92 92 rm $changelog.tmp
93 93
94 94 # remove the node from the version string
95 95 SRCFILE="mercurial_$(echo $debver | sed "s,-$node,,").orig.tar.gz"
96 96 "$ROOTDIR/hg" archive $SRCFILE
97 97 mv $SRCFILE ..
98 98 debuild -us -uc -i -I $DEBFLAGS
99 99 if [ $? != 0 ]; then
100 100 echo 'debuild failed!'
101 101 exit 1
102 102 fi
103 103
104 104 fi
105 105 if [ "$CLEANUP" ] ; then
106 106 echo
107 107 OUTPUTDIR=${OUTPUTDIR:=packages/$DISTID-$CODENAME}
108 108 mkdir -p "$OUTPUTDIR"
109 find ../mercurial*.deb ../mercurial_*.build ../mercurial_*.changes \
109 find ../mercurial*.deb ../mercurial_*.build* ../mercurial_*.changes \
110 110 ../mercurial*.dsc ../mercurial*.gz \
111 111 -type f -newer $control -print0 2>/dev/null | \
112 112 xargs -Inarf -0 mv narf "$OUTPUTDIR"
113 113 echo "Built packages for $debver:"
114 find "$PWD"/"$OUTPUTDIR" -type f -newer $control -name '*.deb'
114 find "$OUTPUTDIR" -type f -newer $control -name '*.deb'
115 115 fi
@@ -1,160 +1,163 b''
1 1 #!/bin/bash -e
2 2 #
3 # Build a Mercurial RPM from the current repo
4 #
5 # Tested on
6 # - Fedora 20
7 # - CentOS 5
8 # - centOS 6
3 # Build a Mercurial RPM from the current repo, mainly for Fedora/CentOS/RHEL
9 4
10 5 . $(dirname $0)/packagelib.sh
11 6
12 7 BUILD=1
13 8 RPMBUILDDIR="$PWD/rpmbuild"
9 PYTHONEXE=python3
14 10
15 11 while [ "$1" ]; do
16 12 case "$1" in
17 13 --prepare )
18 14 shift
19 15 BUILD=
20 16 ;;
17 --python)
18 shift
19 PYTHONEXE=$1
20 shift
21 ;;
21 22 --withpython | --with-python)
22 23 shift
23 24 PYTHONVER=2.7.16
24 25 PYTHONMD5=f1a2ace631068444831d01485466ece0
26 PYTHONEXE=python
25 27 ;;
26 28 --rpmbuilddir )
27 29 shift
28 30 RPMBUILDDIR="$1"
29 31 shift
30 32 ;;
31 33 * )
32 34 echo "Invalid parameter $1!" 1>&2
33 35 exit 1
34 36 ;;
35 37 esac
36 38 done
37 39
38 40 cd "`dirname $0`/../.."
39 41
40 42 specfile=$PWD/contrib/packaging/mercurial.spec
41 43 if [ ! -f $specfile ]; then
42 44 echo "Cannot find $specfile!" 1>&2
43 45 exit 1
44 46 fi
45 47
46 48 if [ ! -d .hg ]; then
47 49 echo 'You are not inside a Mercurial repository!' 1>&2
48 50 exit 1
49 51 fi
50 52
51 53 gethgversion
52 54
53 55 if [ -z "$type" ] ; then
54 release=1
56 release=1
55 57 else
56 58 release=0.9_$type
57 59 fi
58 60
59 61 if [ -n "$distance" ] ; then
60 62 release=$release+${distance}_${node}
61 63 fi
62 64
63 65 if [ "$PYTHONVER" ]; then
64 66 release=$release+$PYTHONVER
65 67 RPMPYTHONVER=$PYTHONVER
66 68 else
67 69 RPMPYTHONVER=%{nil}
68 70 fi
69 71
70 72 mkdir -p $RPMBUILDDIR/{SOURCES,BUILD,SRPMS,RPMS}
71 73 $HG archive -t tgz $RPMBUILDDIR/SOURCES/mercurial-$version-$release.tar.gz
72 74 if [ "$PYTHONVER" ]; then
73 75 (
74 76 mkdir -p build
75 77 cd build
76 78 PYTHON_SRCFILE=Python-$PYTHONVER.tgz
77 79 [ -f $PYTHON_SRCFILE ] || curl -Lo $PYTHON_SRCFILE http://www.python.org/ftp/python/$PYTHONVER/$PYTHON_SRCFILE
78 80 if [ "$PYTHONMD5" ]; then
79 81 echo "$PYTHONMD5 $PYTHON_SRCFILE" | md5sum -w -c
80 82 fi
81 83 ln -f $PYTHON_SRCFILE $RPMBUILDDIR/SOURCES/$PYTHON_SRCFILE
82 84
83 85 DOCUTILSVER=`sed -ne "s/^%global docutilsname docutils-//p" $specfile`
84 86 DOCUTILS_SRCFILE=docutils-$DOCUTILSVER.tar.gz
85 87 [ -f $DOCUTILS_SRCFILE ] || curl -Lo $DOCUTILS_SRCFILE http://downloads.sourceforge.net/project/docutils/docutils/$DOCUTILSVER/$DOCUTILS_SRCFILE
86 88 DOCUTILSMD5=`sed -ne "s/^%global docutilsmd5 //p" $specfile`
87 89 if [ "$DOCUTILSMD5" ]; then
88 90 echo "$DOCUTILSMD5 $DOCUTILS_SRCFILE" | md5sum -w -c
89 91 fi
90 92 ln -f $DOCUTILS_SRCFILE $RPMBUILDDIR/SOURCES/$DOCUTILS_SRCFILE
91 93 )
92 94 fi
93 95
94 96 mkdir -p $RPMBUILDDIR/SPECS
95 97 rpmspec=$RPMBUILDDIR/SPECS/mercurial.spec
96 98
97 99 sed -e "s,^Version:.*,Version: $version," \
98 100 -e "s,^Release:.*,Release: $release," \
101 -e "s/^%global pythonexe .*/%global pythonexe $PYTHONEXE/" \
99 102 $specfile > $rpmspec
100 103
101 104 echo >> $rpmspec
102 105 echo "%changelog" >> $rpmspec
103 106
104 107 if echo $version | grep '+' > /dev/null 2>&1; then
105 108 latesttag="`echo $version | sed -e 's/+.*//'`"
106 109 $HG log -r .:"$latesttag" -fM \
107 110 --template '{date|hgdate}\t{author}\t{desc|firstline}\n' | python -c '
108 111 import sys, time
109 112
110 113 def datestr(date, format):
111 114 return time.strftime(format, time.gmtime(float(date[0]) - date[1]))
112 115
113 116 changelog = []
114 117 for l in sys.stdin.readlines():
115 118 tok = l.split("\t")
116 119 hgdate = tuple(int(v) for v in tok[0].split())
117 120 changelog.append((datestr(hgdate, "%F"), tok[1], hgdate, tok[2]))
118 121 prevtitle = ""
119 122 for l in sorted(changelog, reverse=True):
120 123 title = "* %s %s" % (datestr(l[2], "%a %b %d %Y"), l[1])
121 124 if prevtitle != title:
122 125 prevtitle = title
123 126 print
124 print title
125 print "- %s" % l[3].strip()
127 print(title)
128 print("- %s" % l[3].strip())
126 129 ' >> $rpmspec
127 130
128 131 else
129 132
130 133 $HG log \
131 134 --template '{date|hgdate}\t{author}\t{desc|firstline}\n' \
132 135 .hgtags | python -c '
133 136 import sys, time
134 137
135 138 def datestr(date, format):
136 139 return time.strftime(format, time.gmtime(float(date[0]) - date[1]))
137 140
138 141 for l in sys.stdin.readlines():
139 142 tok = l.split("\t")
140 143 hgdate = tuple(int(v) for v in tok[0].split())
141 print "* %s %s\n- %s" % (datestr(hgdate, "%a %b %d %Y"), tok[1], tok[2])
144 print("* %s %s\n- %s" % (datestr(hgdate, "%a %b %d %Y"), tok[1], tok[2]))
142 145 ' >> $rpmspec
143 146
144 147 fi
145 148
146 149 sed -i \
147 150 -e "s/^%define withpython.*$/%define withpython $RPMPYTHONVER/" \
148 151 $rpmspec
149 152
150 153 if [ "$BUILD" ]; then
151 154 rpmbuild --define "_topdir $RPMBUILDDIR" -ba $rpmspec --clean
152 155 if [ $? = 0 ]; then
153 156 echo
154 157 echo "Built packages for $version-$release:"
155 158 find $RPMBUILDDIR/*RPMS/ -type f -newer $rpmspec
156 159 fi
157 160 else
158 161 echo "Prepared sources for $version-$release $rpmspec are in $RPMBUILDDIR/SOURCES/ - use like:"
159 162 echo "rpmbuild --define '_topdir $RPMBUILDDIR' -ba $rpmspec --clean"
160 163 fi
@@ -1,54 +1,41 b''
1 1 Source: mercurial
2 2 Section: vcs
3 3 Priority: optional
4 4 Maintainer: Mercurial Developers <mercurial-devel@mercurial-scm.org>
5 5 Build-Depends:
6 6 debhelper (>= 9),
7 7 dh-python,
8 8 less,
9 9 netbase,
10 python-all,
11 python-all-dev,
12 python-docutils,
10 python3-all,
11 python3-all-dev,
12 python3-docutils,
13 13 unzip,
14 14 zip
15 15 Standards-Version: 3.9.4
16 X-Python-Version: >= 2.7
16 X-Python3-Version: >= 3.5
17 17
18 18 Package: mercurial
19 19 Depends:
20 python,
20 sensible-utils,
21 21 ${shlibs:Depends},
22 22 ${misc:Depends},
23 ${python:Depends},
24 mercurial-common (= ${source:Version})
23 ${python3:Depends},
24 Recommends: ca-certificates
25 Suggests: wish
26 Replaces: mercurial-common
27 Breaks: mercurial-common
25 28 Architecture: any
26 29 Description: fast, easy to use, distributed revision control tool.
27 30 Mercurial is a fast, lightweight Source Control Management system designed
28 31 for efficient handling of very large distributed projects.
29 32 .
30 33 Its features include:
31 34 * O(1) delta-compressed file storage and retrieval scheme
32 35 * Complete cross-indexing of files and changesets for efficient exploration
33 36 of project history
34 37 * Robust SHA1-based integrity checking and append-only storage model
35 38 * Decentralized development model with arbitrary merging between trees
36 39 * Easy-to-use command-line interface
37 40 * Integrated stand-alone web interface
38 41 * Small Python codebase
39
40 Package: mercurial-common
41 Architecture: all
42 Depends:
43 ${misc:Depends},
44 ${python:Depends},
45 Recommends: mercurial (= ${source:Version}), ca-certificates
46 Suggests: wish
47 Breaks: mercurial (<< ${source:Version})
48 Replaces: mercurial (<< 2.6.3)
49 Description: easy-to-use, scalable distributed version control system (common files)
50 Mercurial is a fast, lightweight Source Control Management system designed
51 for efficient handling of very large distributed projects.
52 .
53 This package contains the architecture independent components of Mercurial,
54 and is generally useless without the mercurial package.
@@ -1,44 +1,44 b''
1 1 #!/usr/bin/make -f
2 2 # Uncomment this to turn on verbose mode.
3 3 # export DH_VERBOSE=1
4 4
5 5 CPUS=$(shell cat /proc/cpuinfo | grep -E ^processor | wc -l)
6 6
7 export HGPYTHON3=1
8 export PYTHON=python3
9
7 10 %:
8 dh $@ --with python2
11 dh $@ --with python3
9 12
10 13 override_dh_auto_test:
11 14 http_proxy='' dh_auto_test -- TESTFLAGS="-j$(CPUS)"
12 15
13 override_dh_python2:
14 dh_python2
15 find debian/mercurial/usr/share -type d -empty -delete
16 override_dh_python3:
17 dh_python3 --shebang=/usr/bin/python3
18
19 override_dh_auto_clean:
20 $(MAKE) cleanbutpackages
21 $(MAKE) -C contrib/chg clean
16 22
17 override_dh_install:
18 python$(PYVERS) setup.py install --root "$(CURDIR)"/debian/mercurial --install-layout=deb
23 override_dh_auto_build:
24 $(MAKE) all
25 $(MAKE) -C contrib/chg all
26
27 override_dh_auto_install:
28 python3 setup.py install --root "$(CURDIR)"/debian/mercurial --install-layout=deb
19 29 # chg
20 30 make -C contrib/chg \
21 31 DESTDIR="$(CURDIR)"/debian/mercurial \
22 32 PREFIX=/usr \
23 clean install
24 # remove arch-independent python stuff
25 find "$(CURDIR)"/debian/mercurial/usr/lib \
26 ! -name '*.so' ! -type d -delete , \
27 -type d -empty -delete
28 python$(PYVERS) setup.py install --root "$(CURDIR)/debian/mercurial-common" --install-layout=deb
29 make install-doc PREFIX="$(CURDIR)"/debian/mercurial-common/usr
30 # remove arch-dependent python stuff
31 find "$(CURDIR)"/debian/mercurial-common/usr/lib \
32 -name '*.so' ! -type d -delete , \
33 -type d -empty -delete
34 cp contrib/hg-ssh "$(CURDIR)"/debian/mercurial-common/usr/bin
35 mkdir -p "$(CURDIR)"/debian/mercurial-common/usr/share/mercurial
36 cp contrib/hgk "$(CURDIR)"/debian/mercurial-common/usr/share/mercurial
37 mkdir -p "$(CURDIR)"/debian/mercurial-common/etc/mercurial/hgrc.d/
38 cp contrib/packaging/debian/*.rc "$(CURDIR)"/debian/mercurial-common/etc/mercurial/hgrc.d/
33 install
34 make install-doc PREFIX="$(CURDIR)"/debian/mercurial/usr
35 cp contrib/hg-ssh "$(CURDIR)"/debian/mercurial/usr/bin
36 mkdir -p "$(CURDIR)"/debian/mercurial/usr/share/mercurial
37 cp contrib/hgk "$(CURDIR)"/debian/mercurial/usr/share/mercurial
38 mkdir -p "$(CURDIR)"/debian/mercurial/etc/mercurial/hgrc.d/
39 cp contrib/packaging/debian/*.rc "$(CURDIR)"/debian/mercurial/etc/mercurial/hgrc.d/
39 40 # completions
40 mkdir -p "$(CURDIR)"/debian/mercurial-common/usr/share/bash-completion/completions
41 cp contrib/bash_completion "$(CURDIR)"/debian/mercurial-common/usr/share/bash-completion/completions/hg
42 mkdir -p "$(CURDIR)"/debian/mercurial-common/usr/share/zsh/vendor-completions
43 cp contrib/zsh_completion "$(CURDIR)"/debian/mercurial-common/usr/share/zsh/vendor-completions/_hg
44 rm "$(CURDIR)"/debian/mercurial-common/usr/bin/hg
41 mkdir -p "$(CURDIR)"/debian/mercurial/usr/share/bash-completion/completions
42 cp contrib/bash_completion "$(CURDIR)"/debian/mercurial/usr/share/bash-completion/completions/hg
43 mkdir -p "$(CURDIR)"/debian/mercurial/usr/share/zsh/vendor-completions
44 cp contrib/zsh_completion "$(CURDIR)"/debian/mercurial/usr/share/zsh/vendor-completions/_hg
@@ -1,15 +1,15 b''
1 FROM fedora:29
1 FROM fedora:%OS_RELEASE%
2 2
3 3 RUN groupadd -g 1000 build && \
4 4 useradd -u 1000 -g 1000 -s /bin/bash -d /build -m build
5 5
6 6 RUN dnf install -y \
7 7 gcc \
8 8 gettext \
9 9 make \
10 python-devel \
11 python-docutils \
10 python3-devel \
11 python3-docutils \
12 12 rpm-build
13 13
14 14 # For creating repo meta data
15 15 RUN dnf install -y createrepo
@@ -1,47 +1,60 b''
1 1 #!/bin/bash -e
2 2
3 3 BUILDDIR=$(dirname $0)
4 4 export ROOTDIR=$(cd $BUILDDIR/../..; pwd)
5 5
6 6 PLATFORM="$1"
7 7 shift # extra params are passed to buildrpm
8 8
9 DOCKERFILE="$PLATFORM"
10 OS_RELEASE="${PLATFORM//[a-z]/}"
11 case "$PLATFORM" in
12 fedora*)
13 DOCKERFILE="${PLATFORM//[0-9]/}.template"
14 ;;
15 esac
16
9 17 DOCKER=$($BUILDDIR/hg-docker docker-path)
10 18
11 19 CONTAINER=hg-docker-$PLATFORM
12 20
13 21 if [[ -z "${HG_DOCKER_OWN_USER}" ]]; then
14 22 DOCKERUID=1000
15 23 DOCKERGID=1000
16 24 else
17 25 DOCKERUID=$(id -u)
18 26 DOCKERGID=$(id -g)
19 27 fi
20 28
21 $BUILDDIR/hg-docker build --build-arg UID=$DOCKERUID --build-arg GID=$DOCKERGID $BUILDDIR/docker/$PLATFORM $CONTAINER
29 $BUILDDIR/hg-docker build \
30 --build-arg UID=$DOCKERUID \
31 --build-arg GID=$DOCKERGID \
32 --build-arg OS_RELEASE=${OS_RELEASE:-latest} \
33 $BUILDDIR/docker/$DOCKERFILE $CONTAINER
22 34
23 35 RPMBUILDDIR=$ROOTDIR/packages/$PLATFORM
36 mkdir -p $RPMBUILDDIR
24 37 $ROOTDIR/contrib/packaging/buildrpm --rpmbuilddir $RPMBUILDDIR --prepare $*
25 38
26 39 DSHARED=/mnt/shared
27 40 DBUILDUSER=build
28 41
29 42 $DOCKER run -e http_proxy -e https_proxy -u $DBUILDUSER --rm -v $RPMBUILDDIR:$DSHARED $CONTAINER \
30 43 rpmbuild --define "_topdir $DSHARED" -ba $DSHARED/SPECS/mercurial.spec --clean
31 44
32 45 $DOCKER run -e http_proxy -e https_proxy -u $DBUILDUSER --rm -v $RPMBUILDDIR:$DSHARED $CONTAINER \
33 46 createrepo $DSHARED
34 47
35 48 cat << EOF > $RPMBUILDDIR/mercurial.repo
36 49 # Place this file in /etc/yum.repos.d/mercurial.repo
37 50 [mercurial]
38 51 name=Mercurial packages for $PLATFORM
39 52 # baseurl=file://$RPMBUILDDIR/
40 53 baseurl=http://hg.example.com/build/$PLATFORM/
41 54 skip_if_unavailable=True
42 55 gpgcheck=0
43 56 enabled=1
44 57 EOF
45 58
46 59 echo
47 60 echo "Build complete - results can be found in $RPMBUILDDIR"
@@ -1,38 +1,120 b''
1 1 #
2 2 # This file is autogenerated by pip-compile
3 3 # To update, run:
4 4 #
5 # pip-compile --generate-hashes contrib/packaging/inno/requirements.txt.in -o contrib/packaging/inno/requirements.txt
5 # pip-compile --generate-hashes --output-file=contrib/packaging/inno/requirements.txt contrib/packaging/inno/requirements.txt.in
6 6 #
7 certifi==2018.11.29 \
8 --hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \
9 --hash=sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033 \
7 certifi==2019.9.11 \
8 --hash=sha256:e4f3620cfea4f83eedc95b24abd9cd56f3c4b146dd0177e83a21b4eb49e21e50 \
9 --hash=sha256:fd7c7c74727ddcf00e9acd26bba8da604ffec95bf1c2144e67aff7a8b50e6cef \
10 10 # via dulwich
11 configparser==3.7.3 \
12 --hash=sha256:27594cf4fc279f321974061ac69164aaebd2749af962ac8686b20503ac0bcf2d \
13 --hash=sha256:9d51fe0a382f05b6b117c5e601fc219fede4a8c71703324af3f7d883aef476a3 \
11 cffi==1.13.1 \
12 --hash=sha256:00d890313797d9fe4420506613384b43099ad7d2b905c0752dbcc3a6f14d80fa \
13 --hash=sha256:0cf9e550ac6c5e57b713437e2f4ac2d7fd0cd10336525a27224f5fc1ec2ee59a \
14 --hash=sha256:0ea23c9c0cdd6778146a50d867d6405693ac3b80a68829966c98dd5e1bbae400 \
15 --hash=sha256:193697c2918ecdb3865acf6557cddf5076bb39f1f654975e087b67efdff83365 \
16 --hash=sha256:1ae14b542bf3b35e5229439c35653d2ef7d8316c1fffb980f9b7647e544baa98 \
17 --hash=sha256:1e389e069450609c6ffa37f21f40cce36f9be7643bbe5051ab1de99d5a779526 \
18 --hash=sha256:263242b6ace7f9cd4ea401428d2d45066b49a700852334fd55311bde36dcda14 \
19 --hash=sha256:33142ae9807665fa6511cfa9857132b2c3ee6ddffb012b3f0933fc11e1e830d5 \
20 --hash=sha256:364f8404034ae1b232335d8c7f7b57deac566f148f7222cef78cf8ae28ef764e \
21 --hash=sha256:47368f69fe6529f8f49a5d146ddee713fc9057e31d61e8b6dc86a6a5e38cecc1 \
22 --hash=sha256:4895640844f17bec32943995dc8c96989226974dfeb9dd121cc45d36e0d0c434 \
23 --hash=sha256:558b3afef987cf4b17abd849e7bedf64ee12b28175d564d05b628a0f9355599b \
24 --hash=sha256:5ba86e1d80d458b338bda676fd9f9d68cb4e7a03819632969cf6d46b01a26730 \
25 --hash=sha256:63424daa6955e6b4c70dc2755897f5be1d719eabe71b2625948b222775ed5c43 \
26 --hash=sha256:6381a7d8b1ebd0bc27c3bc85bc1bfadbb6e6f756b4d4db0aa1425c3719ba26b4 \
27 --hash=sha256:6381ab708158c4e1639da1f2a7679a9bbe3e5a776fc6d1fd808076f0e3145331 \
28 --hash=sha256:6fd58366747debfa5e6163ada468a90788411f10c92597d3b0a912d07e580c36 \
29 --hash=sha256:728ec653964655d65408949b07f9b2219df78badd601d6c49e28d604efe40599 \
30 --hash=sha256:7cfcfda59ef1f95b9f729c56fe8a4041899f96b72685d36ef16a3440a0f85da8 \
31 --hash=sha256:819f8d5197c2684524637f940445c06e003c4a541f9983fd30d6deaa2a5487d8 \
32 --hash=sha256:825ecffd9574557590e3225560a8a9d751f6ffe4a49e3c40918c9969b93395fa \
33 --hash=sha256:9009e917d8f5ef780c2626e29b6bc126f4cb2a4d43ca67aa2b40f2a5d6385e78 \
34 --hash=sha256:9c77564a51d4d914ed5af096cd9843d90c45b784b511723bd46a8a9d09cf16fc \
35 --hash=sha256:a19089fa74ed19c4fe96502a291cfdb89223a9705b1d73b3005df4256976142e \
36 --hash=sha256:a40ed527bffa2b7ebe07acc5a3f782da072e262ca994b4f2085100b5a444bbb2 \
37 --hash=sha256:bb75ba21d5716abc41af16eac1145ab2e471deedde1f22c6f99bd9f995504df0 \
38 --hash=sha256:e22a00c0c81ffcecaf07c2bfb3672fa372c50e2bd1024ffee0da191c1b27fc71 \
39 --hash=sha256:e55b5a746fb77f10c83e8af081979351722f6ea48facea79d470b3731c7b2891 \
40 --hash=sha256:ec2fa3ee81707a5232bf2dfbd6623fdb278e070d596effc7e2d788f2ada71a05 \
41 --hash=sha256:fd82eb4694be712fcae03c717ca2e0fc720657ac226b80bbb597e971fc6928c2 \
42 # via cryptography
43 configparser==4.0.2 \
44 --hash=sha256:254c1d9c79f60c45dfde850850883d5aaa7f19a23f13561243a050d5a7c3fe4c \
45 --hash=sha256:c7d282687a5308319bf3d2e7706e575c635b0a470342641c93bea0ea3b5331df \
14 46 # via entrypoints
15 docutils==0.14 \
16 --hash=sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6 \
17 --hash=sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274 \
18 --hash=sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6
19 dulwich==0.19.11 \
20 --hash=sha256:afbe070f6899357e33f63f3f3696e601731fef66c64a489dea1bc9f539f4a725
47 cryptography==2.8 \
48 --hash=sha256:02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c \
49 --hash=sha256:1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595 \
50 --hash=sha256:369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad \
51 --hash=sha256:3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651 \
52 --hash=sha256:44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2 \
53 --hash=sha256:4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff \
54 --hash=sha256:58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d \
55 --hash=sha256:6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42 \
56 --hash=sha256:7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d \
57 --hash=sha256:73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e \
58 --hash=sha256:7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912 \
59 --hash=sha256:90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793 \
60 --hash=sha256:971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13 \
61 --hash=sha256:a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7 \
62 --hash=sha256:b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0 \
63 --hash=sha256:b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879 \
64 --hash=sha256:d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f \
65 --hash=sha256:de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9 \
66 --hash=sha256:df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2 \
67 --hash=sha256:ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf \
68 --hash=sha256:fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8 \
69 # via secretstorage
70 docutils==0.15.2 \
71 --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \
72 --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \
73 --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99
74 dulwich==0.19.13 \
75 --hash=sha256:0e442f6f96e6d97270a7cca4e75306b6b0228627bdf57dde3759e0e345a6b523 \
76 --hash=sha256:667f49536ccba09d3b90bac80d44048e45566f84b98a5e139cc8c70757a6ae60 \
77 --hash=sha256:82792a9d49b112fa2151fa0fb29b01667855a843ff99325b1c1578a4aec11b57 \
78 --hash=sha256:aa628449c5f594a9a282f4d9e5993fef65481ef5e3b9b6c52ff31200f8f5dc95 \
79 --hash=sha256:ab4668bc4e1996d12eb1910e123a09edcff8e166e7ec46db5aafb5c7e250b99f \
80 --hash=sha256:c35ed2cd5b263ce0d67758ffba590c0466ff13b048457ff060b7d2e6cb55a40e \
81 --hash=sha256:c8b48079a14850cbeb788b38e1061ae6db75061431c1c0f91382460be4c84bbe \
82 --hash=sha256:dfcd9943c69f963dd61a027f480d16f548ea5905c2485be8f4b8f130df2c32de \
83 --hash=sha256:e3693c3238c1a5fc1e4427281c4455d78549f4797f2a7107a5f4443b21efafb4
21 84 entrypoints==0.3 \
22 85 --hash=sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19 \
23 86 --hash=sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451 \
24 87 # via keyring
88 enum34==1.1.6 \
89 --hash=sha256:2d81cbbe0e73112bdfe6ef8576f2238f2ba27dd0d55752a776c41d38b7da2850 \
90 --hash=sha256:644837f692e5f550741432dd3f223bbb9852018674981b1664e5dc339387588a \
91 --hash=sha256:6bd0f6ad48ec2aa117d3d141940d484deccda84d4fcd884f5c3d93c23ecd8c79 \
92 --hash=sha256:8ad8c4783bf61ded74527bffb48ed9b54166685e4230386a9ed9b1279e2df5b1 \
93 # via cryptography
94 ipaddress==1.0.23 \
95 --hash=sha256:6e0f4a39e66cb5bb9a137b00276a2eff74f93b71dcbdad6f10ff7df9d3557fcc \
96 --hash=sha256:b7f8e0369580bb4a24d5ba1d7cc29660a4a6987763faf1d8a8046830e020e7e2 \
97 # via cryptography
25 98 keyring==18.0.1 \
26 99 --hash=sha256:67d6cc0132bd77922725fae9f18366bb314fd8f95ff4d323a4df41890a96a838 \
27 100 --hash=sha256:7b29ebfcf8678c4da531b2478a912eea01e80007e5ddca9ee0c7038cb3489ec6
28 pygments==2.3.1 \
29 --hash=sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a \
30 --hash=sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d
101 pycparser==2.19 \
102 --hash=sha256:a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3 \
103 # via cffi
104 pygments==2.4.2 \
105 --hash=sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127 \
106 --hash=sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297
31 107 pywin32-ctypes==0.2.0 \
32 108 --hash=sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942 \
33 --hash=sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98 \
109 --hash=sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98
110 secretstorage==2.3.1 \
111 --hash=sha256:3af65c87765323e6f64c83575b05393f9e003431959c9395d1791d51497f29b6 \
34 112 # via keyring
35 urllib3==1.24.1 \
36 --hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \
37 --hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22 \
113 six==1.12.0 \
114 --hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
115 --hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73 \
116 # via cryptography
117 urllib3==1.25.6 \
118 --hash=sha256:3de946ffbed6e6746608990594d08faac602528ac7015ac28d33cee6a45b7398 \
119 --hash=sha256:9a107b99a5393caf59c7aa3c1249c16e6879447533d0887f4336dde834c7be86 \
38 120 # via dulwich
@@ -1,4 +1,7 b''
1 1 docutils
2 2 dulwich
3 3 keyring
4 4 pygments
5 # Need to list explicitly so dependency gets pulled in when
6 # not running on Windows.
7 pywin32-ctypes
@@ -1,164 +1,172 b''
1 1 %global emacs_lispdir %{_datadir}/emacs/site-lisp
2 2
3 3 %define withpython %{nil}
4 4
5 %global pythonexe python3
6
5 7 %if "%{?withpython}"
6 8
7 9 %global pythonver %{withpython}
8 10 %global pythonname Python-%{withpython}
9 11 %global docutilsname docutils-0.14
10 12 %global docutilsmd5 c53768d63db3873b7d452833553469de
11 13 %global pythonhg python-hg
12 14 %global hgpyprefix /opt/%{pythonhg}
13 15 # byte compilation will fail on some some Python /test/ files
14 16 %global _python_bytecompile_errors_terminate_build 0
15 17
16 18 %else
17 19
18 %global pythonver %(python -c 'import sys;print ".".join(map(str, sys.version_info[:2]))')
20 %global pythonver %(%{pythonexe} -c 'import sys;print(".".join(map(str, sys.version_info[:2])))')
19 21
20 22 %endif
21 23
22 24 Summary: A fast, lightweight Source Control Management system
23 25 Name: mercurial
24 26 Version: snapshot
25 27 Release: 0
26 28 License: GPLv2+
27 29 Group: Development/Tools
28 30 URL: https://mercurial-scm.org/
29 31 Source0: %{name}-%{version}-%{release}.tar.gz
30 32 %if "%{?withpython}"
31 33 Source1: %{pythonname}.tgz
32 34 Source2: %{docutilsname}.tar.gz
33 35 %endif
34 36 BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root
35 37
36 38 BuildRequires: make, gcc, gettext
37 39 %if "%{?withpython}"
38 40 BuildRequires: readline-devel, openssl-devel, ncurses-devel, zlib-devel, bzip2-devel
39 41 %else
40 BuildRequires: python >= 2.7, python-devel, python-docutils >= 0.5
41 Requires: python >= 2.7
42 BuildRequires: %{pythonexe} >= %{pythonver}, %{pythonexe}-devel, %{pythonexe}-docutils >= 0.5
43 Requires: %{pythonexe} >= %{pythonver}
42 44 %endif
43 45 # The hgk extension uses the wish tcl interpreter, but we don't enforce it
44 46 #Requires: tk
45 47
46 48 %description
47 49 Mercurial is a fast, lightweight source control management system designed
48 50 for efficient handling of very large distributed projects.
49 51
50 52 %prep
51 53
52 54 %if "%{?withpython}"
53 55 %setup -q -n mercurial-%{version}-%{release} -a1 -a2
54 56 # despite the comments in cgi.py, we do this to prevent rpmdeps from picking /usr/local/bin/python up
55 sed -i '1c#! /usr/bin/env python' %{pythonname}/Lib/cgi.py
57 sed -i '1c#! /usr/bin/env %{pythonexe}' %{pythonname}/Lib/cgi.py
56 58 %else
57 59 %setup -q -n mercurial-%{version}-%{release}
58 60 %endif
59 61
60 62 %build
61 63
64 export HGPYTHON3=1
65
62 66 %if "%{?withpython}"
63 67
64 68 PYPATH=$PWD/%{pythonname}
65 69 cd $PYPATH
66 70 ./configure --prefix=%{hgpyprefix}
67 71 make all %{?_smp_mflags}
68 72 cd -
69 73
70 74 cd %{docutilsname}
71 75 LD_LIBRARY_PATH=$PYPATH $PYPATH/python setup.py build
72 76 cd -
73 77
74 78 # verify Python environment
75 79 LD_LIBRARY_PATH=$PYPATH PYTHONPATH=$PWD/%{docutilsname} $PYPATH/python -c 'import sys, zlib, bz2, ssl, curses, readline'
76 80
77 81 # set environment for make
78 82 export PATH=$PYPATH:$PATH
79 83 export LD_LIBRARY_PATH=$PYPATH
80 84 export CFLAGS="-L $PYPATH"
81 85 export PYTHONPATH=$PWD/%{docutilsname}
82 86
83 87 %endif
84 88
85 make all
89 make all PYTHON=%{pythonexe}
86 90 make -C contrib/chg
87 91
92 sed -i -e '1s|#!/usr/bin/env python$|#!/usr/bin/env %{pythonexe}|' contrib/hg-ssh
93
88 94 %install
89 95 rm -rf $RPM_BUILD_ROOT
90 96
97 export HGPYTHON3=1
98
91 99 %if "%{?withpython}"
92 100
93 101 PYPATH=$PWD/%{pythonname}
94 102 cd $PYPATH
95 103 make install DESTDIR=$RPM_BUILD_ROOT
96 104 # these .a are not necessary and they are readonly and strip fails - kill them!
97 105 rm -f %{buildroot}%{hgpyprefix}/lib/{,python2.*/config}/libpython2.*.a
98 106 cd -
99 107
100 108 cd %{docutilsname}
101 109 LD_LIBRARY_PATH=$PYPATH $PYPATH/python setup.py install --root="$RPM_BUILD_ROOT"
102 110 cd -
103 111
104 PATH=$PYPATH:$PATH LD_LIBRARY_PATH=$PYPATH make install DESTDIR=$RPM_BUILD_ROOT PREFIX=%{hgpyprefix} MANDIR=%{_mandir}
112 PATH=$PYPATH:$PATH LD_LIBRARY_PATH=$PYPATH make install PYTHON=%{pythonexe} DESTDIR=$RPM_BUILD_ROOT PREFIX=%{hgpyprefix} MANDIR=%{_mandir}
105 113 mkdir -p $RPM_BUILD_ROOT%{_bindir}
106 114 ( cd $RPM_BUILD_ROOT%{_bindir}/ && ln -s ../..%{hgpyprefix}/bin/hg . )
107 115 ( cd $RPM_BUILD_ROOT%{_bindir}/ && ln -s ../..%{hgpyprefix}/bin/python2.? %{pythonhg} )
108 116
109 117 %else
110 118
111 make install DESTDIR=$RPM_BUILD_ROOT PREFIX=%{_prefix} MANDIR=%{_mandir}
119 make install PYTHON=%{pythonexe} DESTDIR=$RPM_BUILD_ROOT PREFIX=%{_prefix} MANDIR=%{_mandir}
112 120
113 121 %endif
114 122
115 123 install -m 755 contrib/chg/chg $RPM_BUILD_ROOT%{_bindir}/
116 124 install -m 755 contrib/hgk $RPM_BUILD_ROOT%{_bindir}/
117 125 install -m 755 contrib/hg-ssh $RPM_BUILD_ROOT%{_bindir}/
118 126
119 127 bash_completion_dir=$RPM_BUILD_ROOT%{_sysconfdir}/bash_completion.d
120 128 mkdir -p $bash_completion_dir
121 129 install -m 644 contrib/bash_completion $bash_completion_dir/mercurial.sh
122 130
123 131 zsh_completion_dir=$RPM_BUILD_ROOT%{_datadir}/zsh/site-functions
124 132 mkdir -p $zsh_completion_dir
125 133 install -m 644 contrib/zsh_completion $zsh_completion_dir/_mercurial
126 134
127 135 mkdir -p $RPM_BUILD_ROOT%{emacs_lispdir}
128 136 install -m 644 contrib/mercurial.el $RPM_BUILD_ROOT%{emacs_lispdir}/
129 137 install -m 644 contrib/mq.el $RPM_BUILD_ROOT%{emacs_lispdir}/
130 138
131 139 mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/mercurial/hgrc.d
132 140
133 141 %clean
134 142 rm -rf $RPM_BUILD_ROOT
135 143
136 144 %files
137 145 %defattr(-,root,root,-)
138 %doc CONTRIBUTORS COPYING doc/README doc/hg*.txt doc/hg*.html *.cgi contrib/*.fcgi
146 %doc CONTRIBUTORS COPYING doc/README doc/hg*.txt doc/hg*.html *.cgi contrib/*.fcgi contrib/*.wsgi
139 147 %doc %attr(644,root,root) %{_mandir}/man?/hg*
140 148 %doc %attr(644,root,root) contrib/*.svg
141 149 %dir %{_datadir}/zsh/
142 150 %dir %{_datadir}/zsh/site-functions/
143 151 %{_datadir}/zsh/site-functions/_mercurial
144 152 %dir %{_datadir}/emacs/site-lisp/
145 153 %{_datadir}/emacs/site-lisp/mercurial.el
146 154 %{_datadir}/emacs/site-lisp/mq.el
147 155 %{_bindir}/hg
148 156 %{_bindir}/chg
149 157 %{_bindir}/hgk
150 158 %{_bindir}/hg-ssh
151 159 %dir %{_sysconfdir}/bash_completion.d/
152 160 %config(noreplace) %{_sysconfdir}/bash_completion.d/mercurial.sh
153 161 %dir %{_sysconfdir}/mercurial
154 162 %dir %{_sysconfdir}/mercurial/hgrc.d
155 163 %if "%{?withpython}"
156 164 %{_bindir}/%{pythonhg}
157 165 %{hgpyprefix}
158 166 %else
159 167 %{_libdir}/python%{pythonver}/site-packages/%{name}-*-py%{pythonver}.egg-info
160 168 %{_libdir}/python%{pythonver}/site-packages/%{name}
161 169 %{_libdir}/python%{pythonver}/site-packages/hgext
162 170 %{_libdir}/python%{pythonver}/site-packages/hgext3rd
163 171 %{_libdir}/python%{pythonver}/site-packages/hgdemandimport
164 172 %endif
@@ -1,13 +1,13 b''
1 1 #
2 2 # This file is autogenerated by pip-compile
3 3 # To update, run:
4 4 #
5 # pip-compile --generate-hashes contrib/packaging/wix/requirements.txt.in -o contrib/packaging/wix/requirements.txt
5 # pip-compile --generate-hashes --output-file=contrib/packaging/wix/requirements.txt contrib/packaging/wix/requirements.txt.in
6 6 #
7 docutils==0.14 \
8 --hash=sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6 \
9 --hash=sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274 \
10 --hash=sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6
11 pygments==2.3.1 \
12 --hash=sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a \
13 --hash=sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d
7 docutils==0.15.2 \
8 --hash=sha256:6c4f696463b79f1fb8ba0c594b63840ebd41f059e92b31957c46b74a4599b6d0 \
9 --hash=sha256:9e4d7ecfc600058e07ba661411a2b7de2fd0fafa17d1a7f7361cd47b1175c827 \
10 --hash=sha256:a2aeea129088da402665e92e0b25b04b073c04b2dce4ab65caaa38b7ce2e1a99
11 pygments==2.4.2 \
12 --hash=sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127 \
13 --hash=sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297
@@ -1,87 +1,87 b''
1 1 #!/usr/bin/env python
2 2 from __future__ import absolute_import
3 3
4 4 import getopt
5 5 import sys
6 6
7 7 import hgdemandimport
8 8 hgdemandimport.enable()
9 9
10 10 from mercurial.i18n import _
11 11 from mercurial import (
12 12 context,
13 13 error,
14 14 fancyopts,
15 15 pycompat,
16 16 simplemerge,
17 17 ui as uimod,
18 18 )
19 19 from mercurial.utils import (
20 20 procutil,
21 stringutil
21 22 )
22 23
23 24 options = [(b'L', b'label', [], _(b'labels to use on conflict markers')),
24 25 (b'a', b'text', None, _(b'treat all files as text')),
25 26 (b'p', b'print', None,
26 27 _(b'print results instead of overwriting LOCAL')),
27 28 (b'', b'no-minimal', None, _(b'no effect (DEPRECATED)')),
28 29 (b'h', b'help', None, _(b'display help and exit')),
29 30 (b'q', b'quiet', None, _(b'suppress output'))]
30 31
31 32 usage = _(b'''simplemerge [OPTS] LOCAL BASE OTHER
32 33
33 34 Simple three-way file merge utility with a minimal feature set.
34 35
35 36 Apply to LOCAL the changes necessary to go from BASE to OTHER.
36 37
37 38 By default, LOCAL is overwritten with the results of this operation.
38 39 ''')
39 40
40 41 class ParseError(Exception):
41 42 """Exception raised on errors in parsing the command line."""
42 43
43 44 def showhelp():
44 45 pycompat.stdout.write(usage)
45 46 pycompat.stdout.write(b'\noptions:\n')
46 47
47 48 out_opts = []
48 49 for shortopt, longopt, default, desc in options:
49 50 out_opts.append((b'%2s%s' % (shortopt and b'-%s' % shortopt,
50 51 longopt and b' --%s' % longopt),
51 52 b'%s' % desc))
52 53 opts_len = max([len(opt[0]) for opt in out_opts])
53 54 for first, second in out_opts:
54 55 pycompat.stdout.write(b' %-*s %s\n' % (opts_len, first, second))
55 56
56 57 try:
57 58 for fp in (sys.stdin, pycompat.stdout, sys.stderr):
58 59 procutil.setbinary(fp)
59 60
60 61 opts = {}
61 62 try:
62 63 bargv = [a.encode('utf8') for a in sys.argv[1:]]
63 64 args = fancyopts.fancyopts(bargv, options, opts)
64 65 except getopt.GetoptError as e:
65 66 raise ParseError(e)
66 67 if opts[b'help']:
67 68 showhelp()
68 69 sys.exit(0)
69 70 if len(args) != 3:
70 71 raise ParseError(_(b'wrong number of arguments').decode('utf8'))
71 72 local, base, other = args
72 73 sys.exit(simplemerge.simplemerge(uimod.ui.load(),
73 74 context.arbitraryfilectx(local),
74 75 context.arbitraryfilectx(base),
75 76 context.arbitraryfilectx(other),
76 77 **pycompat.strkwargs(opts)))
77 78 except ParseError as e:
78 if pycompat.ispy3:
79 e = str(e).encode('utf8')
79 e = stringutil.forcebytestr(e)
80 80 pycompat.stdout.write(b"%s: %s\n" % (sys.argv[0].encode('utf8'), e))
81 81 showhelp()
82 82 sys.exit(1)
83 83 except error.Abort as e:
84 84 pycompat.stderr.write(b"abort: %s\n" % e)
85 85 sys.exit(255)
86 86 except KeyboardInterrupt:
87 87 sys.exit(255)
@@ -1,124 +1,128 b''
1 1 # demandimportpy3 - global demand-loading of modules for Mercurial
2 2 #
3 3 # Copyright 2017 Facebook Inc.
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 """Lazy loading for Python 3.6 and above.
9 9
10 10 This uses the new importlib finder/loader functionality available in Python 3.5
11 11 and up. The code reuses most of the mechanics implemented inside importlib.util,
12 12 but with a few additions:
13 13
14 14 * Allow excluding certain modules from lazy imports.
15 15 * Expose an interface that's substantially the same as demandimport for
16 16 Python 2.
17 17
18 18 This also has some limitations compared to the Python 2 implementation:
19 19
20 20 * Much of the logic is per-package, not per-module, so any packages loaded
21 21 before demandimport is enabled will not be lazily imported in the future. In
22 22 practice, we only expect builtins to be loaded before demandimport is
23 23 enabled.
24 24 """
25 25
26 26 # This line is unnecessary, but it satisfies test-check-py3-compat.t.
27 27 from __future__ import absolute_import
28 28
29 29 import contextlib
30 30 import importlib.abc
31 31 import importlib.machinery
32 32 import importlib.util
33 33 import sys
34 34
35 35 from . import tracing
36 36
37 37 _deactivated = False
38 38
39 39
40 40 class _lazyloaderex(importlib.util.LazyLoader):
41 41 """This is a LazyLoader except it also follows the _deactivated global and
42 42 the ignore list.
43 43 """
44 44
45 45 def exec_module(self, module):
46 46 """Make the module load lazily."""
47 47 with tracing.log('demandimport %s', module):
48 48 if _deactivated or module.__name__ in ignores:
49 49 self.loader.exec_module(module)
50 50 else:
51 51 super().exec_module(module)
52 52
53 53
54 54 # This is 3.6+ because with Python 3.5 it isn't possible to lazily load
55 55 # extensions. See the discussion in https://bugs.python.org/issue26186 for more.
56 _extensions_loader = _lazyloaderex.factory(
57 importlib.machinery.ExtensionFileLoader
58 )
56 if sys.version_info[0:2] >= (3, 6):
57 _extensions_loader = _lazyloaderex.factory(
58 importlib.machinery.ExtensionFileLoader
59 )
60 else:
61 _extensions_loader = importlib.machinery.ExtensionFileLoader
62
59 63 _bytecode_loader = _lazyloaderex.factory(
60 64 importlib.machinery.SourcelessFileLoader
61 65 )
62 66 _source_loader = _lazyloaderex.factory(importlib.machinery.SourceFileLoader)
63 67
64 68
65 69 def _makefinder(path):
66 70 return importlib.machinery.FileFinder(
67 71 path,
68 72 # This is the order in which loaders are passed in in core Python.
69 73 (_extensions_loader, importlib.machinery.EXTENSION_SUFFIXES),
70 74 (_source_loader, importlib.machinery.SOURCE_SUFFIXES),
71 75 (_bytecode_loader, importlib.machinery.BYTECODE_SUFFIXES),
72 76 )
73 77
74 78
75 79 ignores = set()
76 80
77 81
78 82 def init(ignoreset):
79 83 global ignores
80 84 ignores = ignoreset
81 85
82 86
83 87 def isenabled():
84 88 return _makefinder in sys.path_hooks and not _deactivated
85 89
86 90
87 91 def disable():
88 92 try:
89 93 while True:
90 94 sys.path_hooks.remove(_makefinder)
91 95 except ValueError:
92 96 pass
93 97
94 98
95 99 def enable():
96 100 sys.path_hooks.insert(0, _makefinder)
97 101
98 102
99 103 @contextlib.contextmanager
100 104 def deactivated():
101 105 # This implementation is a bit different from Python 2's. Python 3
102 106 # maintains a per-package finder cache in sys.path_importer_cache (see
103 107 # PEP 302). This means that we can't just call disable + enable.
104 108 # If we do that, in situations like:
105 109 #
106 110 # demandimport.enable()
107 111 # ...
108 112 # from foo.bar import mod1
109 113 # with demandimport.deactivated():
110 114 # from foo.bar import mod2
111 115 #
112 116 # mod2 will be imported lazily. (The converse also holds -- whatever finder
113 117 # first gets cached will be used.)
114 118 #
115 119 # Instead, have a global flag the LazyLoader can use.
116 120 global _deactivated
117 121 demandenabled = isenabled()
118 122 if demandenabled:
119 123 _deactivated = True
120 124 try:
121 125 yield
122 126 finally:
123 127 if demandenabled:
124 128 _deactivated = False
@@ -1,1215 +1,1215 b''
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 # Copyright 2011-4 Jim Hague <jim.hague@acm.org>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''hooks for integrating with the Bugzilla bug tracker
10 10
11 11 This hook extension adds comments on bugs in Bugzilla when changesets
12 12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
13 13 the Mercurial template mechanism.
14 14
15 15 The bug references can optionally include an update for Bugzilla of the
16 16 hours spent working on the bug. Bugs can also be marked fixed.
17 17
18 18 Four basic modes of access to Bugzilla are provided:
19 19
20 20 1. Access via the Bugzilla REST-API. Requires bugzilla 5.0 or later.
21 21
22 22 2. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
23 23
24 24 3. Check data via the Bugzilla XMLRPC interface and submit bug change
25 25 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
26 26
27 27 4. Writing directly to the Bugzilla database. Only Bugzilla installations
28 28 using MySQL are supported. Requires Python MySQLdb.
29 29
30 30 Writing directly to the database is susceptible to schema changes, and
31 31 relies on a Bugzilla contrib script to send out bug change
32 32 notification emails. This script runs as the user running Mercurial,
33 33 must be run on the host with the Bugzilla install, and requires
34 34 permission to read Bugzilla configuration details and the necessary
35 35 MySQL user and password to have full access rights to the Bugzilla
36 36 database. For these reasons this access mode is now considered
37 37 deprecated, and will not be updated for new Bugzilla versions going
38 38 forward. Only adding comments is supported in this access mode.
39 39
40 40 Access via XMLRPC needs a Bugzilla username and password to be specified
41 41 in the configuration. Comments are added under that username. Since the
42 42 configuration must be readable by all Mercurial users, it is recommended
43 43 that the rights of that user are restricted in Bugzilla to the minimum
44 44 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
45 45
46 46 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
47 47 email to the Bugzilla email interface to submit comments to bugs.
48 48 The From: address in the email is set to the email address of the Mercurial
49 49 user, so the comment appears to come from the Mercurial user. In the event
50 50 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
51 51 user, the email associated with the Bugzilla username used to log into
52 52 Bugzilla is used instead as the source of the comment. Marking bugs fixed
53 53 works on all supported Bugzilla versions.
54 54
55 55 Access via the REST-API needs either a Bugzilla username and password
56 56 or an apikey specified in the configuration. Comments are made under
57 57 the given username or the user associated with the apikey in Bugzilla.
58 58
59 59 Configuration items common to all access modes:
60 60
61 61 bugzilla.version
62 62 The access type to use. Values recognized are:
63 63
64 64 :``restapi``: Bugzilla REST-API, Bugzilla 5.0 and later.
65 65 :``xmlrpc``: Bugzilla XMLRPC interface.
66 66 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
67 67 :``3.0``: MySQL access, Bugzilla 3.0 and later.
68 68 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
69 69 including 3.0.
70 70 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
71 71 including 2.18.
72 72
73 73 bugzilla.regexp
74 74 Regular expression to match bug IDs for update in changeset commit message.
75 75 It must contain one "()" named group ``<ids>`` containing the bug
76 76 IDs separated by non-digit characters. It may also contain
77 77 a named group ``<hours>`` with a floating-point number giving the
78 78 hours worked on the bug. If no named groups are present, the first
79 79 "()" group is assumed to contain the bug IDs, and work time is not
80 80 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
81 81 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
82 82 variations thereof, followed by an hours number prefixed by ``h`` or
83 83 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
84 84
85 85 bugzilla.fixregexp
86 86 Regular expression to match bug IDs for marking fixed in changeset
87 87 commit message. This must contain a "()" named group ``<ids>` containing
88 88 the bug IDs separated by non-digit characters. It may also contain
89 89 a named group ``<hours>`` with a floating-point number giving the
90 90 hours worked on the bug. If no named groups are present, the first
91 91 "()" group is assumed to contain the bug IDs, and work time is not
92 92 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
93 93 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
94 94 variations thereof, followed by an hours number prefixed by ``h`` or
95 95 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
96 96
97 97 bugzilla.fixstatus
98 98 The status to set a bug to when marking fixed. Default ``RESOLVED``.
99 99
100 100 bugzilla.fixresolution
101 101 The resolution to set a bug to when marking fixed. Default ``FIXED``.
102 102
103 103 bugzilla.style
104 104 The style file to use when formatting comments.
105 105
106 106 bugzilla.template
107 107 Template to use when formatting comments. Overrides style if
108 108 specified. In addition to the usual Mercurial keywords, the
109 109 extension specifies:
110 110
111 111 :``{bug}``: The Bugzilla bug ID.
112 112 :``{root}``: The full pathname of the Mercurial repository.
113 113 :``{webroot}``: Stripped pathname of the Mercurial repository.
114 114 :``{hgweb}``: Base URL for browsing Mercurial repositories.
115 115
116 116 Default ``changeset {node|short} in repo {root} refers to bug
117 117 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
118 118
119 119 bugzilla.strip
120 120 The number of path separator characters to strip from the front of
121 121 the Mercurial repository path (``{root}`` in templates) to produce
122 122 ``{webroot}``. For example, a repository with ``{root}``
123 123 ``/var/local/my-project`` with a strip of 2 gives a value for
124 124 ``{webroot}`` of ``my-project``. Default 0.
125 125
126 126 web.baseurl
127 127 Base URL for browsing Mercurial repositories. Referenced from
128 128 templates as ``{hgweb}``.
129 129
130 130 Configuration items common to XMLRPC+email and MySQL access modes:
131 131
132 132 bugzilla.usermap
133 133 Path of file containing Mercurial committer email to Bugzilla user email
134 134 mappings. If specified, the file should contain one mapping per
135 135 line::
136 136
137 137 committer = Bugzilla user
138 138
139 139 See also the ``[usermap]`` section.
140 140
141 141 The ``[usermap]`` section is used to specify mappings of Mercurial
142 142 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
143 143 Contains entries of the form ``committer = Bugzilla user``.
144 144
145 145 XMLRPC and REST-API access mode configuration:
146 146
147 147 bugzilla.bzurl
148 148 The base URL for the Bugzilla installation.
149 149 Default ``http://localhost/bugzilla``.
150 150
151 151 bugzilla.user
152 152 The username to use to log into Bugzilla via XMLRPC. Default
153 153 ``bugs``.
154 154
155 155 bugzilla.password
156 156 The password for Bugzilla login.
157 157
158 158 REST-API access mode uses the options listed above as well as:
159 159
160 160 bugzilla.apikey
161 161 An apikey generated on the Bugzilla instance for api access.
162 162 Using an apikey removes the need to store the user and password
163 163 options.
164 164
165 165 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
166 166 and also:
167 167
168 168 bugzilla.bzemail
169 169 The Bugzilla email address.
170 170
171 171 In addition, the Mercurial email settings must be configured. See the
172 172 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
173 173
174 174 MySQL access mode configuration:
175 175
176 176 bugzilla.host
177 177 Hostname of the MySQL server holding the Bugzilla database.
178 178 Default ``localhost``.
179 179
180 180 bugzilla.db
181 181 Name of the Bugzilla database in MySQL. Default ``bugs``.
182 182
183 183 bugzilla.user
184 184 Username to use to access MySQL server. Default ``bugs``.
185 185
186 186 bugzilla.password
187 187 Password to use to access MySQL server.
188 188
189 189 bugzilla.timeout
190 190 Database connection timeout (seconds). Default 5.
191 191
192 192 bugzilla.bzuser
193 193 Fallback Bugzilla user name to record comments with, if changeset
194 194 committer cannot be found as a Bugzilla user.
195 195
196 196 bugzilla.bzdir
197 197 Bugzilla install directory. Used by default notify. Default
198 198 ``/var/www/html/bugzilla``.
199 199
200 200 bugzilla.notify
201 201 The command to run to get Bugzilla to send bug change notification
202 202 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
203 203 id) and ``user`` (committer bugzilla email). Default depends on
204 204 version; from 2.18 it is "cd %(bzdir)s && perl -T
205 205 contrib/sendbugmail.pl %(id)s %(user)s".
206 206
207 207 Activating the extension::
208 208
209 209 [extensions]
210 210 bugzilla =
211 211
212 212 [hooks]
213 213 # run bugzilla hook on every change pulled or pushed in here
214 214 incoming.bugzilla = python:hgext.bugzilla.hook
215 215
216 216 Example configurations:
217 217
218 218 XMLRPC example configuration. This uses the Bugzilla at
219 219 ``http://my-project.org/bugzilla``, logging in as user
220 220 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
221 221 collection of Mercurial repositories in ``/var/local/hg/repos/``,
222 222 with a web interface at ``http://my-project.org/hg``. ::
223 223
224 224 [bugzilla]
225 225 bzurl=http://my-project.org/bugzilla
226 226 user=bugmail@my-project.org
227 227 password=plugh
228 228 version=xmlrpc
229 229 template=Changeset {node|short} in {root|basename}.
230 230 {hgweb}/{webroot}/rev/{node|short}\\n
231 231 {desc}\\n
232 232 strip=5
233 233
234 234 [web]
235 235 baseurl=http://my-project.org/hg
236 236
237 237 XMLRPC+email example configuration. This uses the Bugzilla at
238 238 ``http://my-project.org/bugzilla``, logging in as user
239 239 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
240 240 collection of Mercurial repositories in ``/var/local/hg/repos/``,
241 241 with a web interface at ``http://my-project.org/hg``. Bug comments
242 242 are sent to the Bugzilla email address
243 243 ``bugzilla@my-project.org``. ::
244 244
245 245 [bugzilla]
246 246 bzurl=http://my-project.org/bugzilla
247 247 user=bugmail@my-project.org
248 248 password=plugh
249 249 version=xmlrpc+email
250 250 bzemail=bugzilla@my-project.org
251 251 template=Changeset {node|short} in {root|basename}.
252 252 {hgweb}/{webroot}/rev/{node|short}\\n
253 253 {desc}\\n
254 254 strip=5
255 255
256 256 [web]
257 257 baseurl=http://my-project.org/hg
258 258
259 259 [usermap]
260 260 user@emaildomain.com=user.name@bugzilladomain.com
261 261
262 262 MySQL example configuration. This has a local Bugzilla 3.2 installation
263 263 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
264 264 the Bugzilla database name is ``bugs`` and MySQL is
265 265 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
266 266 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
267 267 with a web interface at ``http://my-project.org/hg``. ::
268 268
269 269 [bugzilla]
270 270 host=localhost
271 271 password=XYZZY
272 272 version=3.0
273 273 bzuser=unknown@domain.com
274 274 bzdir=/opt/bugzilla-3.2
275 275 template=Changeset {node|short} in {root|basename}.
276 276 {hgweb}/{webroot}/rev/{node|short}\\n
277 277 {desc}\\n
278 278 strip=5
279 279
280 280 [web]
281 281 baseurl=http://my-project.org/hg
282 282
283 283 [usermap]
284 284 user@emaildomain.com=user.name@bugzilladomain.com
285 285
286 286 All the above add a comment to the Bugzilla bug record of the form::
287 287
288 288 Changeset 3b16791d6642 in repository-name.
289 289 http://my-project.org/hg/repository-name/rev/3b16791d6642
290 290
291 291 Changeset commit comment. Bug 1234.
292 292 '''
293 293
294 294 from __future__ import absolute_import
295 295
296 296 import json
297 297 import re
298 298 import time
299 299
300 300 from mercurial.i18n import _
301 301 from mercurial.node import short
302 302 from mercurial import (
303 303 error,
304 304 logcmdutil,
305 305 mail,
306 306 pycompat,
307 307 registrar,
308 308 url,
309 309 util,
310 310 )
311 311 from mercurial.utils import (
312 312 procutil,
313 313 stringutil,
314 314 )
315 315
316 316 xmlrpclib = util.xmlrpclib
317 317
318 318 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
319 319 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
320 320 # be specifying the version(s) of Mercurial they are tested with, or
321 321 # leave the attribute unspecified.
322 322 testedwith = b'ships-with-hg-core'
323 323
324 324 configtable = {}
325 325 configitem = registrar.configitem(configtable)
326 326
327 327 configitem(
328 328 b'bugzilla', b'apikey', default=b'',
329 329 )
330 330 configitem(
331 331 b'bugzilla', b'bzdir', default=b'/var/www/html/bugzilla',
332 332 )
333 333 configitem(
334 334 b'bugzilla', b'bzemail', default=None,
335 335 )
336 336 configitem(
337 337 b'bugzilla', b'bzurl', default=b'http://localhost/bugzilla/',
338 338 )
339 339 configitem(
340 340 b'bugzilla', b'bzuser', default=None,
341 341 )
342 342 configitem(
343 343 b'bugzilla', b'db', default=b'bugs',
344 344 )
345 345 configitem(
346 346 b'bugzilla',
347 347 b'fixregexp',
348 348 default=(
349 349 br'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
350 350 br'(?:nos?\.?|num(?:ber)?s?)?\s*'
351 351 br'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
352 352 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
353 353 ),
354 354 )
355 355 configitem(
356 356 b'bugzilla', b'fixresolution', default=b'FIXED',
357 357 )
358 358 configitem(
359 359 b'bugzilla', b'fixstatus', default=b'RESOLVED',
360 360 )
361 361 configitem(
362 362 b'bugzilla', b'host', default=b'localhost',
363 363 )
364 364 configitem(
365 365 b'bugzilla', b'notify', default=configitem.dynamicdefault,
366 366 )
367 367 configitem(
368 368 b'bugzilla', b'password', default=None,
369 369 )
370 370 configitem(
371 371 b'bugzilla',
372 372 b'regexp',
373 373 default=(
374 374 br'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
375 375 br'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
376 376 br'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?'
377 377 ),
378 378 )
379 379 configitem(
380 380 b'bugzilla', b'strip', default=0,
381 381 )
382 382 configitem(
383 383 b'bugzilla', b'style', default=None,
384 384 )
385 385 configitem(
386 386 b'bugzilla', b'template', default=None,
387 387 )
388 388 configitem(
389 389 b'bugzilla', b'timeout', default=5,
390 390 )
391 391 configitem(
392 392 b'bugzilla', b'user', default=b'bugs',
393 393 )
394 394 configitem(
395 395 b'bugzilla', b'usermap', default=None,
396 396 )
397 397 configitem(
398 398 b'bugzilla', b'version', default=None,
399 399 )
400 400
401 401
402 402 class bzaccess(object):
403 403 '''Base class for access to Bugzilla.'''
404 404
405 405 def __init__(self, ui):
406 406 self.ui = ui
407 407 usermap = self.ui.config(b'bugzilla', b'usermap')
408 408 if usermap:
409 409 self.ui.readconfig(usermap, sections=[b'usermap'])
410 410
411 411 def map_committer(self, user):
412 412 '''map name of committer to Bugzilla user name.'''
413 413 for committer, bzuser in self.ui.configitems(b'usermap'):
414 414 if committer.lower() == user.lower():
415 415 return bzuser
416 416 return user
417 417
418 418 # Methods to be implemented by access classes.
419 419 #
420 420 # 'bugs' is a dict keyed on bug id, where values are a dict holding
421 421 # updates to bug state. Recognized dict keys are:
422 422 #
423 423 # 'hours': Value, float containing work hours to be updated.
424 424 # 'fix': If key present, bug is to be marked fixed. Value ignored.
425 425
426 426 def filter_real_bug_ids(self, bugs):
427 427 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
428 428
429 429 def filter_cset_known_bug_ids(self, node, bugs):
430 430 '''remove bug IDs where node occurs in comment text from bugs.'''
431 431
432 432 def updatebug(self, bugid, newstate, text, committer):
433 433 '''update the specified bug. Add comment text and set new states.
434 434
435 435 If possible add the comment as being from the committer of
436 436 the changeset. Otherwise use the default Bugzilla user.
437 437 '''
438 438
439 439 def notify(self, bugs, committer):
440 440 '''Force sending of Bugzilla notification emails.
441 441
442 442 Only required if the access method does not trigger notification
443 443 emails automatically.
444 444 '''
445 445
446 446
447 447 # Bugzilla via direct access to MySQL database.
448 448 class bzmysql(bzaccess):
449 449 '''Support for direct MySQL access to Bugzilla.
450 450
451 451 The earliest Bugzilla version this is tested with is version 2.16.
452 452
453 453 If your Bugzilla is version 3.4 or above, you are strongly
454 454 recommended to use the XMLRPC access method instead.
455 455 '''
456 456
457 457 @staticmethod
458 458 def sql_buglist(ids):
459 459 '''return SQL-friendly list of bug ids'''
460 460 return b'(' + b','.join(map(str, ids)) + b')'
461 461
462 462 _MySQLdb = None
463 463
464 464 def __init__(self, ui):
465 465 try:
466 466 import MySQLdb as mysql
467 467
468 468 bzmysql._MySQLdb = mysql
469 469 except ImportError as err:
470 470 raise error.Abort(
471 471 _(b'python mysql support not available: %s') % err
472 472 )
473 473
474 474 bzaccess.__init__(self, ui)
475 475
476 476 host = self.ui.config(b'bugzilla', b'host')
477 477 user = self.ui.config(b'bugzilla', b'user')
478 478 passwd = self.ui.config(b'bugzilla', b'password')
479 479 db = self.ui.config(b'bugzilla', b'db')
480 480 timeout = int(self.ui.config(b'bugzilla', b'timeout'))
481 481 self.ui.note(
482 482 _(b'connecting to %s:%s as %s, password %s\n')
483 483 % (host, db, user, b'*' * len(passwd))
484 484 )
485 485 self.conn = bzmysql._MySQLdb.connect(
486 486 host=host, user=user, passwd=passwd, db=db, connect_timeout=timeout
487 487 )
488 488 self.cursor = self.conn.cursor()
489 489 self.longdesc_id = self.get_longdesc_id()
490 490 self.user_ids = {}
491 491 self.default_notify = b"cd %(bzdir)s && ./processmail %(id)s %(user)s"
492 492
493 493 def run(self, *args, **kwargs):
494 494 '''run a query.'''
495 495 self.ui.note(_(b'query: %s %s\n') % (args, kwargs))
496 496 try:
497 497 self.cursor.execute(*args, **kwargs)
498 498 except bzmysql._MySQLdb.MySQLError:
499 499 self.ui.note(_(b'failed query: %s %s\n') % (args, kwargs))
500 500 raise
501 501
502 502 def get_longdesc_id(self):
503 503 '''get identity of longdesc field'''
504 504 self.run(b'select fieldid from fielddefs where name = "longdesc"')
505 505 ids = self.cursor.fetchall()
506 506 if len(ids) != 1:
507 507 raise error.Abort(_(b'unknown database schema'))
508 508 return ids[0][0]
509 509
510 510 def filter_real_bug_ids(self, bugs):
511 511 '''filter not-existing bugs from set.'''
512 512 self.run(
513 513 b'select bug_id from bugs where bug_id in %s'
514 514 % bzmysql.sql_buglist(bugs.keys())
515 515 )
516 516 existing = [id for (id,) in self.cursor.fetchall()]
517 517 for id in bugs.keys():
518 518 if id not in existing:
519 519 self.ui.status(_(b'bug %d does not exist\n') % id)
520 520 del bugs[id]
521 521
522 522 def filter_cset_known_bug_ids(self, node, bugs):
523 523 '''filter bug ids that already refer to this changeset from set.'''
524 524 self.run(
525 525 '''select bug_id from longdescs where
526 526 bug_id in %s and thetext like "%%%s%%"'''
527 527 % (bzmysql.sql_buglist(bugs.keys()), short(node))
528 528 )
529 529 for (id,) in self.cursor.fetchall():
530 530 self.ui.status(
531 531 _(b'bug %d already knows about changeset %s\n')
532 532 % (id, short(node))
533 533 )
534 534 del bugs[id]
535 535
536 536 def notify(self, bugs, committer):
537 537 '''tell bugzilla to send mail.'''
538 538 self.ui.status(_(b'telling bugzilla to send mail:\n'))
539 539 (user, userid) = self.get_bugzilla_user(committer)
540 540 for id in bugs.keys():
541 541 self.ui.status(_(b' bug %s\n') % id)
542 542 cmdfmt = self.ui.config(b'bugzilla', b'notify', self.default_notify)
543 543 bzdir = self.ui.config(b'bugzilla', b'bzdir')
544 544 try:
545 545 # Backwards-compatible with old notify string, which
546 546 # took one string. This will throw with a new format
547 547 # string.
548 548 cmd = cmdfmt % id
549 549 except TypeError:
550 550 cmd = cmdfmt % {b'bzdir': bzdir, b'id': id, b'user': user}
551 551 self.ui.note(_(b'running notify command %s\n') % cmd)
552 552 fp = procutil.popen(b'(%s) 2>&1' % cmd, b'rb')
553 553 out = util.fromnativeeol(fp.read())
554 554 ret = fp.close()
555 555 if ret:
556 556 self.ui.warn(out)
557 557 raise error.Abort(
558 558 _(b'bugzilla notify command %s') % procutil.explainexit(ret)
559 559 )
560 560 self.ui.status(_(b'done\n'))
561 561
562 562 def get_user_id(self, user):
563 563 '''look up numeric bugzilla user id.'''
564 564 try:
565 565 return self.user_ids[user]
566 566 except KeyError:
567 567 try:
568 568 userid = int(user)
569 569 except ValueError:
570 570 self.ui.note(_(b'looking up user %s\n') % user)
571 571 self.run(
572 572 '''select userid from profiles
573 573 where login_name like %s''',
574 574 user,
575 575 )
576 576 all = self.cursor.fetchall()
577 577 if len(all) != 1:
578 578 raise KeyError(user)
579 579 userid = int(all[0][0])
580 580 self.user_ids[user] = userid
581 581 return userid
582 582
583 583 def get_bugzilla_user(self, committer):
584 584 '''See if committer is a registered bugzilla user. Return
585 585 bugzilla username and userid if so. If not, return default
586 586 bugzilla username and userid.'''
587 587 user = self.map_committer(committer)
588 588 try:
589 589 userid = self.get_user_id(user)
590 590 except KeyError:
591 591 try:
592 592 defaultuser = self.ui.config(b'bugzilla', b'bzuser')
593 593 if not defaultuser:
594 594 raise error.Abort(
595 595 _(b'cannot find bugzilla user id for %s') % user
596 596 )
597 597 userid = self.get_user_id(defaultuser)
598 598 user = defaultuser
599 599 except KeyError:
600 600 raise error.Abort(
601 601 _(b'cannot find bugzilla user id for %s or %s')
602 602 % (user, defaultuser)
603 603 )
604 604 return (user, userid)
605 605
606 606 def updatebug(self, bugid, newstate, text, committer):
607 607 '''update bug state with comment text.
608 608
609 609 Try adding comment as committer of changeset, otherwise as
610 610 default bugzilla user.'''
611 611 if len(newstate) > 0:
612 612 self.ui.warn(_(b"Bugzilla/MySQL cannot update bug state\n"))
613 613
614 614 (user, userid) = self.get_bugzilla_user(committer)
615 615 now = time.strftime(r'%Y-%m-%d %H:%M:%S')
616 616 self.run(
617 617 '''insert into longdescs
618 618 (bug_id, who, bug_when, thetext)
619 619 values (%s, %s, %s, %s)''',
620 620 (bugid, userid, now, text),
621 621 )
622 622 self.run(
623 623 '''insert into bugs_activity (bug_id, who, bug_when, fieldid)
624 624 values (%s, %s, %s, %s)''',
625 625 (bugid, userid, now, self.longdesc_id),
626 626 )
627 627 self.conn.commit()
628 628
629 629
630 630 class bzmysql_2_18(bzmysql):
631 631 '''support for bugzilla 2.18 series.'''
632 632
633 633 def __init__(self, ui):
634 634 bzmysql.__init__(self, ui)
635 635 self.default_notify = (
636 636 b"cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
637 637 )
638 638
639 639
640 640 class bzmysql_3_0(bzmysql_2_18):
641 641 '''support for bugzilla 3.0 series.'''
642 642
643 643 def __init__(self, ui):
644 644 bzmysql_2_18.__init__(self, ui)
645 645
646 646 def get_longdesc_id(self):
647 647 '''get identity of longdesc field'''
648 648 self.run(b'select id from fielddefs where name = "longdesc"')
649 649 ids = self.cursor.fetchall()
650 650 if len(ids) != 1:
651 651 raise error.Abort(_(b'unknown database schema'))
652 652 return ids[0][0]
653 653
654 654
655 655 # Bugzilla via XMLRPC interface.
656 656
657 657
658 658 class cookietransportrequest(object):
659 659 """A Transport request method that retains cookies over its lifetime.
660 660
661 661 The regular xmlrpclib transports ignore cookies. Which causes
662 662 a bit of a problem when you need a cookie-based login, as with
663 663 the Bugzilla XMLRPC interface prior to 4.4.3.
664 664
665 665 So this is a helper for defining a Transport which looks for
666 666 cookies being set in responses and saves them to add to all future
667 667 requests.
668 668 """
669 669
670 670 # Inspiration drawn from
671 671 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
672 672 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
673 673
674 674 cookies = []
675 675
676 676 def send_cookies(self, connection):
677 677 if self.cookies:
678 678 for cookie in self.cookies:
679 679 connection.putheader(b"Cookie", cookie)
680 680
681 681 def request(self, host, handler, request_body, verbose=0):
682 682 self.verbose = verbose
683 683 self.accept_gzip_encoding = False
684 684
685 685 # issue XML-RPC request
686 686 h = self.make_connection(host)
687 687 if verbose:
688 688 h.set_debuglevel(1)
689 689
690 690 self.send_request(h, handler, request_body)
691 691 self.send_host(h, host)
692 692 self.send_cookies(h)
693 693 self.send_user_agent(h)
694 694 self.send_content(h, request_body)
695 695
696 696 # Deal with differences between Python 2.6 and 2.7.
697 697 # In the former h is a HTTP(S). In the latter it's a
698 698 # HTTP(S)Connection. Luckily, the 2.6 implementation of
699 699 # HTTP(S) has an underlying HTTP(S)Connection, so extract
700 700 # that and use it.
701 701 try:
702 702 response = h.getresponse()
703 703 except AttributeError:
704 704 response = h._conn.getresponse()
705 705
706 706 # Add any cookie definitions to our list.
707 707 for header in response.msg.getallmatchingheaders(b"Set-Cookie"):
708 708 val = header.split(b": ", 1)[1]
709 709 cookie = val.split(b";", 1)[0]
710 710 self.cookies.append(cookie)
711 711
712 712 if response.status != 200:
713 713 raise xmlrpclib.ProtocolError(
714 714 host + handler,
715 715 response.status,
716 716 response.reason,
717 717 response.msg.headers,
718 718 )
719 719
720 720 payload = response.read()
721 721 parser, unmarshaller = self.getparser()
722 722 parser.feed(payload)
723 723 parser.close()
724 724
725 725 return unmarshaller.close()
726 726
727 727
728 728 # The explicit calls to the underlying xmlrpclib __init__() methods are
729 729 # necessary. The xmlrpclib.Transport classes are old-style classes, and
730 730 # it turns out their __init__() doesn't get called when doing multiple
731 731 # inheritance with a new-style class.
732 732 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
733 733 def __init__(self, use_datetime=0):
734 734 if util.safehasattr(xmlrpclib.Transport, "__init__"):
735 735 xmlrpclib.Transport.__init__(self, use_datetime)
736 736
737 737
738 738 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
739 739 def __init__(self, use_datetime=0):
740 740 if util.safehasattr(xmlrpclib.Transport, "__init__"):
741 741 xmlrpclib.SafeTransport.__init__(self, use_datetime)
742 742
743 743
744 744 class bzxmlrpc(bzaccess):
745 745 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
746 746
747 747 Requires a minimum Bugzilla version 3.4.
748 748 """
749 749
750 750 def __init__(self, ui):
751 751 bzaccess.__init__(self, ui)
752 752
753 753 bzweb = self.ui.config(b'bugzilla', b'bzurl')
754 754 bzweb = bzweb.rstrip(b"/") + b"/xmlrpc.cgi"
755 755
756 756 user = self.ui.config(b'bugzilla', b'user')
757 757 passwd = self.ui.config(b'bugzilla', b'password')
758 758
759 759 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
760 760 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
761 761
762 762 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
763 763 ver = self.bzproxy.Bugzilla.version()[b'version'].split(b'.')
764 764 self.bzvermajor = int(ver[0])
765 765 self.bzverminor = int(ver[1])
766 766 login = self.bzproxy.User.login(
767 767 {b'login': user, b'password': passwd, b'restrict_login': True}
768 768 )
769 769 self.bztoken = login.get(b'token', b'')
770 770
771 771 def transport(self, uri):
772 772 if util.urlreq.urlparse(uri, b"http")[0] == b"https":
773 773 return cookiesafetransport()
774 774 else:
775 775 return cookietransport()
776 776
777 777 def get_bug_comments(self, id):
778 778 """Return a string with all comment text for a bug."""
779 779 c = self.bzproxy.Bug.comments(
780 780 {b'ids': [id], b'include_fields': [b'text'], b'token': self.bztoken}
781 781 )
782 782 return b''.join(
783 783 [t[b'text'] for t in c[b'bugs'][b'%d' % id][b'comments']]
784 784 )
785 785
786 786 def filter_real_bug_ids(self, bugs):
787 787 probe = self.bzproxy.Bug.get(
788 788 {
789 789 b'ids': sorted(bugs.keys()),
790 790 b'include_fields': [],
791 791 b'permissive': True,
792 792 b'token': self.bztoken,
793 793 }
794 794 )
795 795 for badbug in probe[b'faults']:
796 796 id = badbug[b'id']
797 797 self.ui.status(_(b'bug %d does not exist\n') % id)
798 798 del bugs[id]
799 799
800 800 def filter_cset_known_bug_ids(self, node, bugs):
801 801 for id in sorted(bugs.keys()):
802 802 if self.get_bug_comments(id).find(short(node)) != -1:
803 803 self.ui.status(
804 804 _(b'bug %d already knows about changeset %s\n')
805 805 % (id, short(node))
806 806 )
807 807 del bugs[id]
808 808
809 809 def updatebug(self, bugid, newstate, text, committer):
810 810 args = {}
811 811 if b'hours' in newstate:
812 812 args[b'work_time'] = newstate[b'hours']
813 813
814 814 if self.bzvermajor >= 4:
815 815 args[b'ids'] = [bugid]
816 816 args[b'comment'] = {b'body': text}
817 817 if b'fix' in newstate:
818 818 args[b'status'] = self.fixstatus
819 819 args[b'resolution'] = self.fixresolution
820 820 args[b'token'] = self.bztoken
821 821 self.bzproxy.Bug.update(args)
822 822 else:
823 823 if b'fix' in newstate:
824 824 self.ui.warn(
825 825 _(
826 826 b"Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
827 827 b"to mark bugs fixed\n"
828 828 )
829 829 )
830 830 args[b'id'] = bugid
831 831 args[b'comment'] = text
832 832 self.bzproxy.Bug.add_comment(args)
833 833
834 834
835 835 class bzxmlrpcemail(bzxmlrpc):
836 836 """Read data from Bugzilla via XMLRPC, send updates via email.
837 837
838 838 Advantages of sending updates via email:
839 839 1. Comments can be added as any user, not just logged in user.
840 840 2. Bug statuses or other fields not accessible via XMLRPC can
841 841 potentially be updated.
842 842
843 843 There is no XMLRPC function to change bug status before Bugzilla
844 844 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
845 845 But bugs can be marked fixed via email from 3.4 onwards.
846 846 """
847 847
848 848 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
849 849 # in-email fields are specified as '@<fieldname> = <value>'. In
850 850 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
851 851 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
852 852 # compatibility, but rather than rely on this use the new format for
853 853 # 4.0 onwards.
854 854
855 855 def __init__(self, ui):
856 856 bzxmlrpc.__init__(self, ui)
857 857
858 858 self.bzemail = self.ui.config(b'bugzilla', b'bzemail')
859 859 if not self.bzemail:
860 860 raise error.Abort(_(b"configuration 'bzemail' missing"))
861 861 mail.validateconfig(self.ui)
862 862
863 863 def makecommandline(self, fieldname, value):
864 864 if self.bzvermajor >= 4:
865 865 return b"@%s %s" % (fieldname, pycompat.bytestr(value))
866 866 else:
867 867 if fieldname == b"id":
868 868 fieldname = b"bug_id"
869 869 return b"@%s = %s" % (fieldname, pycompat.bytestr(value))
870 870
871 871 def send_bug_modify_email(self, bugid, commands, comment, committer):
872 872 '''send modification message to Bugzilla bug via email.
873 873
874 874 The message format is documented in the Bugzilla email_in.pl
875 875 specification. commands is a list of command lines, comment is the
876 876 comment text.
877 877
878 878 To stop users from crafting commit comments with
879 879 Bugzilla commands, specify the bug ID via the message body, rather
880 880 than the subject line, and leave a blank line after it.
881 881 '''
882 882 user = self.map_committer(committer)
883 883 matches = self.bzproxy.User.get(
884 884 {b'match': [user], b'token': self.bztoken}
885 885 )
886 886 if not matches[b'users']:
887 887 user = self.ui.config(b'bugzilla', b'user')
888 888 matches = self.bzproxy.User.get(
889 889 {b'match': [user], b'token': self.bztoken}
890 890 )
891 891 if not matches[b'users']:
892 892 raise error.Abort(
893 893 _(b"default bugzilla user %s email not found") % user
894 894 )
895 895 user = matches[b'users'][0][b'email']
896 896 commands.append(self.makecommandline(b"id", bugid))
897 897
898 898 text = b"\n".join(commands) + b"\n\n" + comment
899 899
900 900 _charsets = mail._charsets(self.ui)
901 901 user = mail.addressencode(self.ui, user, _charsets)
902 902 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
903 903 msg = mail.mimeencode(self.ui, text, _charsets)
904 904 msg[b'From'] = user
905 905 msg[b'To'] = bzemail
906 906 msg[b'Subject'] = mail.headencode(
907 907 self.ui, b"Bug modification", _charsets
908 908 )
909 909 sendmail = mail.connect(self.ui)
910 910 sendmail(user, bzemail, msg.as_string())
911 911
912 912 def updatebug(self, bugid, newstate, text, committer):
913 913 cmds = []
914 914 if b'hours' in newstate:
915 915 cmds.append(self.makecommandline(b"work_time", newstate[b'hours']))
916 916 if b'fix' in newstate:
917 917 cmds.append(self.makecommandline(b"bug_status", self.fixstatus))
918 918 cmds.append(self.makecommandline(b"resolution", self.fixresolution))
919 919 self.send_bug_modify_email(bugid, cmds, text, committer)
920 920
921 921
922 922 class NotFound(LookupError):
923 923 pass
924 924
925 925
926 926 class bzrestapi(bzaccess):
927 927 """Read and write bugzilla data using the REST API available since
928 928 Bugzilla 5.0.
929 929 """
930 930
931 931 def __init__(self, ui):
932 932 bzaccess.__init__(self, ui)
933 933 bz = self.ui.config(b'bugzilla', b'bzurl')
934 934 self.bzroot = b'/'.join([bz, b'rest'])
935 935 self.apikey = self.ui.config(b'bugzilla', b'apikey')
936 936 self.user = self.ui.config(b'bugzilla', b'user')
937 937 self.passwd = self.ui.config(b'bugzilla', b'password')
938 938 self.fixstatus = self.ui.config(b'bugzilla', b'fixstatus')
939 939 self.fixresolution = self.ui.config(b'bugzilla', b'fixresolution')
940 940
941 941 def apiurl(self, targets, include_fields=None):
942 942 url = b'/'.join([self.bzroot] + [pycompat.bytestr(t) for t in targets])
943 943 qv = {}
944 944 if self.apikey:
945 945 qv[b'api_key'] = self.apikey
946 946 elif self.user and self.passwd:
947 947 qv[b'login'] = self.user
948 948 qv[b'password'] = self.passwd
949 949 if include_fields:
950 950 qv[b'include_fields'] = include_fields
951 951 if qv:
952 952 url = b'%s?%s' % (url, util.urlreq.urlencode(qv))
953 953 return url
954 954
955 955 def _fetch(self, burl):
956 956 try:
957 957 resp = url.open(self.ui, burl)
958 return json.loads(resp.read())
958 return pycompat.json_loads(resp.read())
959 959 except util.urlerr.httperror as inst:
960 960 if inst.code == 401:
961 961 raise error.Abort(_(b'authorization failed'))
962 962 if inst.code == 404:
963 963 raise NotFound()
964 964 else:
965 965 raise
966 966
967 967 def _submit(self, burl, data, method=b'POST'):
968 968 data = json.dumps(data)
969 969 if method == b'PUT':
970 970
971 971 class putrequest(util.urlreq.request):
972 972 def get_method(self):
973 973 return b'PUT'
974 974
975 975 request_type = putrequest
976 976 else:
977 977 request_type = util.urlreq.request
978 978 req = request_type(burl, data, {b'Content-Type': b'application/json'})
979 979 try:
980 980 resp = url.opener(self.ui).open(req)
981 return json.loads(resp.read())
981 return pycompat.json_loads(resp.read())
982 982 except util.urlerr.httperror as inst:
983 983 if inst.code == 401:
984 984 raise error.Abort(_(b'authorization failed'))
985 985 if inst.code == 404:
986 986 raise NotFound()
987 987 else:
988 988 raise
989 989
990 990 def filter_real_bug_ids(self, bugs):
991 991 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
992 992 badbugs = set()
993 993 for bugid in bugs:
994 994 burl = self.apiurl((b'bug', bugid), include_fields=b'status')
995 995 try:
996 996 self._fetch(burl)
997 997 except NotFound:
998 998 badbugs.add(bugid)
999 999 for bugid in badbugs:
1000 1000 del bugs[bugid]
1001 1001
1002 1002 def filter_cset_known_bug_ids(self, node, bugs):
1003 1003 '''remove bug IDs where node occurs in comment text from bugs.'''
1004 1004 sn = short(node)
1005 1005 for bugid in bugs.keys():
1006 1006 burl = self.apiurl(
1007 1007 (b'bug', bugid, b'comment'), include_fields=b'text'
1008 1008 )
1009 1009 result = self._fetch(burl)
1010 1010 comments = result[b'bugs'][pycompat.bytestr(bugid)][b'comments']
1011 1011 if any(sn in c[b'text'] for c in comments):
1012 1012 self.ui.status(
1013 1013 _(b'bug %d already knows about changeset %s\n')
1014 1014 % (bugid, sn)
1015 1015 )
1016 1016 del bugs[bugid]
1017 1017
1018 1018 def updatebug(self, bugid, newstate, text, committer):
1019 1019 '''update the specified bug. Add comment text and set new states.
1020 1020
1021 1021 If possible add the comment as being from the committer of
1022 1022 the changeset. Otherwise use the default Bugzilla user.
1023 1023 '''
1024 1024 bugmod = {}
1025 1025 if b'hours' in newstate:
1026 1026 bugmod[b'work_time'] = newstate[b'hours']
1027 1027 if b'fix' in newstate:
1028 1028 bugmod[b'status'] = self.fixstatus
1029 1029 bugmod[b'resolution'] = self.fixresolution
1030 1030 if bugmod:
1031 1031 # if we have to change the bugs state do it here
1032 1032 bugmod[b'comment'] = {
1033 1033 b'comment': text,
1034 1034 b'is_private': False,
1035 1035 b'is_markdown': False,
1036 1036 }
1037 1037 burl = self.apiurl((b'bug', bugid))
1038 1038 self._submit(burl, bugmod, method=b'PUT')
1039 1039 self.ui.debug(b'updated bug %s\n' % bugid)
1040 1040 else:
1041 1041 burl = self.apiurl((b'bug', bugid, b'comment'))
1042 1042 self._submit(
1043 1043 burl,
1044 1044 {
1045 1045 b'comment': text,
1046 1046 b'is_private': False,
1047 1047 b'is_markdown': False,
1048 1048 },
1049 1049 )
1050 1050 self.ui.debug(b'added comment to bug %s\n' % bugid)
1051 1051
1052 1052 def notify(self, bugs, committer):
1053 1053 '''Force sending of Bugzilla notification emails.
1054 1054
1055 1055 Only required if the access method does not trigger notification
1056 1056 emails automatically.
1057 1057 '''
1058 1058 pass
1059 1059
1060 1060
1061 1061 class bugzilla(object):
1062 1062 # supported versions of bugzilla. different versions have
1063 1063 # different schemas.
1064 1064 _versions = {
1065 1065 b'2.16': bzmysql,
1066 1066 b'2.18': bzmysql_2_18,
1067 1067 b'3.0': bzmysql_3_0,
1068 1068 b'xmlrpc': bzxmlrpc,
1069 1069 b'xmlrpc+email': bzxmlrpcemail,
1070 1070 b'restapi': bzrestapi,
1071 1071 }
1072 1072
1073 1073 def __init__(self, ui, repo):
1074 1074 self.ui = ui
1075 1075 self.repo = repo
1076 1076
1077 1077 bzversion = self.ui.config(b'bugzilla', b'version')
1078 1078 try:
1079 1079 bzclass = bugzilla._versions[bzversion]
1080 1080 except KeyError:
1081 1081 raise error.Abort(
1082 1082 _(b'bugzilla version %s not supported') % bzversion
1083 1083 )
1084 1084 self.bzdriver = bzclass(self.ui)
1085 1085
1086 1086 self.bug_re = re.compile(
1087 1087 self.ui.config(b'bugzilla', b'regexp'), re.IGNORECASE
1088 1088 )
1089 1089 self.fix_re = re.compile(
1090 1090 self.ui.config(b'bugzilla', b'fixregexp'), re.IGNORECASE
1091 1091 )
1092 1092 self.split_re = re.compile(br'\D+')
1093 1093
1094 1094 def find_bugs(self, ctx):
1095 1095 '''return bugs dictionary created from commit comment.
1096 1096
1097 1097 Extract bug info from changeset comments. Filter out any that are
1098 1098 not known to Bugzilla, and any that already have a reference to
1099 1099 the given changeset in their comments.
1100 1100 '''
1101 1101 start = 0
1102 1102 hours = 0.0
1103 1103 bugs = {}
1104 1104 bugmatch = self.bug_re.search(ctx.description(), start)
1105 1105 fixmatch = self.fix_re.search(ctx.description(), start)
1106 1106 while True:
1107 1107 bugattribs = {}
1108 1108 if not bugmatch and not fixmatch:
1109 1109 break
1110 1110 if not bugmatch:
1111 1111 m = fixmatch
1112 1112 elif not fixmatch:
1113 1113 m = bugmatch
1114 1114 else:
1115 1115 if bugmatch.start() < fixmatch.start():
1116 1116 m = bugmatch
1117 1117 else:
1118 1118 m = fixmatch
1119 1119 start = m.end()
1120 1120 if m is bugmatch:
1121 1121 bugmatch = self.bug_re.search(ctx.description(), start)
1122 1122 if b'fix' in bugattribs:
1123 1123 del bugattribs[b'fix']
1124 1124 else:
1125 1125 fixmatch = self.fix_re.search(ctx.description(), start)
1126 1126 bugattribs[b'fix'] = None
1127 1127
1128 1128 try:
1129 1129 ids = m.group(b'ids')
1130 1130 except IndexError:
1131 1131 ids = m.group(1)
1132 1132 try:
1133 1133 hours = float(m.group(b'hours'))
1134 1134 bugattribs[b'hours'] = hours
1135 1135 except IndexError:
1136 1136 pass
1137 1137 except TypeError:
1138 1138 pass
1139 1139 except ValueError:
1140 1140 self.ui.status(_(b"%s: invalid hours\n") % m.group(b'hours'))
1141 1141
1142 1142 for id in self.split_re.split(ids):
1143 1143 if not id:
1144 1144 continue
1145 1145 bugs[int(id)] = bugattribs
1146 1146 if bugs:
1147 1147 self.bzdriver.filter_real_bug_ids(bugs)
1148 1148 if bugs:
1149 1149 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1150 1150 return bugs
1151 1151
1152 1152 def update(self, bugid, newstate, ctx):
1153 1153 '''update bugzilla bug with reference to changeset.'''
1154 1154
1155 1155 def webroot(root):
1156 1156 '''strip leading prefix of repo root and turn into
1157 1157 url-safe path.'''
1158 1158 count = int(self.ui.config(b'bugzilla', b'strip'))
1159 1159 root = util.pconvert(root)
1160 1160 while count > 0:
1161 1161 c = root.find(b'/')
1162 1162 if c == -1:
1163 1163 break
1164 1164 root = root[c + 1 :]
1165 1165 count -= 1
1166 1166 return root
1167 1167
1168 1168 mapfile = None
1169 1169 tmpl = self.ui.config(b'bugzilla', b'template')
1170 1170 if not tmpl:
1171 1171 mapfile = self.ui.config(b'bugzilla', b'style')
1172 1172 if not mapfile and not tmpl:
1173 1173 tmpl = _(
1174 1174 b'changeset {node|short} in repo {root} refers '
1175 1175 b'to bug {bug}.\ndetails:\n\t{desc|tabindent}'
1176 1176 )
1177 1177 spec = logcmdutil.templatespec(tmpl, mapfile)
1178 1178 t = logcmdutil.changesettemplater(self.ui, self.repo, spec)
1179 1179 self.ui.pushbuffer()
1180 1180 t.show(
1181 1181 ctx,
1182 1182 changes=ctx.changeset(),
1183 1183 bug=pycompat.bytestr(bugid),
1184 1184 hgweb=self.ui.config(b'web', b'baseurl'),
1185 1185 root=self.repo.root,
1186 1186 webroot=webroot(self.repo.root),
1187 1187 )
1188 1188 data = self.ui.popbuffer()
1189 1189 self.bzdriver.updatebug(
1190 1190 bugid, newstate, data, stringutil.email(ctx.user())
1191 1191 )
1192 1192
1193 1193 def notify(self, bugs, committer):
1194 1194 '''ensure Bugzilla users are notified of bug change.'''
1195 1195 self.bzdriver.notify(bugs, committer)
1196 1196
1197 1197
1198 1198 def hook(ui, repo, hooktype, node=None, **kwargs):
1199 1199 '''add comment to bugzilla for each changeset that refers to a
1200 1200 bugzilla bug id. only add a comment once per bug, so same change
1201 1201 seen multiple times does not fill bug with duplicate data.'''
1202 1202 if node is None:
1203 1203 raise error.Abort(
1204 1204 _(b'hook type %s does not pass a changeset id') % hooktype
1205 1205 )
1206 1206 try:
1207 1207 bz = bugzilla(ui, repo)
1208 1208 ctx = repo[node]
1209 1209 bugs = bz.find_bugs(ctx)
1210 1210 if bugs:
1211 1211 for bug in bugs:
1212 1212 bz.update(bug, bugs[bug], ctx)
1213 1213 bz.notify(bugs, stringutil.email(ctx.user()))
1214 1214 except Exception as e:
1215 1215 raise error.Abort(_(b'Bugzilla error: %s') % e)
@@ -1,376 +1,378 b''
1 1 # gnuarch.py - GNU Arch support for the convert extension
2 2 #
3 3 # Copyright 2008, 2009 Aleix Conchillo Flaque <aleix@member.fsf.org>
4 4 # and others
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8 from __future__ import absolute_import
9 9
10 import email.parser as emailparser
11 10 import os
12 11 import shutil
13 12 import stat
14 13 import tempfile
15 14
16 15 from mercurial.i18n import _
17 16 from mercurial import (
18 17 encoding,
19 18 error,
19 mail,
20 20 pycompat,
21 21 util,
22 22 )
23 23 from mercurial.utils import (
24 24 dateutil,
25 25 procutil,
26 26 )
27 27 from . import common
28 28
29 29
30 30 class gnuarch_source(common.converter_source, common.commandline):
31 31 class gnuarch_rev(object):
32 32 def __init__(self, rev):
33 33 self.rev = rev
34 34 self.summary = b''
35 35 self.date = None
36 36 self.author = b''
37 37 self.continuationof = None
38 38 self.add_files = []
39 39 self.mod_files = []
40 40 self.del_files = []
41 41 self.ren_files = {}
42 42 self.ren_dirs = {}
43 43
44 44 def __init__(self, ui, repotype, path, revs=None):
45 45 super(gnuarch_source, self).__init__(ui, repotype, path, revs=revs)
46 46
47 47 if not os.path.exists(os.path.join(path, b'{arch}')):
48 48 raise common.NoRepo(
49 49 _(b"%s does not look like a GNU Arch repository") % path
50 50 )
51 51
52 52 # Could use checktool, but we want to check for baz or tla.
53 53 self.execmd = None
54 54 if procutil.findexe(b'baz'):
55 55 self.execmd = b'baz'
56 56 else:
57 57 if procutil.findexe(b'tla'):
58 58 self.execmd = b'tla'
59 59 else:
60 60 raise error.Abort(_(b'cannot find a GNU Arch tool'))
61 61
62 62 common.commandline.__init__(self, ui, self.execmd)
63 63
64 64 self.path = os.path.realpath(path)
65 65 self.tmppath = None
66 66
67 67 self.treeversion = None
68 68 self.lastrev = None
69 69 self.changes = {}
70 70 self.parents = {}
71 71 self.tags = {}
72 self.catlogparser = emailparser.Parser()
73 72 self.encoding = encoding.encoding
74 73 self.archives = []
75 74
76 75 def before(self):
77 76 # Get registered archives
78 77 self.archives = [
79 78 i.rstrip(b'\n') for i in self.runlines0(b'archives', b'-n')
80 79 ]
81 80
82 81 if self.execmd == b'tla':
83 82 output = self.run0(b'tree-version', self.path)
84 83 else:
85 84 output = self.run0(b'tree-version', b'-d', self.path)
86 85 self.treeversion = output.strip()
87 86
88 87 # Get name of temporary directory
89 88 version = self.treeversion.split(b'/')
90 89 self.tmppath = os.path.join(
91 90 pycompat.fsencode(tempfile.gettempdir()), b'hg-%s' % version[1]
92 91 )
93 92
94 93 # Generate parents dictionary
95 94 self.parents[None] = []
96 95 treeversion = self.treeversion
97 96 child = None
98 97 while treeversion:
99 98 self.ui.status(_(b'analyzing tree version %s...\n') % treeversion)
100 99
101 100 archive = treeversion.split(b'/')[0]
102 101 if archive not in self.archives:
103 102 self.ui.status(
104 103 _(
105 104 b'tree analysis stopped because it points to '
106 105 b'an unregistered archive %s...\n'
107 106 )
108 107 % archive
109 108 )
110 109 break
111 110
112 111 # Get the complete list of revisions for that tree version
113 112 output, status = self.runlines(
114 113 b'revisions', b'-r', b'-f', treeversion
115 114 )
116 115 self.checkexit(
117 116 status, b'failed retrieving revisions for %s' % treeversion
118 117 )
119 118
120 119 # No new iteration unless a revision has a continuation-of header
121 120 treeversion = None
122 121
123 122 for l in output:
124 123 rev = l.strip()
125 124 self.changes[rev] = self.gnuarch_rev(rev)
126 125 self.parents[rev] = []
127 126
128 127 # Read author, date and summary
129 128 catlog, status = self.run(b'cat-log', b'-d', self.path, rev)
130 129 if status:
131 130 catlog = self.run0(b'cat-archive-log', rev)
132 131 self._parsecatlog(catlog, rev)
133 132
134 133 # Populate the parents map
135 134 self.parents[child].append(rev)
136 135
137 136 # Keep track of the current revision as the child of the next
138 137 # revision scanned
139 138 child = rev
140 139
141 140 # Check if we have to follow the usual incremental history
142 141 # or if we have to 'jump' to a different treeversion given
143 142 # by the continuation-of header.
144 143 if self.changes[rev].continuationof:
145 144 treeversion = b'--'.join(
146 145 self.changes[rev].continuationof.split(b'--')[:-1]
147 146 )
148 147 break
149 148
150 149 # If we reached a base-0 revision w/o any continuation-of
151 150 # header, it means the tree history ends here.
152 151 if rev[-6:] == b'base-0':
153 152 break
154 153
155 154 def after(self):
156 155 self.ui.debug(b'cleaning up %s\n' % self.tmppath)
157 156 shutil.rmtree(self.tmppath, ignore_errors=True)
158 157
159 158 def getheads(self):
160 159 return self.parents[None]
161 160
162 161 def getfile(self, name, rev):
163 162 if rev != self.lastrev:
164 163 raise error.Abort(_(b'internal calling inconsistency'))
165 164
166 165 if not os.path.lexists(os.path.join(self.tmppath, name)):
167 166 return None, None
168 167
169 168 return self._getfile(name, rev)
170 169
171 170 def getchanges(self, rev, full):
172 171 if full:
173 172 raise error.Abort(_(b"convert from arch does not support --full"))
174 173 self._update(rev)
175 174 changes = []
176 175 copies = {}
177 176
178 177 for f in self.changes[rev].add_files:
179 178 changes.append((f, rev))
180 179
181 180 for f in self.changes[rev].mod_files:
182 181 changes.append((f, rev))
183 182
184 183 for f in self.changes[rev].del_files:
185 184 changes.append((f, rev))
186 185
187 186 for src in self.changes[rev].ren_files:
188 187 to = self.changes[rev].ren_files[src]
189 188 changes.append((src, rev))
190 189 changes.append((to, rev))
191 190 copies[to] = src
192 191
193 192 for src in self.changes[rev].ren_dirs:
194 193 to = self.changes[rev].ren_dirs[src]
195 194 chgs, cps = self._rendirchanges(src, to)
196 195 changes += [(f, rev) for f in chgs]
197 196 copies.update(cps)
198 197
199 198 self.lastrev = rev
200 199 return sorted(set(changes)), copies, set()
201 200
202 201 def getcommit(self, rev):
203 202 changes = self.changes[rev]
204 203 return common.commit(
205 204 author=changes.author,
206 205 date=changes.date,
207 206 desc=changes.summary,
208 207 parents=self.parents[rev],
209 208 rev=rev,
210 209 )
211 210
212 211 def gettags(self):
213 212 return self.tags
214 213
215 214 def _execute(self, cmd, *args, **kwargs):
216 215 cmdline = [self.execmd, cmd]
217 216 cmdline += args
218 217 cmdline = [procutil.shellquote(arg) for arg in cmdline]
219 218 bdevnull = pycompat.bytestr(os.devnull)
220 219 cmdline += [b'>', bdevnull, b'2>', bdevnull]
221 220 cmdline = procutil.quotecommand(b' '.join(cmdline))
222 221 self.ui.debug(cmdline, b'\n')
223 222 return os.system(pycompat.rapply(procutil.tonativestr, cmdline))
224 223
225 224 def _update(self, rev):
226 225 self.ui.debug(b'applying revision %s...\n' % rev)
227 226 changeset, status = self.runlines(b'replay', b'-d', self.tmppath, rev)
228 227 if status:
229 228 # Something went wrong while merging (baz or tla
230 229 # issue?), get latest revision and try from there
231 230 shutil.rmtree(self.tmppath, ignore_errors=True)
232 231 self._obtainrevision(rev)
233 232 else:
234 233 old_rev = self.parents[rev][0]
235 234 self.ui.debug(
236 235 b'computing changeset between %s and %s...\n' % (old_rev, rev)
237 236 )
238 237 self._parsechangeset(changeset, rev)
239 238
240 239 def _getfile(self, name, rev):
241 240 mode = os.lstat(os.path.join(self.tmppath, name)).st_mode
242 241 if stat.S_ISLNK(mode):
243 242 data = util.readlink(os.path.join(self.tmppath, name))
244 243 if mode:
245 244 mode = b'l'
246 245 else:
247 246 mode = b''
248 247 else:
249 248 data = util.readfile(os.path.join(self.tmppath, name))
250 249 mode = (mode & 0o111) and b'x' or b''
251 250 return data, mode
252 251
253 252 def _exclude(self, name):
254 253 exclude = [b'{arch}', b'.arch-ids', b'.arch-inventory']
255 254 for exc in exclude:
256 255 if name.find(exc) != -1:
257 256 return True
258 257 return False
259 258
260 259 def _readcontents(self, path):
261 260 files = []
262 261 contents = os.listdir(path)
263 262 while len(contents) > 0:
264 263 c = contents.pop()
265 264 p = os.path.join(path, c)
266 265 # os.walk could be used, but here we avoid internal GNU
267 266 # Arch files and directories, thus saving a lot time.
268 267 if not self._exclude(p):
269 268 if os.path.isdir(p):
270 269 contents += [os.path.join(c, f) for f in os.listdir(p)]
271 270 else:
272 271 files.append(c)
273 272 return files
274 273
275 274 def _rendirchanges(self, src, dest):
276 275 changes = []
277 276 copies = {}
278 277 files = self._readcontents(os.path.join(self.tmppath, dest))
279 278 for f in files:
280 279 s = os.path.join(src, f)
281 280 d = os.path.join(dest, f)
282 281 changes.append(s)
283 282 changes.append(d)
284 283 copies[d] = s
285 284 return changes, copies
286 285
287 286 def _obtainrevision(self, rev):
288 287 self.ui.debug(b'obtaining revision %s...\n' % rev)
289 288 output = self._execute(b'get', rev, self.tmppath)
290 289 self.checkexit(output)
291 290 self.ui.debug(b'analyzing revision %s...\n' % rev)
292 291 files = self._readcontents(self.tmppath)
293 292 self.changes[rev].add_files += files
294 293
295 294 def _stripbasepath(self, path):
296 295 if path.startswith(b'./'):
297 296 return path[2:]
298 297 return path
299 298
300 299 def _parsecatlog(self, data, rev):
301 300 try:
302 catlog = self.catlogparser.parsestr(data)
301 catlog = mail.parsebytes(data)
303 302
304 303 # Commit date
305 304 self.changes[rev].date = dateutil.datestr(
306 dateutil.strdate(catlog[b'Standard-date'], b'%Y-%m-%d %H:%M:%S')
305 dateutil.strdate(catlog[r'Standard-date'], b'%Y-%m-%d %H:%M:%S')
307 306 )
308 307
309 308 # Commit author
310 self.changes[rev].author = self.recode(catlog[b'Creator'])
309 self.changes[rev].author = self.recode(catlog[r'Creator'])
311 310
312 311 # Commit description
313 312 self.changes[rev].summary = b'\n\n'.join(
314 (catlog[b'Summary'], catlog.get_payload())
313 (
314 self.recode(catlog[r'Summary']),
315 self.recode(catlog.get_payload()),
316 )
315 317 )
316 318 self.changes[rev].summary = self.recode(self.changes[rev].summary)
317 319
318 320 # Commit revision origin when dealing with a branch or tag
319 if b'Continuation-of' in catlog:
321 if r'Continuation-of' in catlog:
320 322 self.changes[rev].continuationof = self.recode(
321 catlog[b'Continuation-of']
323 catlog[r'Continuation-of']
322 324 )
323 325 except Exception:
324 326 raise error.Abort(_(b'could not parse cat-log of %s') % rev)
325 327
326 328 def _parsechangeset(self, data, rev):
327 329 for l in data:
328 330 l = l.strip()
329 331 # Added file (ignore added directory)
330 332 if l.startswith(b'A') and not l.startswith(b'A/'):
331 333 file = self._stripbasepath(l[1:].strip())
332 334 if not self._exclude(file):
333 335 self.changes[rev].add_files.append(file)
334 336 # Deleted file (ignore deleted directory)
335 337 elif l.startswith(b'D') and not l.startswith(b'D/'):
336 338 file = self._stripbasepath(l[1:].strip())
337 339 if not self._exclude(file):
338 340 self.changes[rev].del_files.append(file)
339 341 # Modified binary file
340 342 elif l.startswith(b'Mb'):
341 343 file = self._stripbasepath(l[2:].strip())
342 344 if not self._exclude(file):
343 345 self.changes[rev].mod_files.append(file)
344 346 # Modified link
345 347 elif l.startswith(b'M->'):
346 348 file = self._stripbasepath(l[3:].strip())
347 349 if not self._exclude(file):
348 350 self.changes[rev].mod_files.append(file)
349 351 # Modified file
350 352 elif l.startswith(b'M'):
351 353 file = self._stripbasepath(l[1:].strip())
352 354 if not self._exclude(file):
353 355 self.changes[rev].mod_files.append(file)
354 356 # Renamed file (or link)
355 357 elif l.startswith(b'=>'):
356 358 files = l[2:].strip().split(b' ')
357 359 if len(files) == 1:
358 360 files = l[2:].strip().split(b'\t')
359 361 src = self._stripbasepath(files[0])
360 362 dst = self._stripbasepath(files[1])
361 363 if not self._exclude(src) and not self._exclude(dst):
362 364 self.changes[rev].ren_files[src] = dst
363 365 # Conversion from file to link or from link to file (modified)
364 366 elif l.startswith(b'ch'):
365 367 file = self._stripbasepath(l[2:].strip())
366 368 if not self._exclude(file):
367 369 self.changes[rev].mod_files.append(file)
368 370 # Renamed directory
369 371 elif l.startswith(b'/>'):
370 372 dirs = l[2:].strip().split(b' ')
371 373 if len(dirs) == 1:
372 374 dirs = l[2:].strip().split(b'\t')
373 375 src = self._stripbasepath(dirs[0])
374 376 dst = self._stripbasepath(dirs[1])
375 377 if not self._exclude(src) and not self._exclude(dst):
376 378 self.changes[rev].ren_dirs[src] = dst
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (4094 lines changed) Show them Hide them
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
General Comments 0
You need to be logged in to leave comments. Login now